Underrated React Hook - useSyncExternalStore
Senior Developer
Posted on July 9, 2024
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: [] };
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;
}
}
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);
This code snippet illustrates the creation of a simple Redux-like state management system in TypeScript. Here’s a breakdown of how it works:
listeners
Array: This array holds a list of listener functions that will be notified whenever the state changes.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.
state
: This variable holds the current state of the application.getState
Function: Returns the current state.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.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.store
Object: The created store object holds references to the dispatch, getState, and subscribe functions.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);
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:
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
Posted on July 9, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.