DEV Community

Senior Developer
Senior Developer

Posted on

Underrated React Hook - useSyncExternalStore

Image description

Overview

Discover a hidden powerhouse in the React ecosystem: the “useSyncExternalStore” hook. This article delves into its transformative potential, challenging traditional state management paradigms. By seamlessly integrating external data sources and enhancing cross-component communication, this hook offers an unconventional yet powerful approach.

Journey with us as we demystify useSyncExternalStore. We’ll dissect its mechanics, unveil its benefits, and showcase practical applications through real-world examples. By the end, you’ll grasp how to wield this hook to streamline complexity, boost performance, and bring a new level of organization to your codebase.

Usage

According to React, useSyncExternalStore is a React Hook that lets you subscribe to an external store. But what is an “external store” exactly ? It literally takes 2 functions:

  • The subscribe function should subscribe to the store and return a function that unsubscribes.
  • The getSnapshot function should read a snapshot of the data from the store. Okay it’s might be hard to get at first. We can go into the example.

The Demo

For our demo today, I will go into a classic application: The “Todo List”.

The Store

First, we have to define the initial state:

export type Task = {
  id: string;
  content: string;
  isDone: boolean;
};

export type InitialState = {
  todos: Task[];
};

export const initialState: InitialState = { todos: [] };
Enter fullscreen mode Exit fullscreen mode

You can see that I defined the types and then created the state that has todos as an empty array

Now is the reducer:

export function reducer(state: InitialState, action: any) {
  switch (action.type) {
    case "ADD_TASK":
      const task = {
        content: action.payload,
        id: uid(),
        isDone: false,
      };
      return {
        ...state,
        todos: [...state.todos, task],
      };

    case "REMOVE_TASK":
      return {
        ...state,
        todos: state.todos.filter((task) => task.id !== action.payload),
      };

    case "COMPLETE_TASK":
      const tasks = state.todos.map((task) => {
        if (task.id === action.payload) {
          task.isDone = !task.isDone;
        }
        return task;
      });
      return {
        ...state,
        todos: tasks,
      };

    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

Our reducer only has 3 actions: ADD_TASK, REMOVE_TASK and COMPLETE_TASK. This is the classic example of a to-do list logic.

Finally, what we are waiting for, the store:

let listeners: any[] = [];

function createStore(reducer: any, initialState: InitialState) {
  let state = initialState;

  function getState() {
    return state;
  }

  function dispatch(action: any) {
    state = reducer(state, action);

    emitChange();
  }

  function subscribe(listener: any) {
    listeners = [...listeners, listener];
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  }

  const store = {
    dispatch,
    getState,
    subscribe,
  };

  return store;
}

function emitChange() {
  for (let listener of listeners) {
    listener();
  }
}

export const store = createStore(reducer, initialState);
Enter fullscreen mode Exit fullscreen mode

This code snippet illustrates the creation of a simple Redux-like state management system in TypeScript. Here’s a breakdown of how it works:

  1. listeners Array: This array holds a list of listener functions that will be notified whenever the state changes.

  2. createStore Function: This function is responsible for creating a Redux-style store. It takes two parameters:

  • reducer: A reducer function responsible for calculating the next state based on the current state and dispatched action.
  • initialState: The initial state of the application.
  1. state: This variable holds the current state of the application.

  2. getState Function: Returns the current state.

  3. dispatch Function: Accepts an action object, passes it to the reducer along with the current state, updates the state with the result, and then calls the emitChange function to notify listeners about the state change.

  4. subscribe Function: Accepts a listener function, adds it to the listeners array, and returns a cleanup function that can be called to remove the listener.

  5. store Object: The created store object holds references to the dispatch, getState, and subscribe functions.

  6. emitChange Function: Iterates through the listeners array and invokes each listener function, notifying them of a state change.

At the end of the code, a store is created using the createStore function, with a given reducer and initial state. This store can now be imported and used in other parts of the application to manage and control the state.

It’s important to note that this code provides a simplified implementation of a state management system and lacks some advanced features and optimizations found in libraries like Redux. However, it serves as a great starting point to understand the basic concepts of state management using listeners and a reducer function.

To use the useSyncExternalStore hook. We can get the state like this:

const { todos } = useSyncExternalStore(store.subscribe, store.getState);
Enter fullscreen mode Exit fullscreen mode

With this hook call, we can access the store globally and dynamically, while maintain the readability and maintainability

Pros and Cons

The “useSyncExternalStore” hook presents both advantages and potential drawbacks in the context of state management within a React application:

Pros:

  1. Seamless Integration with External Sources: The hook enables effortless integration with external data sources, promoting a unified approach to state management. This integration can simplify the handling of data from various origins, enhancing the application’s cohesion.

  2. Cross-Component Communication: “useSyncExternalStore” facilitates efficient communication between components, streamlining the sharing of data and reducing the need for complex prop drilling or context management.

  3. Performance Improvements: By centralizing state management and minimizing the propagation of state updates, this hook has the potential to optimize rendering performance, resulting in a more responsive and efficient application.

  4. Simplicity and Clean Code: The hook’s abstracted API can lead to cleaner and more organized code, making it easier to understand and maintain, particularly in large-scale applications.

  5. Reduced Boilerplate: “useSyncExternalStore” may reduce the need for writing redundant code for state management, providing a concise and consistent way to manage application-wide state.

Cons:

  1. Learning Curve: Developers unfamiliar with this hook might experience a learning curve when transitioning from more established state management solutions. Adapting to a new approach could initially slow down development.

  2. Customization Limitations: The hook’s predefined functionalities might not align perfectly with every application’s unique requirements. Customizing behavior beyond the hook’s capabilities might necessitate additional workarounds.

  3. Potential Abstraction Overhead: Depending on its internal mechanics, the hook might introduce a slight overhead in performance or memory usage compared to more optimized solutions tailored specifically for the application’s needs.

  4. Community and Ecosystem: As an underrated or lesser-known hook, “useSyncExternalStore” might lack a well-established community and comprehensive ecosystem, potentially resulting in fewer available resources or third-party libraries.

  5. Compatibility and Future Updates: Compatibility with future versions of React and potential updates to the hook itself could be points of concern. Ensuring long-term support and seamless upgrades may require extra diligence.

Conclusion

In summary, useSyncExternalStore offers a unique approach to state management, emphasizing seamless integration and cross-component communication. While it provides several benefits, such as improved performance and simplified code, developers should carefully evaluate its compatibility with their project’s requirements and consider the potential learning curve and limitations.

Top comments (0)