Sync local and remote Zustand store in ReactJS

Thomas Rutzer
2 min readJan 26, 2022

Disclaimer:
The initial concept has been refactored and optimized after internal reviews and a helpful
GitHub Discussion with one of the maintainers of Zustand. Main differences are that I would now recommend subscribe and only send the properties which have been changed, not a whole diff.

In highly specific scenarios, you might need to sync multiple identical Zustand stores. In my case, this was due to a WebRTC data sync. Other use cases might be multiple browser contexts.

If your store object is flat, it will simplify things a lot! But I think most stores consist of more complex application nested Objects. I used Lodash helper set and get and immer. Use what suits you best!

Now here is my very solution:

  1. The store
const useStore = create(set => ({
bears: 0,
nutrition: {
breakfast: ["honey"],
diner: ["nuts"]
}
}))

2. Sync with useSyncedStore

import { useEffect } from "react"
import shallow from "zustand/shallow"
import produce from "immer"
import get from "lodash.get"
import set from "lodash.set"
import { useStore } from "path/to/your/store"/**
* @param {string[]} syncPaths - Paths of the store you want to sync, e.g. ["nutrition.breakfast", "nutrition.diner"]
* @param {Object} newData - Patched data from remote store
* @param {Function} doSendData - Function will be invoked with patched local store data, up to send to remote
*/
const useStoreSync = (syncPaths, newData, doSendData) => {
/**
* This useEffect will be invoked everytime prop syncPaths change.
* It calls doSendData with currently changed state, where key is path of syncedPaths and value is the current state, e.g.:
* {
* "nutrition.breakfast": ["honey"],
* "nutrition.diner": ["nuts"]
* }
*/
useEffect(() => {
const unsubscribes = syncPaths.map(path => {
return useStore.subscribe(
state => get(state, path), changedPath => {
const sendState = { [path]: changedPath }
doSendData(sendState)
}
)
})
return () => unsubscribes.map(unsubscribe => unsubscribe())
}, [syncPaths])
/**
* This useEffect will be invoked everytime remote data (@param newData) changes.
* The local store will be updated with patch Object, which might be:
* {
* "nutrition.breakfast": ["berries", "honey"],
* }
*/
useEffect(() => {
Object.keys(newData).forEach(key => {
useStore.setState(
produce(state => {
set(state, key, newData[key])
}),
)
})
}, [newData])
}
export default useStoreSync

3. Write configurations

It’s up to you which connection you setup between local and remote store. To push updates from your local to remote provide doSendData callback. And you might have any kind of subscriptions to remote data, e.g. a WebRTC data connection like in my case, which updates param newData. syncPaths is just a string[] with all the relevant paths of your local store, which you want to sync.

That’s it! Hope it will help someone out there.

P.S.: If someone can point me to a solution how to write JavaScript snippets better in Medium articles, I’d be more than happy. Until then, I recommend to copy the code into your favorite editor for formatting and syntax highlighting.

--

--

Thomas Rutzer

hay I’m Thomas, specialized in crafting unique interfaces & interactions for the browser platform. Meet me on twitter or github: @thomasrutzer