Implementing a Pub-Sub Based State Management Tool in React
Published: 2023-05-18 21:54:22

State management is a crucial aspect of building complex applications, especially in frameworks like React. While React provides its own state management solutions like useState and useReducer, there are cases where a more flexible approach is needed. In this article, we'll explore how to implement a simple state management tool using the pub-sub (publish-subscribe) design pattern in React. The pub-sub pattern allows components to subscribe to state changes and receive updates whenever the state is modified, providing a scalable and decoupled state management solution.

**Understanding the Pub-Sub Design Pattern:**

The pub-sub design pattern is a messaging pattern where senders of messages, called publishers, do not directly communicate with specific receivers, called subscribers. Instead, publishers "publish" messages to specific topics or channels, and subscribers "subscribe" to those topics or channels to receive messages. This decoupling allows for loose coupling between publishers and subscribers, enabling flexibility and scalability.

**Step 1: Importing Dependencies**

To get started, we need to import the necessary dependencies from the React library. We'll be using the useState and useEffect hooks, so include the following import statement:

```ts
import { useState, useEffect } from "react";
```

**Step 2: Creating the Pub-Sub Function:**

Next, let's create a createSubscribable function that sets up the pub-sub mechanism. This function will handle subscriptions, unsubscriptions, and publish messages to the subscribers:

```ts
function createSubscribable<T>() {
  const subscribers: Set<(message: T) => void> = new Set();

  return {
    subscribe(cb: (message: T) => void): void {
      subscribers.add(cb);
    },

    unsubscribe(cb: (message: T) => void): void {
      subscribers.delete(cb);
    },

    publish(message: T): void {
      subscribers.forEach((cb) => cb(message));
    },
  };
}

```

The createSubscribable function returns an object with three methods: subscribe, unsubscribe, and publish. It uses a Set to store subscriber callbacks, ensuring uniqueness.

The subscribe method adds a callback to the subscribers set, allowing components to subscribe to state changes.

The unsubscribe method removes a specific callback from the subscribers set, allowing components to unsubscribe from state changes when they are no longer interested.

The publish method iterates over all the subscribers and invokes their callback functions with the provided message.


**Step 3: Creating the State Hook:**

Now that we have the pub-sub mechanism in place, let's create the createStateHook function, which utilizes the pub-sub mechanism to manage and share state between components:

```ts
export function createStateHook<T>(
  initialValue: T
): () => [T, (value: T) => void] {
  const subscribers = createSubscribable<T>();

  return () => {
    const [value, setValue] = useState<T>(initialValue);

    useEffect(() => subscribers.subscribe(setValue), []);
    
    useEffect(() => {
      return () => {
        subscribers.unsubscribe(setValue);
      };
    }, []);

    return [
      value,
      (v: T) => {
        subscribers.publish(v);
      },
    ];
  };
}

```

The createStateHook function takes an initialValue parameter and returns a function that can be used as a custom React hook.

Inside the returned function, it initializes a state using the useState hook, with the provided initialValue. It also creates two side effects using the useEffect hook. The first side effect is triggered only once when the component mounts (empty dependency array []), and it subscribes the setValue function to the subscribers using subscribers.subscribe(setValue). The second side effect is responsible for cleaning up the subscription by calling subscribers.unsubscribe(setValue) when the component unmounts.

The returned function returns an array with two values: the current state value (value) and a function (v) to update the state. When the state is updated using the provided function, it publishes the new value to all the subscribers using subscribers.publish(v).


Step 4: Usage:

Now that we have our state management tool ready, we can use it in our React components. Here's an example of how to use the state hook createStateHook:

```ts
const useCustomState = createStateHook<string>("initial value");

function MyComponent() {
  const [state, setState] = useCustomState();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setState(e.target.value);
  };

  return (
    <div>
      <input type="text" value={state} onChange={handleChange} />
      <p>Current value: {state}</p>
    </div>
  );
}

```

In this example, we create a custom hook useCustomState using createStateHook. It returns an array with the current state value (state) and a function to update the state (setState). We can use these values to manage and update the state within our component.

The handleChange function is an event handler that updates the state whenever the input value changes.

By utilizing the pub-sub design pattern, any component that uses the useCustomState hook will automatically receive updates whenever the state changes, ensuring a synchronized and reactive UI.