How to create global state management in React applications without side dependencies and unnecessary rerendering First of all, I would like to give special thanks to for inspiring this topic. Jack Herrington has made a significant advance in the tools that help us create global state management. React has pushed us to a new level, and ’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. React Redux Today, we’ll learn about the hook and add polish to create state management without unnecessarily rendering our components. useSyncExternalStore Application structure Before diving into the code, let’s check the structure of our test application. Each component will iterate with the global store. We need that structure to know which component will be rerendered with the store’s manipulation. Create components To simplify the development process, I will use , and choose React with JavaScript. https://vitejs.dev/ Here are a few steps to get started: Run yarn create vite Choose your project name, framework: , and language variant: React javascript Add the following: and cd project_name yarn Remove in to avoid a second rerender <React.StrictMode> main.jsx Now, let’s create components in . There should be four files with the following names: , , , and . src/components Header.jsx Body.jsx Shares.jsx Footer.jsx Create store In the folder, create the folder and put the following two files into it: and . src store initialState.js index.js For , I put some nested objects and values that will get updated in our application. initialState.js Here’s what the code looks like: export const initialState = { name: "John", age: 30, status: "active", details: { shares: [ { symbol: "AAPL", price: 100, }, { symbol: "MSFT", price: 200, }, { symbol: "GOOG", price: 300, }, ], }, }; Before creating the function, let’s figure out how the store should work and what we should expect. Similar to Redux, we can use hooks or with and apply them across the application. useState useContexts useReducer Let’s check the implementation with , as shown below: useState import { useState } from "react"; import { initialState } from "./initialState.js"; function createStore() { return { useStore() { return useState(initialState); }, }; } export default createStore(); As you can see, we’re gonna reuse the across the application. No magic; it’s a simple implementation that clarifies which component will be rerendered after manipulating the store. useState Let’s update our with : App.jsx store.useStore() import "./App.css"; import { Footer } from "./components/Footer/index.jsx"; import store from "./store/index.js"; function App() { console.log("App updated"); const [state, setState] = store.useStore(); return ( <div> <div>My value: {state.name}</div> <div>My value: {state.age}</div> <button onClick={() => setState((prevStore) => { return { ...prevStore, name: "New name", age: 100, }; }) } > Update Shares From App </button> <Footer /> </div> ); } And let’s have access the current state with the following code: Footer.jsx import store from "../../store"; export const Footer = () => { const [state] = store.useStore(); console.log("Footer updated"); return ( <footer> <p>Footer</p> <p>Status: {state.status}</p> </footer> ); }; Now run the app with and hit the button with an open console. yarn dev Update Shares From App You will see that all of our components are updated. In , we will read from the unmodified store, and this will always return . Footer status active But the problem is we didn’t update any values in 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 . Here’s how to do that: Footer useSelector 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 to the name of that component. An alternative solution is to use Google Chrome with React Developer Tools. console.log Here’s what that looks like: Now, will use from the created store. Footer.jsx useSelector import { useSelector } from "../../store"; export const Footer = () => { console.log("Footer updated"); const status = useSelector((state) => state.status); return ( <footer> <p>Footer</p> <p>Name: {status}</p> </footer> ); }; For the , we’re gonna only use from store. Header setState import { setState } from "../../store"; export const Header = () => { console.log("Header updated"); return ( <header> <p>Header</p> <button onClick={() => setState((prevStore) => { return { ...prevStore, name: "Michael", age: 99, }; }) } > Update Name And Age from Header </button> </header> ); }; And for the , we’re gonna use both functions to update and read the store. Body import { useSelector, setState } from "../../store"; export const Body = () => { const name = useSelector((state) => state.name); const age = useSelector((state) => state.age); console.log("Body updated"); return ( <div className="body"> <h1>Body</h1> <p>Name: {name}</p> <p>Age: {age}</p> <button onClick={() => setState((prevStore) => { return { ...prevStore, name: "Michael", age: 99, }; }) } > Update Name And Age from Body </button> </div> ); }; And the last section, , will use the store to read data. Shares import { useSelector } from "../../store"; export const Shares = () => { const { shares } = useSelector((state) => state.details); console.log("Shares updated") return ( <div className="shares"> <h1>Shares</h1> <ul> {shares.map(({ symbol, price }) => { return ( <li key={price + symbol}> {symbol} : {price} </li> ); })} </ul> </div> ); }; Finally, to wrap them all in one application, let’s put the components into . To check the component’s render as well, we’ll use the function. App.jsx App setState import "./App.css"; import { Header } from "./components/Header/index.jsx"; import { Body } from "./components/Body/index.jsx"; import { Shares } from "./components/Shares/index.jsx"; import { Footer } from "./components/Footer/index.jsx"; import { setState } from "./store/index.js"; function App() { console.log("App updated"); return ( <div> <Header /> <Body /> <Shares /> <button onClick={() => setState((prevStore) => { const newShare = { symbol: "XRP", price: 1.27472, }; const share = prevStore.details.shares.find( (share) => share.symbol === newShare.symbol ); if (!share) { return { ...prevStore, details: { ...prevStore.details, shares: [...prevStore.details.shares, newShare], }, }; } else { return prevStore; } }) } > Update Shares From App </button> <Footer /> </div> ); } export default App; To highlight our components, let’s use to add borders. App.css header, footer, .body, .shares { border: 1px solid #2c2c2c; } Store and Listeners To prevent unnecessary redrawing, we need to create a function. This will improve ’s implementation with . notifies React about store changes. Inside , let’s create the function with . useSelector createStore subscribe Subscribe createStore.js subscribe listeners function createStore() { let state = initialState; const listeners = new Set(); const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); }; // ... With this technic, we can subscribe to our store and notify React about changes. As you can see, this function will also return and allow us to unsubscribe. This technic came from the which lets you subscribe and unsubscribe for changes. To receive notifications about changes, we must create another function, . listeners.delete publisher-subscriber pattern setState const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); }; return { setState(callback) { state = callback(state); listeners.forEach((listener) => listener(state)); } } The listener is always gonna get the current state and set it to . listeners And the last part, the function, uses the hook and lets us get all the changes from our store. createStore useSelector useSelector(selector) { return selector(state) }, 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 function to the hook from React. subscribe useSyncExternalStore This hook takes three arguments: , , and to render on the server side. subscribe getSnapshot getServerSnapshot useSelector(selector) { return useSyncExternalStore( subscribe, () => selector(state) ); } The function will register a callback to notify us about store changes. And combining and will return our store’s current state. In this case, we won’t be using server-side rendering for a while. subscribe () => selector(state) getSnapshot import { useSyncExternalStore } from "react"; import { initialState } from "./initialState.js"; function createStore() { let state = initialState; const listeners = new Set(); const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); }; return { setState(callback) { state = callback(state); listeners.forEach((listener) => listener(state)); }, useSelector(selector) { return useSyncExternalStore(subscribe, () => selector(state)); }, }; } const store = createStore(); export const { setState, useSelector } = store; Now, let’s run our server and check how the components will re-render. You will see something like this: yarn dev By clicking on the button, , the store’s data will update. This data is used only in , and that’s the only component that has to be rerendered because other components didn’t receive updates. Update Shares From App Shares.jsx Now, click on , and you will see that updates only happen in . And if you click again, nothing is gonna rerender because the data is the same. This is absolutely fine. Update Name And Age from Header Body.jsx What About Server-Side Rendering To sync the server-side data and store, we need to improve the function. To test that, I suggest you create a application and apply our created components to the view. While you’re at it, add the function to provide additional changes to the store’s data. createStore Next JS index getServerSideProps export async function getServerSideProps() { return { props: { initialState: { ...initialState, name: "Black", }, }, }; } To apply new store data from our view, we have to initialize our store with server data from . props export default function Home(props) { console.log("Home updated"); store.init(props.initialState); return ( <div> <Header /> <Body /> <Shares /> <Footer /> </div> ); } The function should get a new state and apply that to our current state. Here’s what that looks like: init import { useSyncExternalStore } from "react"; import { initialState } from "./initialState.js"; function createStore() { let state = initialState; let listeners = new Set(); let isInitialized = false; // ... return { init: (initState) => { if (!isInitialized) { state = initState; } }, // ... }; } const store = createStore(); export default store; export const { setState, useSelector } = 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 re-rendering. The hook 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. useSyncExternalStore Resources GitHub Repo: https://github.com/antonkalik/global-store Also published here.