
useExternalSyncStore the React hook that externalises state!
January 20, 2024 12:20 PM
What is it
This new hook is available in React 18 (also has a Shim that can be used in React 17) that enables hook ups to custom stores that can publish updates into reacts internal state.
How to use this hook
First we need to create a factory function which returns our custom store, this store will have a specific boilerplate which will enable publishing a state change within react. Here is a break down the following factory function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const createStore = (initialState) => {
let currentState = initialState
const listeners = new Set<(state) => void>()
const _setInternalState = (state) => {
currentState = { ...currentState, ...state }
}
const _internalUpdateState = () => {
listeners.forEach((listener) => { listener(currentState) })
}
return {
getState: () => currentState,
setState: (state) => _setInternalState(state),
publishStateUpdate: () => _internalUpdateState(),
subscribe: (listener) => {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
},
}
}
let currentState = initialState
Keeps the current state, is updated whenever we want to change state.
const listeners = new Set<(state) => void>()
Keeps a set of all the listeners that subscribe to this stores state, each listener is a React.setState when plugged into the useSyncExternalStore
.
This function is used to update the currentState, _setInternalState
can be used internally for factory function's store methods you wish to use inside the custom store, setState
is exposing this method for changing currentState on the store:
1
2
3
4
5
6
7
// Internal
const _setInternalState = (state) => {
currentState = { ...currentState, ...state }
}
...
// External
setState: (state) => _setInternalState(state),
_internalUpdateState will publish the the stores currentState change into react setState through the useSyncExternalStore and trigger a re-render on any component listening to this state change. publishStateUpdate is the method on the store:
1
2
3
4
5
6
7
// Internal
const _internalUpdateState = () => {
listeners.forEach((listener) => { listener(currentState) })
}
...
// External
publishStateUpdate: () => _internalUpdateState(),
getState: () => currentState, Gets the most current state
The subscribe method enables the useSyncExternalStore to add a setState from React.useState as a subscriber to this store which acts as a listens on a react component for a change to the stores currentState and updates reacts internal state which will trigger a re-render on components that listen to the state change.
1
2
3
4
5
6
subscribe: (listener) => {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
},
Now we can initialise the store:
1
2
3
4
5
6
7
8
const store = createStore({
firstName: 'Kiran',
lastName: 'Earle',
team: ‘bubbles’,
role: 'FED'
})
export default store
Setting up our useStore custom hook with the useSyncExternalStore hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// React 18
import { useCallback, useSyncExternalStore } from 'react'
// Shim if using React 17
// import { useSyncExternalStore } from 'use-sync-external-store/shim'
import store from './store'
const useStore = <T>(selector = (state) => state) =>
useSyncExternalStore(
store.subscribe,
useCallback(() => selector(store.getState()), [selector])
) as T
export default useStore
The hook takes a function as an argument [selector], this enables us to target the specific property on the currentState object within your store. So that we only watch for specific state changes in a component without needless re-renders whenever state is changed:
1
2
3
4
5
const useStore = <T>(selector = (state) => state) =>
useSyncExternalStore(
store.subscribe,
useCallback(() => selector(store.getState()), [selector])
) as T
Usage
We use useStore to apply our store state into reacts internal state on the component, this component is subscribed to the changes in the store.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect } from 'react'
import useStore from './useStore'
const UserComponent = () => {
const state = useStore((state) => {
return {
firstName: state.firstName,
lastName: state.lastName
}
})
return (
<>
<p>First Name: {state.firstName}</p>
<p>Last Name: {state.lastName}</p>
</>
)
}
Here is an example of us changing the state of the component with a new first and last name. We use the setState method from our custom store to apply these changes to the currentState on the custom store. Once we are happy with our changes we can then publish the change for React.setState to trigger a re-render from our custom hook with the new state by calling the publishStateUpdate() method from our custom store.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { useEffect } from 'react'
import useStore from './useStore'
import store from './store'
const UserComponent = () => {
const state = useStore((state) => {
return {
firstName: state.firstName,
lastName: state.lastName
}
})
useEffect(() => {
const { setState, publishStateUpdate } = store
setState({
firstName: 'Alberto',
lastName: 'Ferioli',
})
publishStateUpdate()
})
return (
<>
<p>First Name: {state.firstName}</p>
<p>Last Name: {state.lastName} </p>
</>
)
}
We can make updates to our currentState as much as we want without triggering a re-render in React, then when we are ready to publish the changes on our currentState we call publishStateUpdate() which means we control when React should listen to a change in state to avoid unnecessary re-renders when we change state properties in our logic.