Picture this: you’re faced with the challenge of updating state from realms outside the familiar component tree. What was once a straightforward task within React’s structured paradigm suddenly transforms into a complex puzzle. Traditional solutions falter, leaving React developers grappling with a dilemma that defies the simplicity promised by the framework.

As React developers, we love how easy it is to handle state inside our components. But things get a bit tricky when we need to update state from outside React’s usual territory. The usual ways we handle this become less helpful, and a once-simple solution becomes more complicated.

Most developers reach out to complicated global state libraries. But wait!

The Singleton Observable Pattern ✨

The “Singleton Observable Pattern” is a variation of the Observer pattern where a single observable instance is shared and accessed globally within an application. In this pattern, there is a single source of truth for state changes, and multiple observers subscribe to this shared observable to receive updates.

screenshot

Here’s how it’ll look when we’re done 🤌

// BankBalanceUtils.js
import { userState } from "./userState";

function bankBalanceUpdater() {
  // directly update property
  // updates are immediately reflected in the UI 🤯
  userState.funds += 100;
}

// BankBalance.jsx
import { useUserState } from "./userState";

const BankBalance = () => {
  const user = useUserState();

  return (
    <>
      <button
        onClick={() => {
          // directly update property
          user.funds += 100;
        }}
      >
        create money
      </button>

      {/* UI reacts to changes in user 🤯 */}
      <p>your funds : {user.funds}</p>
    </>
  );
};

Creating an Observable Singleton

  1. Creating a singleton class to store data

In the heart of our pattern lies a state object, encapsulating the data we want to observe. Utilizing JavaScript’s class syntax, we establish a singleton instance for managing state changes across our application.

Identify the properties within the SingletonObservable class that you want to observe. These could be the critical pieces of data that, when changed, should trigger reactions across your application.

class UserState {
  static instance = null;
  #observers = [];

  #funds = 0;
  #name = "";

  constructor() {
    if (UserState.instance) {
      return UserState.instance;
    }
    UserState.instance = this;
  }
}
  1. Writing Getters and Setters for Observing Properties

By employing getters and setters, we intercept the usage of these properties. The get method allows access to the state, while the set method not only updates the state but also triggers the notification process.

class UserState {
  // Previous code...

  get funds() {
    return this.#funds;
  }

  set funds(newFunds) {
    this.#funds = newFunds;
    this.notify();
  }

  get name() {
    return this.#name;
  }

  set name(newName) {
    this.#name = newName;
    this.notify();
  }
}
  1. Adding Observable Callbacks for Property Changes:

The magic happens in the notify method. Whenever a property undergoes a change, this method iterates through all registered observers, invoking their update methods and providing them with the updated state.

class UserState {
  // Previous code...

  subscribe(callBackFn) {
    this.#observers.push(callBackFn);

    // returns a function that can be called to unsubscribe from updates
    return () => this.unsubscribe(callBackFn);
  }

  notify() {
    this.#observers.forEach((callBackFn) => {
      callBackFn();
    });
  }
}
  1. Singleton UserState Instance

Ensure there’s a single instance of UserState that components can access globally. This instance will be created once and used throughout the application.

class UserState {
  // ... (previous UserState implementation)
}

// Create a singleton instance of UserState
const userState = new UserState();

// Export the instance for use in the hook
export default userState;

Create the React Hook for Subscription

import { useEffect } from "react";
import userState from "./path-to-your-userstate-file";

// Custom hook to subscribe to UserState changes
export const useUserState = (callback) => {
  const [user, setUser] = useState(userState);

  useEffect(() => {
    const unsubscribe = userState.subscribe(() => {
      setUser(userState);
    });

    userState.subscribe(observer);

    return () => {
      unsubscribe();
    };
  }, []);
};

By following this approach, you maintain a global state accessible through a singleton instance of UserState. The custom hook, useUserState(), facilitates easy subscription to state changes within React components, ensuring a reactive and straightforward approach to managing global state.

Final Thoughts

To sum it up, the way we’ve organized state in React using the Observable Singleton pattern with getters and setters is like having a neat and tidy package.

Although manually setting up getters and setters has some downsides, the cool part is that this method is all self-contained – no need for external tools or libraries!

This example provides a straightforward introduction to the Observable Singleton pattern in React, but there’s room for improvement and customization. It’s intentionally kept simple, leaving ample space for readers to innovate and tailor the approach to their specific application needs.


P.S

  • (bonus reading) : Automate getters and setters using Proxies in javascript

  • I’ve used the class syntax in the code examples above because I think they look cleaner. We can use plain objects with getters and setters to achieve the same behavior.