function react(gd, data, layout, config) {
    var frames, plotDone;

    function addFrames() { return exports.addFrames(gd, frames); }

    gd = Lib.getGraphDiv(gd);
    helpers.clearPromiseQueue(gd);

    var oldFullData = gd._fullData;
    var oldFullLayout = gd._fullLayout;

    // you can use this as the initial draw as well as to update
    if(!Lib.isPlotDiv(gd) || !oldFullData || !oldFullLayout) {
        plotDone = exports.newPlot(gd, data, layout, config);
    } else {
        if(Lib.isPlainObject(data)) {
            var obj = data;
            data = obj.data;
            layout = obj.layout;
            config = obj.config;
            frames = obj.frames;
        }

        var configChanged = false;
        // assume that if there's a config at all, we're reacting to it too,
        // and completely replace the previous config
        if(config) {
            var oldConfig = Lib.extendDeep({}, gd._context);
            gd._context = undefined;
            setPlotContext(gd, config);
            configChanged = diffConfig(oldConfig, gd._context);
        }

        gd.data = data || [];
        helpers.cleanData(gd.data);
        gd.layout = layout || {};
        helpers.cleanLayout(gd.layout);

        applyUIRevisions(gd.data, gd.layout, oldFullData, oldFullLayout);

        // "true" skips updating calcdata and remapping arrays from calcTransforms,
        // which supplyDefaults usually does at the end, but we may need to NOT do
        // if the diff (which we haven't determined yet) says we'll recalc
        Plots.supplyDefaults(gd, {skipUpdateCalc: true});

        var newFullData = gd._fullData;
        var newFullLayout = gd._fullLayout;
        var immutable = newFullLayout.datarevision === undefined;
        var transition = newFullLayout.transition;

        var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition);
        var newDataRevision = relayoutFlags.newDataRevision;
        var restyleFlags = diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision);

        // TODO: how to translate this part of relayout to Plotly.react?
        // // Setting width or height to null must reset the graph's width / height
        // // back to its initial value as computed during the first pass in Plots.plotAutoSize.
        // //
        // // To do so, we must manually set them back here using the _initialAutoSize cache.
        // if(['width', 'height'].indexOf(ai) !== -1 && vi === null) {
        //     fullLayout[ai] = gd._initialAutoSize[ai];
        // }

        if(updateAutosize(gd)) relayoutFlags.layoutReplot = true;

        // clear calcdata and empty categories if required
        if(restyleFlags.calc || relayoutFlags.calc) {
            gd.calcdata = undefined;
            var allNames = Object.getOwnPropertyNames(newFullLayout);
            for(var q = 0; q < allNames.length; q++) {
                var name = allNames[q];
                var start = name.substring(0, 5);
                if(start === 'xaxis' || start === 'yaxis') {
                    var emptyCategories = newFullLayout[name]._emptyCategories;
                    if(emptyCategories) emptyCategories();
                }
            }
        // otherwise do the calcdata updates and calcTransform array remaps that we skipped earlier
        } else {
            Plots.supplyDefaultsUpdateCalc(gd.calcdata, newFullData);
        }

        // Note: what restyle/relayout use impliedEdits and clearAxisTypes for
        // must be handled by the user when using Plotly.react.

        // fill in redraw sequence
        var seq = [];

        if(frames) {
            gd._transitionData = {};
            Plots.createTransitionData(gd);
            seq.push(addFrames);
        }

        // Transition pathway,
        // only used when 'transition' is set by user and
        // when at least one animatable attribute has changed,
        // N.B. config changed aren't animatable
        if(newFullLayout.transition && !configChanged && (restyleFlags.anim || relayoutFlags.anim)) {
            if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout);

            Plots.doCalcdata(gd);
            subroutines.doAutoRangeAndConstraints(gd);

            seq.push(function() {
                return Plots.transitionFromReact(gd, restyleFlags, relayoutFlags, oldFullLayout);
            });
        } else if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) {
            gd._fullLayout._skipDefaults = true;
            seq.push(exports._doPlot);
        } else {
            for(var componentType in relayoutFlags.arrays) {
                var indices = relayoutFlags.arrays[componentType];
                if(indices.length) {
                    var drawOne = Registry.getComponentMethod(componentType, 'drawOne');
                    if(drawOne !== Lib.noop) {
                        for(var i = 0; i < indices.length; i++) {
                            drawOne(gd, indices[i]);
                        }
                    } else {
                        var draw = Registry.getComponentMethod(componentType, 'draw');
                        if(draw === Lib.noop) {
                            throw new Error('cannot draw components: ' + componentType);
                        }
                        draw(gd);
                    }
                }
            }

            seq.push(Plots.previousPromises);
            if(restyleFlags.style) seq.push(subroutines.doTraceStyle);
            if(restyleFlags.colorbars || relayoutFlags.colorbars) seq.push(subroutines.doColorBars);
            if(relayoutFlags.legend) seq.push(subroutines.doLegend);
            if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles);
            if(relayoutFlags.axrange) addAxRangeSequence(seq);
            if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout);
            if(relayoutFlags.modebar) seq.push(subroutines.doModeBar);
            if(relayoutFlags.camera) seq.push(subroutines.doCamera);
            seq.push(emitAfterPlot);
        }

        seq.push(
            Plots.rehover,
            Plots.redrag,
            Plots.reselect
        );

        plotDone = Lib.syncOrAsync(seq, gd);
        if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd);
    }

    return plotDone.then(function() {
        gd.emit('plotly_react', {
            data: data,
            layout: layout
        });

        return gd;
    });
}
Plotly.react:
A plot/update method that takes the full plot state (same API as plot/newPlot)
and diffs to determine the minimal update pathway

@param {string id or DOM element} gd
     the id or DOM element of the graph container div
@param {array of objects} data
     array of traces, containing the data and display information for each trace
@param {object} layout
     object describing the overall display of the plot,
     all the stuff that doesn't pertain to any individual trace
@param {object} config
     configuration options (see ./plot_config.js for more info)

OR

@param {string id or DOM element} gd
     the id or DOM element of the graph container div
@param {object} figure
     object containing `data`, `layout`, `config`, and `frames` members