react-server-data

Server side data loading for React

Usage no npm install needed!

<script type="module">
  import reactServerData from 'https://cdn.skypack.dev/react-server-data';
</script>

README

react-server-data is a simple API to allow pushing asynchronous data (database, network, file access, etc) from a Server Side Rendered React app to the browser. It is designed to easily slot into existing apps and to support the new server React.Suspense asynchronous rendering when it is made available.

It does this by registering "action" promises to run. Components declare what actions they will need in the render method, and then react-server-data loads all of them before the final HTML page is sent. This can also allow HTML head tags to be populated without relying on a library like react-helmet (which has asynchronous problems)

React-server-data is not a perfect solution, the current limitations means it cannot prepopulate the page from the server side (this will require the new server side React.Suspense). Suspense and hooks are going to be supported as soon as they are officially available in React.

example

Setup

Actions must be setup at the start on both the Client and Server. Server actions can talk directly to the database or API endpoint, while clients will look closer to traditional fetch/AJAX requests.

Server Side

import { ServerDataStore } from "react-server-data";

ServerDataStore.registerAction("blogs", (...options) => {
    return db.getBlogs(...options); // Some asynchronous method
});

Client Side

import { ServerDataStore } from "react-server-data";
ServerDataStore.registerAction("blogs", (...options) => {
    // This is only run if the server never sent any data (for example, when navigating in a SPA)
    return fetch("https://api.com/blogs");
});

After registering actions, you can pass down the ServerDataContext when you SSR

import { ServerDataContext, ServerDataStore } from "react-server-data";

/** Server render */
function onPageRender() {
    // Create a context store for this request
    const serverDataStore = new ServerDataStore();
    const renderedString = renderToString(
        <ServerDataContext.Provider value={serverDataStore}>
            <BlogPage/>
        </ServerDataContext.Provider>
    );

    const serverData = await serverDataStore.getDataAsString();
    const tags = await serverDataStore.getTagsAsString();

    return `<!DOCTYPE html>
    <html>
    <head>
        ${tags}
        ${serverData}
    </head>
    <body>
        ${renderedString}
    </body>
    </html>`;
}

When a component that loads data renders, it must use runAction to begin running an action, this can either be done with a context Consumer or using the static contextType.

In the future this will be much easier to do with react-hooks and React.useContext

import { ServerDataContext, ServerDataStore } from "react-server-data";

class BlogPage extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = { blogs: [] };
    }

    async componentDidMount(){
        // If the server already ran this action, getResult will load that instead of doing a fetch request
        const data = await ServerDataStore.getResult("blogs", ...optional);
        this.setState({ blogs: data.data.blogs });
    }

    render() {
        return (
            <>
                <ServerDataContext.Consumer>
                    {(store) => {
                        store.runAction("blogs", ...optional);
                    }}
                </ServerDataContext.Consumer>
                {this.state.blogs &&
                    <>
                    {/** Render the blogs here */}
                    </>
                }
            </>
        );
    }
}

/** Optionally you can use the static contextType instead */
class BlogPage extends React.PureComponent {
    static contextType = ServerDataContext;
    constructor(props) {
        super(props);
        this.state = { blogs: [] };
    }

    async componentDidMount(){
        // If the server already ran this action, getResult will load that instead of doing a fetch request
        const data = await ServerDataStore.getResult("blogs", ...optional);
        this.setState({ blogs: data.data.blogs });
    }

    render() {
        this.context.runAction("blogs", ...optional);
        return (
            <>
                {this.state.blogs &&
                    <>
                    {/** Render the blogs here */}
                    </>
                }
            </>
        );
    }
}

Can also push meta tags out by using ServerDataTag

import { ServerDataTag } from "react-server-data";

function BlogPage(){
    return (
        <>
            <ServerDataTag action="blog" args={[...options]}>
                {(blog) =>
                    <>
                        <title>{blog.title}</title>
                        <meta name="description" content={blog.short_description} />
                        <meta property="og:title" content={blog.title} />
                        <meta property="og:description" content={blog.short_description} />
                    </>
                }
            </ServerDataTag>
            // Rest of the page as normal
        </>
    );
}