T
TanStack•17mo ago
ambitious-aqua

Initializing/synching Zustand stores with React Query

I know that we typically do not want to sync React Query data to Zustand, because React Query already does a good job of managing server state. But I was wondering if there is anything wrong with using React Query data to initialize a Zustand store if we know that we need to change the data locally before saving it back to our database. For example, I am building a bulk email tool that lets you select multiple recipients using a virtual table. An email draft may already have some recipients selected, so when the page loads I want to fetch the email and initialize a Zustand context + store using any existing contacts from the email. Once this is done, users can select/deselect contacts in the table, which Zustand manages, and if they hit Save then I use a React Query mutation to update the Email in the database. The code looks similar to this:
const { data: email, isLoading, isError } = useQuery(emailQueries.detail(emailId))

if (isLoading) { // ... }
if (isError) { // ... }

<ContactsProvider initialContacts={email.contacts}>
<ContactsTable />
</ContactsProvider>
const { data: email, isLoading, isError } = useQuery(emailQueries.detail(emailId))

if (isLoading) { // ... }
if (isError) { // ... }

<ContactsProvider initialContacts={email.contacts}>
<ContactsTable />
</ContactsProvider>
I'm following @TkDodo 🔮 's Zustand context pattern (https://tkdodo.eu/blog/zustand-and-react-context) so the ContactsProvider only creates the store once. This all works great, but it does feel a little odd to me because prior to using Zustand, my typical workflow was to fetch from React Query, make some changes, mutate, invalidate, refetch, and update the UI. But with the code above I'm taking an initial snapshot of the data, and then never reading from it again. There's enough complexity inside of the ContactsTable that it is helpful to move a lot of its state management into Zustand. I guess I'm wondering if other folks have used this approach before of snapshotting state into a Zustand store, or if I'm going off in the wrong direction.
Zustand and React Context
Zustand stores a global and don't need React Context - but sometimes, it makes sense to combine them regardless.
9 Replies
dependent-tan
dependent-tan•17mo ago
What you are trying to do sounds like an antipattern, but I'm not an expert. A few things to think about: - You can change react query's state client-side without a mutation, and without a network request. by calling queryClient.setQueryData(). - There is a very helpful library called React Hook Form, that is very good at managing all kinds of forms. It even has a hook (useFieldArray) just for complex and dynamic table data inputs. If it were me, I would utilize React Hook Form for the form state, and then pass the data to a useMutation when submitting it. If you needed access it globally for some reason, I would use the queryClient.useQueryData(). Though that seems unlikely.
ambitious-aqua
ambitious-aquaOP•16mo ago
@7Samurai I think we're talking about slightly different things. Let me try to rephrase the question. Let's say my UI has two tabs, and each tab lets you edit part of an email. In TabA you can edit the From Address and the Subject, in TabB you can select the recipients from a table. Both Tabs have a save button, which lets you save just that portion of the email. This data all lives on a single email object:
type Email = {
fromAddress: string
subject: string
recipients: string[] // e.g. foo@bar.com, baz@qux.com, etc.
}
type Email = {
fromAddress: string
subject: string
recipients: string[] // e.g. foo@bar.com, baz@qux.com, etc.
}
I want to be able to save in either tab and not have it overwrite the in-progress work in the other tab when the invalidated query refetches. I described how I solved this with Zustand, but it feels a bit weird. I think another option would be to make the components in each tab uncontrolled. Initialize them from React Query but subsequent updates do not overwrite the form or table selection. This is how a library like Formik works, where you can set initialValues on a form, but if your component renders again, it ignores initialValues so it doesn't throw away the user's work. This would allow the user to start editing the form in TabA, switch to TabB and select a contact, press Save, and then switch back to TabA to see that their in-progress work is still there.
dependent-tan
dependent-tan•16mo ago
In React Hook Form land, they call a multi-page form a 'Wizard Form' or 'Funnel' https://react-hook-form.com/advanced-usage#WizardFormFunnel You have the right idea tho. Store it in a global state manager, and on the final page of the flow, you can submit it all as one update to the useMutation. You don't need RHF if you don't want it. Your original question was about syncing zustand with useQuery. And I think the answer is that you don't need to. useQuery is for displaying 'server state' and what you are looking for really is a local 'global state' You could just load the useQuery data into Zustand if/when you resume a saved form.
Performant, flexible and extensible forms with easy-to-use validation.
flat-fuchsia
flat-fuchsia•14mo ago
Hey I was wondering if you had a solution for this? I think you're describing the exact same issue I am having currently. In brief: 1. There is data stored in a database, that is fetched with react query (with useQuery) 2. I need to let users work with the data locally, making small, rapid, changes, before hitting a "submit" button and sending it off to the server (using useMutation). 3. Because of this, I create a local, temporary store of data initialized by the result of the useQuery result.
const App = () => {

const { data: initialData } = useDataQuery()
const [newData, setNewData] = useState(initialData? || [])

return (
<div className="...">
<MyDataComponent data={newData} setData={setNewData} />
</div>
}
const App = () => {

const { data: initialData } = useDataQuery()
const [newData, setNewData] = useState(initialData? || [])

return (
<div className="...">
<MyDataComponent data={newData} setData={setNewData} />
</div>
}
The issue with this is that initialData is (initially) undefined on component mount because its fetching still. My solution to this was to just use an effect:
const App = () => {

const { data: initialData } = useDataQuery()
const [newData, setNewData] = useState(initialData? || [])

useEffect(() => {
if (initialData) {
setNewData(initialData)
}
}, [initialData])

return (
<div className="...">
<MyDataComponent data={newData} setData={setNewData} />
</div>
}
const App = () => {

const { data: initialData } = useDataQuery()
const [newData, setNewData] = useState(initialData? || [])

useEffect(() => {
if (initialData) {
setNewData(initialData)
}
}, [initialData])

return (
<div className="...">
<MyDataComponent data={newData} setData={setNewData} />
</div>
}
It honestly works? But I keep being told its an antipattern, and I agree it's a bit messy and confusing. I'm trying to solve some bugs and smooth out the code base, and targeting this pattern I am using as a potential culprit. I'm curious if you (or anyone) has a solution for this? This is a pattern I use quite regularly... I think that useQuery ad useMutation work super well for 95% of use cases. But, occasionally, I run into scenarios where there are complex forms or data requirements and I need to have a sort of local version of the new data before I send it off to the server for an update, invaldiate, etc etc etc
ratty-blush
ratty-blush•14mo ago
You may want to read this, it's a bit different example from what you're doing but it has the same idea behind it. https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
You Might Not Need an Effect – React
The library for web and native user interfaces
flat-fuchsia
flat-fuchsia•14mo ago
I have read it; probably the most important piece of React documentation that exists haha. I dont know how well it answers the question though. I stumbled on this: https://github.com/TanStack/query/discussions/6220 And there still doesnt seem to be a clear answer, but perhaps react-hook-form is a good solution for this. TkDodos blog post on it: https://tkdodo.eu/blog/react-query-and-forms
GitHub
Best practice for derived local states updates · TanStack query · D...
Hi all, my company uses react-query extensively to sync and update server states in our react frontend. Recently, we have seen a pattern emerging and I'm not sure whether we are following the b...
React Query and Forms
Forms tend to blur the line between server and client state, so let's see how that plays together with React Query.
ratty-blush
ratty-blush•14mo ago
Then you know everything you need to accomplish what you're doing. General rule of thumb to use RQ as a readonly state you should never modify values inside RQ or use it for intermediate state. Here's a quote from TkDodo.
Don't use the queryCache as a local state manager
If you tamper with the queryCache (queryClient.setQueryData), it should only be for optimistic updates or for writing data that you receive from the backend after a mutation. Remember that every background refetch might override that data, so use something else for local state.
Don't use the queryCache as a local state manager
If you tamper with the queryCache (queryClient.setQueryData), it should only be for optimistic updates or for writing data that you receive from the backend after a mutation. Remember that every background refetch might override that data, so use something else for local state.
A good example of intermediate/local state are forms. It's a temporary state of form before you submit it to the BE. You initialize it with RQ data, store it in a useState/react-hook-form/formik/zustandect. modify it and submit to BE. It's that simple.
flat-fuchsia
flat-fuchsia•14mo ago
Yes. I agree 100%. But this:
You initialize it with RQ data, store it in a useState/react-hook-form/formik/zustandect. modify it and submit to BE. It's that simple.
it didn't seem like there was an agreed-upon, idiomatic approach to this. Hence, this OP's original question, my question, and the GitHub link... But, after resaerch and thinking, I suppose some mixture of react-hook-form, higher-order-components (key props), and zustand seems to be the solution. I do appreciate all the insight and I think this rabbit hole has made me think differently (more correctly) about React and state
ratty-blush
ratty-blush•14mo ago
It wasn't agreed upon because there's no single approach to it. Also, you must take into account the API of the library you're using. For example, formik uses enableReinitialize: true to override the initialState when RQ data is fetched. react-hook-form uses a slightly different approach for this.

Did you find this page helpful?