DEV Community

Cover image for Server Side Rendering(SSR) With "State Pool" React State Manager
Yezy Ilomo
Yezy Ilomo

Posted on

Server Side Rendering(SSR) With "State Pool" React State Manager

Introduction

Since I wrote the blog "You Can Definitely Use Global Variables To Manage Global State In React", I've been getting a lot of questions asking whether it's possible to use State Pool if you are using server side rendering(SSR) approach.

The answer to this question is YES, YOU CAN, it's actually very easy to do SSR with State Pool.

Server rendering

The most common use case for server-side rendering is to handle the initial render when a user (or search engine crawler) first requests our app. When the server receives the request, it renders the required component(s) into HTML string, and then sends it as a response to the client. From that point on, the client takes over rendering duties.

When using State pool with server side rendering, we must also send the state of our app along in our response, so the client can use it as the initial state. This is important because, if we preload any data before generating the HTML, we want the client to also have access to this data. Otherwise, the markup generated on the client won't match the server markup, and the client would have to load the data again.

To send the data down to the client, we need to:

  • Create a fresh, new state pool store instance on every request
  • Pull the state out of store
  • And then pass the state along to the client.

On the client side, a new store will be created and initialized with the state provided from the server.

State pool's only job on the server side is to provide the initial state for our app.

Implementation

Now let's write code, we're going to create a file and name it ssr.js, that's where we are going to put all the code which will help us achieve server side rendering.


// ssr.js

import React from 'react';
import { store } from 'state-pool';


const PRELOADED_STATE = '__PRELOADED_STATE__';

function initializeClientStoreByUsingServerState(serverState) {
    for (let key in serverState) {
        store.setState(key, serverState[key]);
    }
}


function initializeStore(initializeStoreNormally) {
    if (typeof window !== 'undefined' && window[PRELOADED_STATE]) {
        // We're on client side and there're states which have been sent from a server
        // So we initialize our store by using server states
        let states = JSON.parse(window[PRELOADED_STATE]);
        initializeClientStoreByUsingServerState(states);
    }
    else {
        // We're on server side or on client side without server state
        // so we initialize the store normally
        initializeStoreNormally(store);
    }
}


function getServerStatesToSendToClient() {
    let states = {}
    for (let key in store.value) {
        states[key] = store.value[key].getValue();
    }
    return JSON.stringify(states);
}


function Provider({ children }) {
    const script = {
        __html: `window.${PRELOADED_STATE} = '${getServerStatesToSendToClient()}';`
    }

    return (
        <>
            <script dangerouslySetInnerHTML={script} />
            {children}
        </>
    );
}


const SSR = {
    Provider: Provider,
    initializeStore: initializeStore
};

export default SSR;
Enter fullscreen mode Exit fullscreen mode

Believe it or not, that's all we need to use State Pool in SSR. Now let's use the code we have written above to write SSR app. We are going to use NextJS for server side rendering.

import { useGlobalState } from 'state-pool'
import SSR from '../ssr';  // From the file we wrote before


function lastUpdateLocation() {
    if (typeof window !== 'undefined') {
        return "client side";
    }
    return "server side"
}

SSR.initializeStore((store) => {
    store.setState("state", {
        "count": 0,
        "lastUpdateLocation": lastUpdateLocation()
    });
});


function Counter() {
    const [state, setState] = useGlobalState("state");

    const setCount = (count) => {
        setState({
            "count": count,
            "lastUpdateLocation": lastUpdateLocation()
        })
    }

    return (
        <center>
            <br /><br />
            <br /><br />
            {state.count}
            <br /><br />
            <button onClick={() => setCount(state.count - 1)}>Decrement</button>
            &#160;--&#160;
            <button onClick={() => setCount(state.count + 1)}>Increment</button>
            <br /><br />
            Last updated on {state.lastUpdateLocation}
        </center>
    )
}

export default function Home() {
    return (
        <SSR.Provider >
            <Counter />
        </SSR.Provider>
    )
}
Enter fullscreen mode Exit fullscreen mode

So what happens here is that we have a global state and we are tracking where it was last updated(Whether on server side or client side)

Below is the result of our app

You can see from our app that when it starts it shows the global state was last updated on server, that's because with SSR, states are initialized on server side.

After incrementing or decrementing, it says the global state was last updated on client side, which makes sense because immediate after receiving a response from a server the client took over rendering duties which means any update done from that point would be client's doing.

Security Considerations

Because we have introduced more code that relies on user generated content and input, we have increased our attack surface area for our application. It is important for any application that you ensure your input is properly sanitized to prevent things like cross-site scripting (XSS) attacks or code injections.

For our simplistic example, coercing our input into a number is sufficiently secure. If you're handling more complex input, such as freeform text, then you should run that input through an appropriate sanitization function.

Here is the repository for the demo app if you want to play with it.

Congratulation for making to this point, I would like to hear from you, what's your opinion on this?.

Discussion (0)