mpen
mpen

Reputation: 282825

React server-side fetch (asynchronous rendering)

In short, how can I make this work on the server?

import React from 'react';

export default class RemoteText extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {text: null};
        fetch(props.src).then(res => res.text()).then(text => {
            this.setState({text});
        })
    }

    render() {
        if(this.state.text) {
            return <span>{this.state.text}</span>;
        }
        return null;
    }
}

Even if I use isomorphic-fetch, I get this warning:

Warning: setState(...): Can only update a mounting component. This usually means you called setState() outside componentWillMount() on the server. This is a no-op. Please check the code for the RemoteText component.

Which makes sense. I believe my component has already been rendered to a string by the time the fetch resolves. I need to somehow defer rendering until that's complete, but I don't know how.

Apollo somehow manages this with getDataFromTree so I'm sure it's possible.

How can I do the same thing? i.e., walk my component tree, extract all pending fetches and then wait for them to resolve before ReactDOMServer.renderToString?


Since it doesn't appear to be clear, I would like to reiterate that I want the fetch to happen server-side and all the data should be loaded before the first render. The fetches should be co-located with the component (probably via a HOC).

If I can get the data into my Redux store before my renderToString, I already have some code to serialize it and pass it down to the client. For reference, I'm rendering my application like this:

export default async (req,res) => {
    const rrCtx = {};
    const store = createStore(combineReducers({
        apollo: gqlClient.reducer(),
    }), {}, applyMiddleware(gqlClient.middleware()));

    const Chain = (
        <ApolloProvider store={store} client={gqlClient}>
            <StaticRouter location={req.url} context={rrCtx}>
                <App/>
            </StaticRouter>
        </ApolloProvider>
    );

    await getDataFromTree(Chain); // <--- waits for GraphQL data to resolve before first render -- I wan the same but for fetch()
    const html = ReactDOMServer.renderToString(Chain);

    if(rrCtx.url) {
        ctx.redirect(rrCtx.url);
    } else {


        const stats = await readJson(`${__dirname}/../webpack/stats.json`);
        const styles = stats.assetsByChunkName.main.filter(f => f.endsWith('.css'));
        const scripts = stats.assetsByChunkName.main.filter(f => f.endsWith('.js'));

        let manifest = {};
        try {
            manifest = await readJson(`${__dirname}/../webpack/manifest.json`);
        } catch(_){}

        if(stats.assetsByChunkName.vendor) {
            styles.unshift(...stats.assetsByChunkName.vendor.filter(f => f.endsWith('.css')));
            scripts.unshift(...stats.assetsByChunkName.vendor.filter(f => f.endsWith('.js')));
        }

        res.send(`<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="Content-Language" content="en" />
  <title>RedSpider</title>
  <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  ${styles.map(f => `<link rel="stylesheet" href="${stats.publicPath}${f}">`).join('\n')}
  <link rel="icon" type="image/png" href="/spider.png">
  ${scripts.map(f => `<script src="${stats.publicPath}${f}" defer></script>`).join('\n')}
</head>
<body>
  <div id="react-root">${html}</div>
  <script>
  window.__STATE__=${jsSerialize(store.getState())};
  window.__MANIFEST__=${jsSerialize(manifest)};
  </script>
</body>
</html>`);
    }
}

I'm looking for something akin to react-apollo but works for arbitrary fetches/GET requests and not just GraphQL queries.


Everyone's saying "just await it". Await what? The fetch is inside the component and the place where I need to await is in a different file. The problem is how do I gather up the fetch-promises such that I can await them, and then get that data back into the component?

But nevermind, I'm going to try to reverse engineer getDataFromTree and see if I can get something to work.

Upvotes: 7

Views: 4997

Answers (4)

John Weisz
John Weisz

Reputation: 31924

I believe my component has already been rendered to a string by the time the fetch resolves. I need to somehow defer rendering until that's complete, but I don't know how. [...] I can fetch the data where I call renderToString (not that I want to) but then how do I thread it back into the component?

AFAIK, you will always have to await any async calls to finish before calling renderToString into the response, one way or another.

This problem is not at all uncommon, although this is merely a generic answer, as I use a custom flux implementation instead of Redux. Additionally, this problem is not unique to server-side rendering of a React component tree, as it may just happen that you need to wait on the client before you can proceed with rendering into the DOM.

Basically, how I solve this particular problem (whether on the client or server) is the following:

  1. simply await your fetch before calling renderToString (as I mentioned, this is virtually unavoidable, as your data must exist before proceeding with the response)
  2. store the fetch results in your store
  3. in the component, only initiate a fetch for data if that particular data is not yet "preloaded" into your store

In your renderer entry point, simply (although I believe this should be obvious):

export default async (req, res) => {
    await fetch().then(res => res.text()).then(text => {
        this.setState({text});
    });

    // Store your data in your store here and instantiate your components.

    const html = ReactDOMServer.renderToString(Chain);
}

In your component:

export default class RemoteText extends React.PureComponent {
    constructor(props, context) {
        super(props, context);

        // If your store has the data you need, set it directly.
        // Otherwise, call your fetch so that it works on client seamlessly.
    }
}

In overall, this will ensure your component works just as well on the client, and has the required data render-ready when prerendering on the server. I'm afraid there is no alternative to awaiting your fetch call.

Upvotes: 2

alex1sz
alex1sz

Reputation: 340

Couple things.

Move your fetch request out of the constructor, it does not belong there. setState() also does not belong in the constructor.

The fetch request and subsequent setState() call belong in componentDidMount(), not the constructor.

Also, when you make the setState() call you are going to want setState({text: text}) not setState({text}). As a side-note, it is idiomatic to forgo blocks for arrow functions that have a single line especially when chaining fetch requests. Likewise placing chained statements on separate lines increases readability. ie:

fetch(props.src)
.then(res => res.text())
.then(text => this.setState({text: text}))

You can chain .catch(error => console.log(error)); at the end if you like. Again the important thing to do, is move your fetch request and setState() call into componentDidMount().

Defining a generic fetch method would be appropriate (especially if there will be more elsewhere); it can take a requestObj, and callback as arguments. You'd call it within in componentDidMount(), and anywhere else where fetch requests are made within your app.

In your render() call, encapsulate everything you return in a top level div. There is no need to return null in your render call, even when this.state.text is null it is still unnecessary. Instead, as you learn React especially at the beginning you're better off always returning a div. In case you haven't seen it before you have the inline operator at your disposal for conditional rendering, it look like this: render() { const text = this.state.text;

  return (
    <div>
      {text &&
        <span>{text}</span>
      }
    </div>
  );
}

Edit: I misunderstood what you are looking to do on first glance. If what you want is for all the data to be loaded before render, pass the data to the component via props. I'm not sure what the structure of your component tree looks like or what you are doing server side but if your not able to pass the data with props alone, pass it to the component via context. To do that you need a higher order component that sets your data to context. Your existing constructor would then look something like this:

constructor(props, context) {

  super(props, context);
  this.state = this.context.text || window.__INITIAL_STATE__ || {text: null};
}

Upvotes: 0

Anthony Garcia-Labiad
Anthony Garcia-Labiad

Reputation: 3711

When you call renderToString from the server, react will call the constructor, componentWillMount and render, then create a string from the rendered element. Since the async operations will be resolved on another tick, you cannot use fetch from componentWillMount or the constructor.

The simpler way to do it (if you don't use any framework/lib that handle that for you) is to resolve fetch before calling renderToString.

Upvotes: 3

genestd
genestd

Reputation: 404

As the comments mentioned, do the async operation in componentWillMount

componentWillMount=()=>{
    fetch(props.src).then(res => res.text()).then(text => {
        this.setState({text});
    })
}

Upvotes: -2

Related Questions