Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React server-side fetch (asynchronous rendering)

Tags:

reactjs

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.

like image 223
mpen Avatar asked Jun 27 '17 17:06

mpen


People also ask

How do I fetch async data in React?

From the response object you can extract data in the format you need: JSON, raw text, Blob. Because fetch() returns a promise, you can simplify the code by using the async/await syntax: response = await fetch() .

Is React server-side or client-side rendering?

React along with other framework like angular and vue. js are traditional client side framework ,they run in browser but there are technology to run this framework on server side, and next.

Does React render asynchronously?

This means that while the component Shows is waiting for some asynchronous operation, such as fetching shows from TVMaze's API, React will render <p>loading... </p> to the DOM instead. The Shows component is then rendered only after the promises and APIs are resolved.

Is React CSR or SSR?

In fact, it was Facebook's release of the React library that popularised a CSR approach to applications by making it more technologically accessible. On the other hand, a web-based app serving mainly static content, like this website, would be expected to opt for an SSR approach.


2 Answers

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.

like image 196
Anthony Garcia-Labiad Avatar answered Oct 10 '22 09:10

Anthony Garcia-Labiad


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.

like image 20
John Weisz Avatar answered Oct 10 '22 08:10

John Weisz