06. Introduction to Global State Management Libraries

Solving global state management problems, data-centric approach vs. component-centric approach, re-rendering optimization

πŸ”– 1. Two Problems in Global State Design

(1) How to Read Global State

As mentioned continuously in previous chapters, components using global state don't always need all values of the global state. When global state changes, re-rendering occurs, but re-rendering also occurs even when the changed value is unrelated to the component. Global state libraries provide solutions for such inefficient cases.

As explained in later content, this is also why global state libraries try to solve this problem through selector or atom-like structures.

(2) How to Put or Update Values in Global State

Directly modifying nested objects in global state is difficult to track and can break immutability. Rather than developers directly changing global variable values, providing dedicated functions or using closures to prevent direct access to variables makes state changes clearer and more stable to manage.

Example Code Solving Both Problems

The global state store in the example below has a nested object user, and safely changes the user.name value through the setUserName function. This way, instead of directly changing global state, changes occur through dedicated functions, making management and tracking easier.

// Store implementation for managing global state
const createStore = (initialState) => {
  let state = initialState;
  const listeners = new Set();

  // Function to read global state
  const getState = () => state;

  // Function to update global state
  const setState = (newState) => {
    //
    state = { ...state, ...newState };
    // Notify registered listeners (subscribers) when state changes
    listeners.forEach((listener) => listener());
  };

  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };

  return { getState, setState, subscribe };
};

// Initial global state: user is a nested object
const store = createStore({
  user: {
    name: 'Ella',
    age: 20,
  },
  theme: 'dark',
});

// Provide dedicated function for global state changes
function setUserName(newName) {
  // Update user object in global state while maintaining immutability
  const current = store.getState();
  store.setState({
    user: {
      ...current.user,
      name: newName,
    },
  });
}

// Usage example: change user.name using dedicated function instead of directly
setUserName('Chloe');
console.log(store.getState().user.name); // "Chloe"

πŸ”– 2. Using Data-Centric and Component-Centric Approaches

Understanding through paraphrasing in my own language

Data-Centric Approach

  • In data-centric approach, module state is managed in JavaScript memory outside of React.

  • This means the state can be maintained before React rendering starts or after all components have disappeared (unmounted).

Component-Centric Approach

  • Manages global state along with component lifecycle.

  • When all components referencing global state disappear (unmount), that state can automatically disappear.

  • Through this approach, you can also create multiple instances of the same form of global state, where each global state instance can belong to different component trees.

Factory Function

  • Global state libraries using data-centric approach provide "factory functions."

  • Factory functions themselves don't immediately create global state, but by using global state initialization functions created through this function, React can be made to take responsibility for and manage the lifecycle of that global state.

Factory Function? Factory Pattern?

"Factory Pattern is a design pattern that abstracts object creation logic into a separate 'factory'. One way to implement the concept of 'factory' is to use factory functions. In other words, within the conceptual framework of factory pattern, functions that play the role of creating objects (or components) can be called factory functions."

Component-Centric Approach Example Code

I wrote example code that briefly shows component-centric approach. When a specific component mounts, it initializes global state (store), and when the component unmounts, it cleans up that global state. This allows placing stores with the same global state schema in multiple component trees respectively.

πŸ”– 3. Re-rendering Optimization

The core of re-rendering optimization is specifying which part of state will be used in components, and there are three approaches to specify parts of state. Let's understand by paraphrasing in my own language.

Using Selector Functions

  • To optimize re-rendering, it's important to clearly specify which part of state the component actually uses.

  • Using "selector functions," one of these approaches, you can extract only the necessary parts rather than the entire state.

  • Selector functions take state as input and return specific values (or derived values).

  • At this time, by always producing the same result for the same input, parts that don't change even when state changes can avoid re-rendering.

  • This process of explicitly specifying necessary parts is called 'manual optimization.'

State Usage Tracking

  • State usage tracking is a technique that tracks which properties (state values) components access from global state or Store. Through this, components can be re-rendered only when the state values they actually reference change.

  • In other words, when a component renders, it tracks which state fields (e.g., state.count, state.text) it accesses. Based on the tracked information (which fields were read), the component is made to re-render only when those fields change. When field changes are unnecessary, unnecessary re-rendering is prevented.

  • The core of this technique is wrapping state objects with Proxy and recording which fields are accessed in the current rendering process whenever 'property access' occurs.

State Usage Tracking vs. Subscription

  • Both approaches have similar goals in that they "only re-render components when necessary state changes," but there are clear differences.

  • Subscription is a 'manual' approach where developers explicitly declare and manage dependencies.

  • State Usage Tracking is a way to 'automatically' understand dependencies during code execution.

  • Both approaches prevent unnecessary re-rendering, but State Usage Tracking seems to be able to respond flexibly to structural changes due to more automated dependency management.

Using Atoms

  • Atoms, as can be inferred from atom in atomic design, are the minimum state units used to cause re-rendering.

  • Instead of subscribing to entire global state to avoid re-rendering, using atoms allows granular subscription.

  • Atom-based approach can be seen as intermediate between manual optimization and automatic optimization. Dependency tracking is automatic.

My Thoughts

This chapter provides a comprehensive overview of global state management libraries and their different approaches. The key insight is that there are multiple ways to solve the same problems, each with their own trade-offs.

The comparison between data-centric and component-centric approaches is particularly valuable - it helps us understand when to use each approach based on our specific needs.

The discussion about re-rendering optimization techniques (selectors, usage tracking, atoms) shows how different libraries solve the same performance problems in different ways.

Code Examples

Last updated