Facebook React.js: how do you render stateful components on the server?

phtrivier picture phtrivier · Jan 7, 2014 · Viewed 12.5k times · Source

I think I'm conceptually missing something with server-side rendering using React.js

Assume I want to create a page to display items from a server-side DB, with an input field to filter them.

I want a page:

  • that responds to a URL like /items?name=foobar
  • with a React input field to filter the items by name
  • with a React component to display the list of filtered items

Assume I have a public REST API to query the items on the client side.

Conceptually, what I want to do at first request (GET /items?name=foobar) is :

  • I want my input field to show what the user passed as a Parameter, so I need to pass the query parameter ('foobar') to the react component, as a 'prop' (like initialName)

So I tried this:

 // A stateful component, maintaining the value of a query field
 var ItemForm = React.createClass({
    getInitialState : function () {
        return {
            name : this.props.initialName
        };
    },
    handleInputValueChanged : function() {
        var enteredName = this.refs.query.getDOMNode().value.trim();
        this.props.onNameChanged(enteredName);
    },
    render : function () {

        return React.DOM.form({
            children : [
                React.DOM.label({
                    children : "System name"
                }),
                React.DOM.input({
                    ref : "query",
                    value : this.state.name,
                    onChange : this.handleInputValueChanged
                })
            ]
        });
    }
});
  • I also have to do my database query on the server to get a list of items, and pass this list of items as a 'prop' of the ItemList (like 'initialItems')

As I understand it, I need a simple component to display the list, receiving it as a 'prop':

 // A stateless component, displaying a list of item
 var ItemList = return React.createClass({

    propTypes : {
        items : React.PropTypes.array
    },

    render : function () {
        return React.DOM.ul({
            children : _.map(this.props.items, function (item) {
                return React.DOM.li({
                    children : [
                        React.DOM.span({
                            children : ["Name : ", item.name].join(" ")
                        })]
                });
            })
        });
    }
});
  • Now, I need a component do display the whole Page ; this component will have to maintain the state of the whole page, that is, look at the name entered in the field, and make API queries to update the list items. However I don't understand how this component can have an 'initialState' that would work both on the server-side and for later rendering on the client side.

I tried this:

 // A stateful react component, maintaining the list of items
 var ItemPage = React.createClass({

    getInitialState : function () {
        // ?????
        // This is where I'm sure the problem lies. 
        // How can this be known both on server and client side ? 
        return {
            items : this.props.initialItems || []
        };
    },
    queryItems : function (enteredName) {

        var self = this;

        // The field was emptied, we must clear everything
        if (!enteredName) {
            this.setState({
                items : []
            });

        } else {

            // The field was changed, we want to do a query
            // then change the state to trigger a UI update. 
            // The query code is irrelevant, I think.
            doQuery(enteredName).then(function (items) {
                self.setState({
                   items : items
                });
            });
        }
    },
    render : function () {

        // I don't want to display the same view
        // if there is no results. 
        // This uses a 'state' property of the page
        var results = null;
        if (_.isEmpty(this.state.items)) {
            results = React.DOM.div({
                ref : "results",
                children : "No results"
            });
        } else {
            results = ItemListView({
                // Here items is a 'prop', the ItemList is technically
                // stateless
                items : this.state.items
            });
        }

        return React.DOM.div({
            children : [
                ItemForm({
                    initialName : this.props.initialName,
                    onNameChanged : this.queryItems
                }),
                results
            ]
        });
    }
});

That's where I'm stuck. I can render things on the server side using something like:

var name = // The name from the query parameters
var items = doQueryOnServerSide(name);
React.renderComponentAsString(ItemPage({
  initialName : name,
  initialItems : items
});

But when I try to do write the client side javascript, what should I do ? I know where I want my dom to be rendered, but what initial props should I pass to the react component?

React.renderComponent(ItemPage({
  initialName : ??? // Read the name parameter from URL ? 
  initialItems : ??? // I don't know yet, and I don't want to make an API call until the user entered something in the input box 
});

Most attempts I have tried ends up with the DOM being 'erased' on the client-side, with this message:

React attempted to use reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server.

Note that only the markup of my ItemList component is erased, so I suppose it's an implementation problem of my component, but I don't really know where to start.

Am I on the right track? Or do I miss something entirely?

Answer

Sophie Alpert picture Sophie Alpert · Jan 7, 2014

When using server rendering, you should always pass down the same props that you used to render the component on the server. In this case, you need to pass down the same initialItems prop in order for React.renderComponent to pick up the server-rendered markup (by simply JSONifying the props and putting it in the call to renderComponent).

Your general structure of reading from initialItems when specified makes sense to me. Doing so allows you to either make a component with preloaded data or one that has none. What you need to set the initial state to depends on what you want to show in cases where you're rendering a brand-new component from scratch. (If you're always picking up server-rendered markup then you can simply always pass the initialName and initialItems props.)

Hope that makes sense.