How to use useSyncExternalStore in React 18

How to use useSyncExternalStore in React 18

Dead Simple Chat Team

Table of Contents

Dead Simple Chat offers Javascript Chat API and SDK to add in-app chat to your React applications in minutes. Dead Simple Chat is a highly customizable chat solution and can be used for any chat use case.

The useSyncExternalStore is a custom hook available in React 18, that lets you subscribe to an external store and update your React component when the external store updates.

It is especially useful in subscribing to external stores that are not built on top of React state management.

useSyncExternalStore API

You should call the useSyncExternalStore method at the top level of your component.

import { useSyncExternalStore } from 'react';
import { myStore } from "./mystore.js";

function MyComponent() {
	const data = useSyncExternalStore(myStore.subscribe, myStore.getSnapshot);
    // Rest of the component ...
}

The useSyncExternalStore method accepts two parameters:

  • subscribe - The subscribe method should subscribe to the store updates, and it should return a function to unsubscribe from the store update. We will look at an example below of how to create a store to use with useSyncExternalStore.
  • getSnapshot - Method would return a snapshot of data from the store.

Basic Example using useSyncExternalStore

Let's build a very basic store to understand the useSyncExternalStore API.

Our store would store just a count and would provide a method to increment and decrement the count.

let count = 0; // Variable to store count
let subscribers = new Set(); // Set to store callback functions

const countStore = {
  read() {
    // Method to get the count, this is basically getSnapshot method.
    return count;
  },
  // Subscribe method adds the "callback" to the "subscribers" set, and
  // return a method to unsubscribe from the store.
  subscribe(callback) {
    subscribers.add(callback);
    return () => subscribers.delete(callback);
  },
  // Method to increment the count
  increment() {
    count++;
    subscribers.forEach((callback) => callback());
  },
  decrement() {
    count--;
    subscribers.forEach((callback) => callback());
  },
};

export default countStore;
store.js

The count is used to store our counter and the subscribers array store the list of subscriber methods.

The read() method is the getSnapshot() method that fetches the snapshot of the store.

The subscribe(callback) method is used to subscribe to the store, and it is in the format that is required by the useSyncExternalStore.

We are storing the callback method in a Set, and each time the count is updated we are iterating over all the callback methods and calling them.

Once the callback is called, the useSyncExternalStore will call the read() method to fetch the value from the store.

Now, let's build our component to use the store, we will update our App.js component to use our newly created store.

import "./App.css";
import countStore from "./store";
import { useSyncExternalStore } from "react";

function App() {
  const count = useSyncExternalStore(countStore.subscribe, countStore.read);

  return (
    <div className="App">
      count: {count}
      <div>
        <button onClick={countStore.increment}>Increment</button>
        <button onClick={countStore.decrement}>Decrement</button>
      </div>
    </div>
  );
}

export default App;
App.js
0:00
/
Demo of useSyncExternalStore

As you can see, when we increment or decrement the count, the component automatically updates to reflect the updated count value.

Javascript Chat API and SDK | DeadSimpleChat
Easily add chat to your website or app within seconds. Pre-built chat with API and Javascript SDK | DeadSimpleChat.

Building a Todo App using useSyncExternalStore

Let's basic a Todo application, first, we will create a store.js file that will store our todos:

let todos = [];
let subscribers = new Set();

const store = {
  getTodos() {
    // Method to get the todos array.
    return todos;
  },
  // Subscribe method adds the "callback" to the "subscribers" set, and
  // return a method to unsubscribe from the store.
  subscribe(callback) {
    subscribers.add(callback);
    return () => subscribers.delete(callback);
  },
  addTodo(text) {
    todos = [
      ...todos,
      {
        id: new Date().getTime(),
        text: text,
        completed: false,
      },
    ];

    subscribers.forEach((callback) => {
      callback();
    });
  },
  toggleTodo(id) {
    todos = todos.map((todo) => {
      return todo.id === id ? { ...todo, completed: !todo.completed } : todo;
    });
    subscribers.forEach((callback) => callback());
  },
};

export default store;
store.js

In our store.js file we have created an array called as todos to store the list of todos.

Next similar to our previous example we have created a variable called as subscribers that contains an array of callback functions that have subscribed to the store.

We would have to call these functions whenever the value of the store is updated.

In the addTodo the method you might have noticed that we are not using push method to add a todo to the todos array because by doing this React will not detect a change and reload the component because of immutability in React.

Hence you should remember not to update the todo array in place, rather creates a new array when adding a todo.

We are doing the same thing in the toggleTodo method, rather than updating the todo by index in the toggleTodo method we are creating a new todos array using the map method that contains the updated value.

Next, we will create a TodoList.js file to hold our TodoList component, the TodoList component will use the useSyncExternalStore subscribe to the store and update its UI when a new Todo is added.

import { useSyncExternalStore } from "react";
import store from "./store";
function TodoList() {
  const todos = useSyncExternalStore(store.subscribe, store.getTodos);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              value={todo.completed}
              onClick={() => store.toggleTodo(todo.id)}
            />
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </label>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;
TodoList.js

In our TodoList component we are fetching the list of todos from the store using the useSyncExternalStore hook.

We are also calling the toggleTodo method when the todo is checked, and when we will run this code you will see that the UI will be updated.

Next, we will create a AddTodoForm component, to add a new Todo, here is the code for our AddTodoForm component:

import store from "./store";
import { useState } from "react";
function AddTodoForm() {
  const [text, setText] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    store.addTodo(text);
    setText("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button type="submit">Add Todo</button>
    </form>
  );
}

export default AddTodoForm;
AddTodoForm.js

The AddTodoForm component is also very basic, here we are importing our store, and using the useState method from react.

We have created a state variable called as text that will store the text for our todo.

Next, we created two event listeners, we have added an onChange event listener on the <input /> tag, when the value of the input tag changes we are updating the state variable called as text with value typed on the input tag.

The second event listener we have added to the form, we have added an onSubmit event listener when the form is submitted by pressing the "Add Todo" button.

In the onSubmit event listener we are calling the handleSubmit method, this method, this method will call the addTodo method of our Store.

When a todo is added from this component, as we are using useSyncExternalStore hook in our TodoList component, the TodoList the component will also update automatically to display the newly added todo.

Now, lets finally build our App.js component, and import our AddTodoForm and TodoList components:

import React, { useState } from "react";

import AddTodoForm from "./AddTodoForm";
import TodoList from "./TodoList";

function App() {
  return (
    <div>
      <h1>Todo App</h1>
      <AddTodoForm />
      <TodoList />
    </div>
  );
}

export default App;
App.js

Here is the demo of our application, as you can see in the video below the Component UI is updating automatically as the data in the store updates.

0:00
/

Improving code by using custom hook

We can improve our code further, but extracting the call toe useSyncExternalStore in a custom hook and then simply using the custom hook in our components.

We will update our store.js file to include the code for our custom hook called as useTodo

import { useSyncExternalStore } from "react";


// Custom Hook useTodo
export function useTodo() {
  const todos = useSyncExternalStore(store.subscribe, store.getTodos);
  return todos;
}

// Rest of store.js code ..
store.js

Then we will update our TodoList.js file to include our custom useTodo hook:

import store, { useTodo } from "./store";

function TodoList() {
  const todos = useTodo();
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <label>
            <input
              type="checkbox"
              value={todo.completed}
              onClick={() => store.toggleTodo(todo.id)}
            />
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </label>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;
TodoList.js

When to use useSyncExternalStore ?

It is recommended that you use React's built-in state management hooks like useState and useReducer to manage the state.

But there are some scenarios where useSyncExternalStore make sense:

  • Integrating Reacting with an existing non-react codebase

If you have a non-react codebase that uses some type of external store, and you want to integrate your react application with the existing store then in that case you can build a wrapper around the store that is in line with the useSyncExternalStore API to seamlessly integrate the store with the React application.

  • Subscribing to browser APIs

You can use it to subscribe to browser APIs, like web push notifications or the navigator.onLine property.

React official documentation has a good example explaining how the use the navigator.onLine property with useSyncExternalStore hook.

You can read the doc here.

Dead Simple Chat offers Javascript Chat API and SDK to add in-app chat to your React applications in minutes. Dead Simple Chat is a highly customizable chat solution and can be used for any chat use case.

You might be interested in some of our other articles

Conclusion

In this blog post, we have learned how to use useSyncExternalStore in our code to sync with an external store in React.

We should use useState and useReducer hooks in our code whenever possible, but in a few cases where it is not possible to use the useState and useReducer we can use the useSyncExternalStore to sync our react components with an external store.