Architecting Custom State Management in React

Architecting Custom State Management in React

Β·

9 min read

Introduction

Hello everyone! πŸ‘‹, I was just exploring state management tools in React JS and thought of replicating the same using core React APIs. So here I am in front of you all with this new blog.

While well-established libraries like Redux and Zustand offer powerful solutions, understanding the core principles behind them can deepen your grasp of React's capabilities. In this blog, we'll embark on a journey to construct a state management system using a custom hook called useCustomeState.


Let's Go πŸš€

What is State Management?

State management lies at the heart of every modern web application, enabling efficient data sharing and synchronization among different parts of the user interface.

In today's web apps, we split the design into smaller parts, like building blocks, called components. These components come together to make a complete web page. Now, to handle information for all these components, we need a storage place. This storage lets components grab the data and show it to users on the page.

And when this information changes, the component automatically updates on the page, without needing to reload the whole page.

Basic functionality of State Management

We'll create a central store to store and manage data. This data will be accessible to components that have subscribed and depend on it.

Additionally, when the state changes, the subscribed components should receive notifications to update the component with the new state value.


πŸ’‘
Before we begin, it's important to have a clear understanding of React Custom Hooks. For more information, you can refer to the official React documentation on this topic.

Steps

  1. Create the Central Store to update and access the data.

  2. Create the Custome Hook to inject into the components, if any state change occurs the components will be notified and updated.

  3. Use the Hooks in the required components.

Create the central store.

  1. Create react project:

    npx create-react-app custom-state-app

  2. Inside your project directory, create a lib folder. In this folder, create a new file named customState.js

  3. Let's create the central store.

const createStore = (initialState = {}) => {
  let state = initialState;
  const subscribers = [];

  const getState = () => state;

  const updateState = (newState) => {
    state = { ...state, ...newState };
    subscribers.forEach((subscriber) => subscriber(state));
  };

  const subscribe = (callback) => {
    subscribers.push(callback);
    return () => {
      const index = subscribers.indexOf(callback);
      if (index !== -1) subscribers.splice(index, 1);
    };
  };

  return { getState, updateState, subscribe };
};
  1. In this above code, we have created the store where the data is managed with a few helper functions like getState to read the data and setState to update the data

  2. If you take a closer look, you'll notice that we've also created another function called the subscribe function. This function is what enables a component to subscribe to state changes.

  3. Also in the setState the function we are looping into the subscribers array and executing the registered callback functions on every state change.

What's important to grasp about the function above is that it contains three key helper functions:

  1. getState: This function allows us to read the data from the global state.

  2. setState: Used to update the data within the global state.

  3. subscribe: This function stores references to callback functions defined in the components. These callbacks are executed whenever there's a change in the state.


πŸͺCreate a custom hook to read, update and subscribe components to the global state.

This is the step where all the magic happens, let's understand the importance of this custom hook.

Thing's to understand from this custom hook:

  1. How does this useCustomeState hook help to update the component or re-render the component whenever the state value changes?

  2. How does this useCustomeState custom hook act as the messenger between the components that use this hook and the createStore(global data store)?

  3. How this hook binds the component to the createStore(global store)?

Let's create the hook to inject them into the components.

import { useEffect } from 'react';

// hook that subscribes and updates the child component.
const useCustomeState = (store) => {
  const [state, setState] = useState(store.getState());

  const handleStateChange = (newState) => {
    setState(newState);
  };

  // run the side effect on the state
  useEffect(() => {
    const unsubscribe = store.subscribe(handleStateChange);
    return () => {
      unsubscribe();
    };
  }, []);

  return { state, setState };
};

export { createStore, useCustomeState };

In this above code, we created a hook that takes the store object ({ getState, setState, subscribe }) returned from the createStore() function.

The point we need to understand from the above code is, we pass the initial state value coming from the createStore function itself.

But what it additionally does is, it runs the side effect using useEffect , whenever the state changes it passes the callback function and stores them in the subscribers array.

πŸ€” What is this callback function?

  1. When we use these useCustomState hooks in components, we're essentially injecting the code from the useCustomState function directly inside the components themselves.

  2. When these components are executed, the state and the useEffect defined in the hooks also come into play.

  3. This means that the callback functions we pass to the store's subscribe function are individually defined within these components. We then simply hand over references to these functions and store them in the subscribers array.

  4. Whenever the state changes, the program goes through the subscribers array and triggers these callback functions that were defined. As a result, the state gets updated in the component's local state that uses useCustomState hook, leading the component to refresh and display the new state value.

This is how the components subscribe to the global state created by the createStore function using the useCustomState hook as a messenger.

Here's a consolidated response to the top three questions that we asked ourselves on useCustomState hook.

In the useEffect hook, the store.subscribe method is employed to link the handleStateChange function with any alterations to the global state. This implies that whenever a change occurs in the global state (for instance, through the updateState function of the global store), the handleStateChange function will be invoked.

When the handleStateChange function is triggered (owing to modifications in the global state), it updates the local state of the component using the setState function. Consequently, this action initiates a re-render of the component, causing the new global state to be visually reflected in the user interface.


Create the components and inject the hook.

Let's build a counter application in React that uses our custom state management library for managing the counter state just like using Redux and Zustand.

We're going to make two components, A and B. These components will use our custom hook to read and update data in a global state. When we change something in the global state, the fresh and updated value should automatically show up on the page in both components.

Let's put all the code together and create the components.

Hook and createStore code:

// lib/customeState.js
import { useEffect, useState } from "react";

const createStore = (initialState = {}) => {
  let state = initialState;
  const subscribers = [];

  const getState = () => state;

  const updateState = (newState) => {
    state = { ...state, ...newState };
    subscribers.forEach((subscriber) => subscriber(state));
  };

  const subscribe = (callback) => {
    subscribers.push(callback);
    return () => {
      const index = subscribers.indexOf(callback);
      if (index !== -1) subscribers.splice(index, 1);
    };
  };

  return { getState, updateState, subscribe };
};

// hook that subscribes and updates the component.
const useCustomeState = (store) => {
  const [state, setState] = useState(store.getState());

  // this function is passed as the callback to subscribe function
  const handleStateChange = (newState) => {
    setState(newState);
  };

  // run the side effect on the state
  useEffect(() => {
    const unsubscribe = store.subscribe(handleStateChange);
    return () => {
      unsubscribe();
    };
  }, []);

  return { state, setState };
};
export { createStore, useCustomeState };

In this above code, we just created the store and useCustomeState on a single page and exported both functions.


Let's create and initialize the store object to use that in the components.

// store.js

import { createStore } from "./customState";

const initialState = { counter: 0 };
const store = createStore(initialState);

export default store;

Component A:

// components/ComponentA.js

import React from "react";
import { useCustomeState } from "../lib/customState";
import store from "../lib/store";

const ComponentA = () => {
  const { state } = useCustomeState(store);

  const incrementCounter = () => {
    const newCounterValue = state.counter + 1;
    store.updateState({ counter: newCounterValue });
  };

  return (
    <div>
      <h2>Component A</h2>
      <p>Counter: {state.counter}</p>
      <button onClick={incrementCounter}>Increment Counter</button>
    </div>
  );
};

export default ComponentA;

Component B:

// components/ComponentB.js

import React from "react";
import { useCustomeState } from "../lib/customState";
import store from "../lib/store";

const ComponentB = () => {
  const { state } = useCustomeState(store);

  return (
    <div>
      <h2>Component B</h2>
      <p>Counter: {state.counter}</p>
    </div>
  );
};

export default ComponentB;

App.js :

// App.js

import React from "react";
import ComponentA from "./components/ComponentA";
import ComponentB from "./components/ComponentB";

function App() {
  return (
    <div className="App">
      <h1>Counter App</h1>
      <ComponentA />
      <ComponentB />
    </div>
  );
}

export default App;

πŸ€” Why we should not directly update the state values, but rather use the function to update the state?

Now we can also correlate and understand why we should always use the function to update the state value while using the useState hook in react.

The reason is if we update the state value directly react cannot re-render the component with the new state values.

In our example the ComponentA which uses updateState funciton from the createStore also triggers the componentB to re-render with the latest values, by looping the subscriber array and executing the callback functions defined inside the components.

const setState = (newState) => {
    state = { ...state, ...newState };
    subscribers.forEach((subscriber) => subscriber(state));
  };

If we don't use the updateState function to modify the state, the state value may change, but the components subscribed to this state change will not receive notifications.

Once you run this counter app, you'll notice that components A and B are subscribed to the global store. While the state update occurs only in componentA, but both componentA and componentB receives the latest values. This behavior occurs because both components are subscribed to the global store using the useCustomState hook. As a result, any state change triggers updates and re-renders in both components.

Conclusion:

I tried my best to explain and make you all understand on building your custom own state management library using React.

If you have any questions or regards please feel free to reach out to me on social networks, and let's have a chat there on this topic.

It's time to say bye now πŸ‘‹. See you all in the next one, until that keep writing some code.

Happy Coding πŸ‘¨β€πŸ’».

Resources:

Source Code: GitHub

Did you find this article valuable?

Support Basavaraj Patil by becoming a sponsor. Any amount is appreciated!

Β