BinaryMuse / fluxxor

:hammer_and_wrench: Flux architecture tools for React
http://fluxxor.com/
MIT License
1.69k stars 154 forks source link

create example using server API #5

Open ahdinosaur opened 10 years ago

ahdinosaur commented 10 years ago

this would be very helpful. :)

skmasq commented 10 years ago

+1

bitmage commented 10 years ago

I think this question was more pertaining to having some sort of running server API connected to the front end Fluxxor / React code. The examples you have are interesting, but only demonstrate client code in isolation.

The default scenario that most people will want to see is connecting to a REST API. A more interesting scenario is connecting to a websockets API that would stream changes in the data (ala Firebase, Meteor, or some similar approach).

BinaryMuse commented 10 years ago

@bitmage You're right; I meant to reference #6. Thanks!

bitmage commented 10 years ago

Cool! Would you like help building some examples?

On Jul 4, 2014, at 4:28 PM, Brandon Tilley notifications@github.com wrote:

@bitmage You're right; I meant to reference #6. Thanks!

— Reply to this email directly or view it on GitHub.

BinaryMuse commented 10 years ago

I have a couple demos/examples in various stages of completeness, but ideas and code are always welcome and appreciated!

jwalton commented 10 years ago

A few random thoughts about this:

First, our objective is to build an isomorphic app, so we want to render an HTML page on the server which contains fully rendered React views and which has Javascript which prepopulates all the stores and then calls into React to hook up our event handlers.

My first thought is that we probably do not want to run the same actions on the client and server; the client side may do Ajax requests which go fetch data from the server, but server side we don't want to send HTTP Ajax requests to ourselves; we want to replace these calls with synchronous or asynchronous calls. Server side there also may not be any need to fire a bunch of actions through flux in the first place, as it may be more desirable to just build an initial view of the stores through plain old sync and async calls.

If we head this way, note that if your components use FluxMixin or StoreWatchMixin, then you must pass a flux instance to your components, even when rendering them on the server, because these two classes try to find flux in componentWillMount() which is called on both the client and the server. It's tempting to say that in the case of StoreWatchMixin, this block should be moved to componentDidMount(), which is only called client side - this way server side we can just pre-populate the stores and we don't need to bother hooking up any listeners. Alternatively, StoreWatchMixin could take an optional parameter listenToStoresOnServer which controls whether this is done in componentWillMount() or componentDidMount().

Rendering the react components on the server is straightforward, but we also need to marshal the contents of the stores into a JSON string that we can pass down to the client. Ideally this should be straightforward without having to write a lot of boilerplate code. Stores can store data fairly arbitrarily, though. Perhaps if we defined a toJSON() and a fromJSON(jsonString) on each Store, then on the server we could call flux.storesToJSON() to get a big JSON object of all the stores, which we could embed in a <script> tag, then client side call flux.storesFromJSON(jsonStores) to prepopulate the stores before our initial render.

How does this sound? Do you have some completely other way of doing this in mind? :)

BinaryMuse commented 10 years ago

@jwalton I think this is right on. I think you're right about moving the StoreWatchMixin code to componentDidMount; in fact, the current implementation will probably leak on the server.

ptomasroos commented 10 years ago

@jwalton look at the components yahoo has open sourced? https://github.com/yahoo/flux-examples

ftorre104 commented 9 years ago

+1 for a server-side rendered example. Fluxxor is great; would love to use it for our isomorphic app. IMO Yahoo's implementation is too involved, they give you more of a complete framework rather than a library.

ftorre104 commented 9 years ago

I have been able to get fluxxor to work with server side rendering, but feel it is not 100%. Functioning implementation: Create a flux object on the server and pass it into your App before you renderToString:

app.route('/*').get(function(req, res, next) {
    var path = url.parse(req.url).pathname;
    var stores = {
        CommentStore: new CommentStore(),
        TodoStore: new TodoStore()
    };
    var flux = new Fluxxor.Flux(stores, actions);
    var AppElement = React.createElement(App, {
        flux: flux,
        path: path
    });
    var markup = React.renderToString(AppElement);
    res.send(markup);
});

Then in your client side bootstrap code, you have to recreate the same flux object:

if (typeof window !== 'undefined') {
    // trigger render to bind UI to application on front end
    window.onload = function() {
        console.log('RUNNING ON CLIENT: re-render app on load to initialize UI elements and all')
        var path = url.parse(document.URL).pathname;
        var stores = {
            TodoStore: new TodoStore(),
            CommentStore: new CommentStore()
        };
        var flux = new Fluxxor.Flux(stores, actions);
        flux.on("dispatch", function(type, payload) {
            if (console && console.log) {
                console.log("[CLIENT Dispatch]", type, payload);
            }
        });
        // re-render on client side with same information to bind UI actions!
        React.render(React.createElement(App, {
            flux: flux,
            path: path
        }), document);
    };
}

The code above works, however I see it potentially having some drawbacks, particularly because we are recreating the flux object. 1) Stores could have changed between server side rendering and client side re-rendering (not sure if this would be an invariant error, though - ??) 2) Duplication of code in the creation of the stores -- you have to keep up with 2 spots. I guess this can be outsourced to a CommonJS module or somthing?

I guess what inherently bothers me is that it allows for inconsistencies between server/client version.

I would love to be able to pass the flux object down to the client: either

markup += '<script id="flux">window.flux = ' + JSON.stringify(flux) + '</script>';

or

markup += '<script id="flux" type="application/json">' + JSON.stringify(flux) + '</script>';

and then simply retrieve it in the bootstrap client code.

However whenever I try to do this, I get an error. Apparently the flux object is too cool.

TypeError: Converting circular structure to JSON

Any thoughts? Is it safe to create the flux object separately? Or is there another way to pass it down?

ftorre104 commented 9 years ago

For anyone interested, @BinaryMuse has a good solution. Since the flux object is circular, you can't really serialize it (as shown above).

Here's the example I followed: https://github.com/BinaryMuse/isomorphic-fluxxor-experiment/

Brief overview: 1) Modify your stores such that: -- all state data is held in a this.state -- add methods serialize() and hydrate() that call JSON.stringify() and JSON.parse(), respectively, on the this.state

Example:

var ExampleStore = Fluxxor.createStore({
    initialize: function() {
        // fetch initial data here!
        this.state = {
            page: {
                title: 'test@aol.com',
                layout: 'Test Account',
                content: '<h1> this is a page </h1><div> content goes here! </div>'
            }
        };
        this.state = {
            todos: [],
            otherData: {}
        };
        // BIND actions that this store can take to dispatcher actions
        this.bindActions(
            constants.ADD_TODO, this.onAddTodo,
            constants.TOGGLE_TODO, this.onToggleTodo,
            constants.CLEAR_TODOS, this.onClearTodos
        );
    },
    serialize: function() {
        return JSON.stringify(this.state);
    },
    hydrate: function(json) {
        this.state = JSON.parse(json);
    },
    // when the ADD_TODO action is called, the dispatcher notifies this store
    // and this store runs this function
    onAddTodo: function(payload) {
        // save data to server here
        this.todos.push({
            text: payload.text,
            complete: false
        });
        this.emit('change');
 ....

2) Extend your flux object with 2 methods: serialize() and hydrate()

var flux = new Fluxxor.Flux(stores, actions);

// Add our own custom serialize and hydrate methods.
flux.serialize = function() {
    var data = {};
    for (var key in stores) {
        data[key] = stores[key].serialize();
    }
    return JSON.stringify(data);
};

flux.hydrate = function(data) {
    for (var key in data) {
        console.log(key)
        stores[key].hydrate(data[key]);
    }
};

3) On pass the serializedFlux into your render function on the server (client too, but in a second!)

        var serializedFlux = flux.serialize();
        var AppElement = React.createElement(App, {
            flux: flux,
            path: path,
            serializedFlux: serializedFlux
        });
        var markup = React.renderToString(AppElement);
        if (markup.indexOf('NotFoundPage') > -1) {
            console.log('404 page not found!');
            res.status(404);
        }
        res.send(markup);

4) Include the serializedFlux in your template

var App = React.createClass({
    displayName: 'App',
    mixins: [FluxMixin],
    test: function(){
        console.log('hey');
    },
    render: function() {
        return  <html>
                    <AppHead />
                    <body>
                        <AppRoutes path={this.props.path} />
                        <script type='text/javascript' src={formatTo.cdn('/dist/js/bundle.js')}/>
                        <script id="serializedFlux" type="application/json" dangerouslySetInnerHTML={{__html: this.props.serializedFlux}} />
                    </body>
                </html>;

5) When you are going to rehydrate, create a new flux object and then hydrate it with the serializedFlux data

var React = require('react');
var App = require('./App');
var flux = require('./flux');

if (typeof window !== 'undefined') {
    window.onload = function() {
        console.log('Rehydrating application.');
        flux.on('dispatch', function(type, payload) {
            if (console && console.log) {
                console.log('[CLIENT Dispatch]', type, payload);
            }
        });
        var path = window.location.pathname;
        var serializedFlux = document.getElementById('serializedFlux').innerHTML;
        flux.hydrate(JSON.parse(serializedFlux));
        var AppElement = React.createElement(App, {
            flux: flux,
            path: path,
            serializedFlux: serializedFlux
        });
        React.render(AppElement, document);
    }
}

And that should do it!

BinaryMuse commented 9 years ago

Thanks for the note, @ftorre104. The example currently on master at https://github.com/BinaryMuse/isomorphic-fluxxor-experiment/ seems to work out pretty well, but it has some caveats (see the bottom of the readme). I'm toying with some other ideas, and I'll get an example into the Fluxxor examples folder before too long. :)

ftorre104 commented 9 years ago

Ah that's true. I set it up to only do one render on the server. I call an action serverFetchAsync() that makes all the calls. Once the calls are finished, it will render:

Server:

....
var App = require('../app/App');
var flux = require('../app/flux');
....

server.route('/*').get(function(req, res, next) {
    var path = url.parse(req.url).pathname;
    flux.actions.serverFetchAsync(path, doRender);

    function doRender() {
        var serializedFlux = flux.serialize();
        var AppElement = React.createElement(App, {
            flux: flux,
            path: path,
            serializedFlux: serializedFlux
        });
        var markup = React.renderToString(AppElement);
        if (markup.indexOf('NotFoundPage') > -1) {
            console.log('404 page not found!');
            res.status(404);
        }
        res.send(markup);
    }
});

And:

var Actions = {
    serverFetchAsync: function(path, callback) {
        var asyncCallCount = 0;
        if (path.indexOf('user') > -1) {
            // ex. mydomain.com/user/123
            // can be improved to better path matching!
            asyncCallCount++;
            request.get('https://mydomain.com/api/v1/user/123', function(err, response, body) {
                this.dispatch(constants.USER_DETAILS_SUCCESS, body);
                finished();
            }.bind(this));
        }
        if (path.indexOf('posts') > -1) {
            asyncCallCount++;
            var url = 'https://mydomain.com/api/v1/posts'
            request.get(url, function(err, response, body) {
                this.dispatch(constants.USERS_SUCCESS, body);
                finished();
            }.bind(this));
        }
        var finished = _.after(asyncCallCount, callback);
    },

Actions will be dispatched w/ payloads to the stores listening to them. You can then load the data into the relevant store. After that, your app will be rendered on the server.

jwalton commented 9 years ago

@BinaryMuse A slight suggested variation on what you've got going on in https://github.com/BinaryMuse/isomorphic-fluxxor-experiment/. Instead of throwing away the initial result from the first server side render, just return it.

The idea I'm proposing here is, on the server side, stores should basically not do any async work - if they have the data cached, return it, and if not, return a loading token (and then maybe go kick off the async call so the result will be cached for later, if you want to be fancy.) If we render a page with "Loading...", so be it; we return that to the client. Client side, when we do our client-side re-render, components will ask the stores for this data again, and if they returned a loading token on the server, they still won't have them, so they'll kick off an async call to the server to go fetch this data.

There are a couple of advantages here; first and foremost we return something to the client as quickly as possible, and our user is not left staring at a loading bar (especially good if our async call is taking a while for some reason.) Second, this neatly solves the caveat you mention where one async call completes, which adds new widgets, which make more async calls. This cascade all happens on the client now.

BinaryMuse commented 9 years ago

@jwalton I could see this technique being useful for a subset of applications, but one of the draws of server-rendering with React is SEO; in these cases, we would want to have content before we render.

ptomasroos commented 9 years ago

We're doing it wrong. Lets just make something that no one else does, just reuse the routes and pipe out the JSON results needed for the stores ?

Proof: http://www.onebigfluke.com/2015/01/experimentally-verified-why-client-side.html