Better Programming

Advice for programmers.

Follow publication

React State Management Without Dependencies

Anton Kalik
Better Programming
Published in
5 min readJan 27, 2023
Photo by Luca Bravo on Unsplash

First of all, I would like to give special thanks to Jack Herrington for inspiring this topic.

React has made significant advances in the tools that help us create global state management. React has pushed us to a new level, and Redux’s huge boilerplate base and Mobx’s decorators have sunk into oblivion. Creating your own global state management without side dependencies is an easy challenge.

Today, we’ll learn about the useSyncExternalStore hook and add polish to create state management without unnecessarily rendering our components.

Application Structure

Before starting the code, let’s check the structure of our test application. Each component will do iterations with the global store. We need that structure to know which component will be rerendered with the store’s manipulation.

Application components

Create Components

To simplify the development process, I’m gonna use https://vitejs.dev/, and choose React with JavaScript. Here’s the steps to do get started:

  • Run yarn create vite
  • Choose your project name, framework: React, and language variant: javascript
  • Add the following: cd project_name and yarn
  • Remove <React.StrictMode> in main.jsx to avoid a second rerender

Now, let’s create components in src/components. There should be four files with the following names: Header.jsx, Body.jsx, Shares.jsx, and Footer.jsx.

Create Store

In the src folder, create the store folder and put the following two files into it: initialState.js and index.js.

For initialState.js, I put some nested objects and values that will get updated in our application. Here’s what the code looks like:

Initial state

Before creating the function, let’s figure out how the store should work and what we should expect. Similar to Redux, we can use useState hooks or useContexts with useReducer and apply them across the application. Let’s check the implementation with useState, as shown below:

Create store function

As you can see, we’re gonna reuse the useState across the application. No magic; it’s a simple implementation that clarifies which component will be rerendered after manipulating the store. Let’s update our App.jsx with store.useStore():

Application component

And let’s have Footer.jsx access the current state with the following code:

Footer component
  • Run the app with yarn dev and hit the button Update Shares From App with an open console.

You will see that all of our components are updated. In Footer, we will read status from the unmodified store, and this will always return active.

But the problem is we didn’t update any values in Footer because we got an updated object that rerendered the component. To avoid rerendering, we will create a selector and read the store from the hook useSelector. Here’s how to do that:

const status = useSelector((state) => state.status);

The hook will use the function selector to get the current state from our store.

Update Components

Now, let’s create our remaining components. In each component, we’re gonna add console.log to the name of that component. An alternative solution is to use Google Chrome with React Developer Tools. Here’s what that looks like:

Highlight updates when components render in React Developer Tools

Now, Footer.jsx will use useSelector from the created store.

Footer with useSelector

For the Header, we’re gonna only use setState from store.

Header with setState

And for the Body, we’re gonna use both functions to update and read the store.

Body component

And the last section, Shares, will use the store to read data.

Finally, to wrap them all in one application, let’s put the components into App.jsx. To check the App component’s render as well, we’ll use the setState function.

To highlight our components, let’s use App.css to add borders.

header, footer, .body, .shares {
border: 1px solid #2c2c2c;
}

Store and Listeners

To prevent unnecessary redrawing, we need to create a useSelector function. This will improve createStore’s implementation with subscribe. Subscribe notifies React about store changes. Inside createStore.js, let’s create the subscribe function with listeners.

Create a store with subscribe function

With this technic, we can subscribe to our store and notify React about changes. As you can see, this function will also return listeners.delete and allow us to unsubscribe. This technic came from the publisher-subscriber pattern which lets you subscribe and unsubscribe for changes. To receive notifications about changes, we must create another function, setState.

Notify React about changes

The listener is always gonna get the current state and set it to listeners.

And the last part, the createStore function, uses the useSelector hook and lets us get all the changes from our store.

use selector hook

But in this case, we are not gonna be able to get updated data because we are not subscribed to our changes from the state. To fix that, we have to apply the subscribe function to the useSyncExternalStore hook from React. This hook takes three arguments: subscribe, getSnapshot, and getServerSnapshot to render on the server side.

use selector hook

The subscribe function will register a callback to notify us about store changes. And combining () => selector(state) and getSnapshot will return our store’s current state. In this case, we won’t be using server-side rendering for a while.

Create store function

Now, let’s run our server yarn dev and check how the components will rerender. You will see something like this:

The result of rendered components

By clicking on the button, Update Shares From App, the store’s data will update. This data is used only in Shares.jsx , and that’s the only component that has to be rerendered because other components didn’t receive updates.

Now, click on Update Name And Age from Header, and you will see that updates only happen in Body.jsx. And if you click again, nothing is gonna rerender because the data is the same. This is absolutely fine.

What About Server-Side Rendering

To sync the server-side data and store, we need to improve the createStore function. To test that, I suggest you create a Next JS application and apply our created components to the index view. While you’re at it, add the getServerSideProps function to provide additional changes to the store’s data.

index.js main route

To apply new store data from our view, we have to initialize our store with server data from props.

The init function should get a new state and apply that to our current state. Here’s what that looks like:

Create store

The assignment will happen only once for the view.

Conclusion

It’s fascinating! With one function, we solved the global state management problem without any boilerplate code or unnecessary rerendering. The hook useSyncExternalStore helps us synchronize our store with our React application’s state. Just one function can connect our global store’s values across the entire application.

Resources

GitHub Repo: https://github.com/antonkalik/global-store

Want to Connect?

I'll be glad to keep in touch through Twitter.

It's always a pleasure to receive suggestions and comments
related to the topic. Feel free to ask any questions. Thank you!
Anton Kalik
Anton Kalik

Written by Anton Kalik

Senior Software Engineer at Amenitiz

Responses (2)

Write a response