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?.

Oldest comments (3)

Collapse
 
lincolnwdaniel profile image
Lincoln W Daniel • Edited

Thanks for making this package, it's wonderfully simple and effective. However, it certainly is not well suited for SSR as it stands because the global state is shared across the NodeJs server, so all requests would share the same state.

A little more work is needed to make it work with SSR, but that entails creating a new store for each request and using the Context API & hooks to distribute that store to components that need to make use of the store. This also means the singleton store pattern has to be eliminated.

Please let me know if there's something I'm missing.

Collapse
 
yezyilomo profile image
Yezy Ilomo

I think a fresh store is created in each request, so it wouldn’t be possible for two requests to share a global state, you can try to recreate an example where two requests would return different global values and see if they get merged or not, feel free to correct me if you get different results, am here for it.

Collapse
 
lincolnwdaniel profile image
Lincoln W Daniel

Can you point me to where the fresh store is being created? All throughout the code for the package, it's using the singleton that's exported. I already fixed the problem as I described because I tested setting values on the store with the incoming request ID as the key during SSR from multiple browsers and saw that all the values were in the store across the requests.