T
TanStack•9mo ago
wise-white

Auto-refresh select function when Zustand state changes

Hello! I need help with an issue I'm facing when using TanStack Query with Zustand. Current Situation I'm fetching data with TanStack Query and then filtering it based on a state managed by Zustand. My selector function directly accesses the Zustand store internally:
// Selector function that directly accesses Zustand state
export const filteredTodosByStatus = (data: Todo[]) => {
// Directly accessing Zustand state internally
const status = useFilterStore.getState().status;

if (status === 'all') return data;
return data.filter(todo =>
status === 'completed' ? todo.completed : !todo.completed
);
};

// In the component
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => filteredTodosByStatus(data),
});
// Selector function that directly accesses Zustand state
export const filteredTodosByStatus = (data: Todo[]) => {
// Directly accessing Zustand state internally
const status = useFilterStore.getState().status;

if (status === 'all') return data;
return data.filter(todo =>
status === 'completed' ? todo.completed : !todo.completed
);
};

// In the component
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => filteredTodosByStatus(data),
});
Problem Description 1. The filteredTodosByStatus function internally uses useFilterStore.getState().status to get the current filter state. 2. When the user changes the filter state by clicking a filter button, TanStack Query doesn't detect this change and doesn't re-run the select function. 3. As a result, the UI doesn't update to show the filtered data when the filter state changes.
8 Replies
wise-white
wise-whiteOP•9mo ago
What I Want to Achieve - I want the select function to automatically re-run when the filter state changes, WITHOUT refetching the data. - I'd like to maintain the current code structure where the selector function internally accesses the Zustand store. What I've Tried 1. Adding the filter state to the queryKey array, but this causes the data to be refetched when the filter changes, which is not what I want. 2. Using useMemo in the component to filter the data, but this requires significant code restructuring. 3. Passing the filter state as an argument to the select function:
const filter = useFilterStore((state) => state.status);
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => filterTodosByParam(data, filter),
});
const filter = useFilterStore((state) => state.status);
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => filterTodosByParam(data, filter),
});
While this approach works as expected, I'd like to maintain the current structure where the selector function internally accesses the Zustand store. Is there a way to make TanStack Query's select function automatically re-run when the Zustand state changes, without refetching the data and while keeping the selector function's internal access to the Zustand store? Thank you!
like-gold
like-gold•9mo ago
Imperative getters like useStore.geState() will not subscribe you to changes, so no, you need to call the useStore hook in your component.
wise-white
wise-whiteOP•9mo ago
When I call useStore((state) => state.someValue) inside a select function like that, I get an error saying it violates the rules of hooks. Would it be the correct approach to create a custom hook that combines useStore((state) => state.someValue) and useQuery, and returns the desired shape?
fair-rose
fair-rose•9mo ago
just call useStore above the useQuery You want your component to rerender to re-execute the select. useStore will subscribe to the store state and rerender the component when the store state changes, hence rerunning the select. if you want it reusable, sure, make a custom hook
like-gold
like-gold•9mo ago
This is the correct answer 👍
wise-white
wise-whiteOP•9mo ago
It seems like I need to extract this logic into a custom hook, since it won’t only be used in a single component. However, I don’t quite understand the part where calling useStore above useQuery would cause the component to re-render when the store state changes, and consequently cause the select function to run again. Just writing it like this inside the component doesn’t seem to trigger the select function to recalculate when the store changes — at least not from what I’ve tested:
const filter = useFilterStore((state) => state.status);
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: filterTodosByParam,
});
const filter = useFilterStore((state) => state.status);
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: filterTodosByParam,
});
Additionally, this component does not use the filter value.
fair-rose
fair-rose•9mo ago
The select function will only run if data changed, or if the reference to the select function itself changes. To optimize, wrap the function in useCallback.
you can do it like
const filter = useFilterStore((state) => state.status);
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: () => { /* Logic here */ }, // This will create a new reference on each render, hence running select on every rerender
});

// or, more optimized, tbh, this might only be needed if your `select` function is very heavy computationally

const filter = useFilterStore((state) => state.status);
const select = useCallback(() => { /* Logic here */ }, [filter]) // Only change `select`'s reference when `filter` changes
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select,
});
const filter = useFilterStore((state) => state.status);
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: () => { /* Logic here */ }, // This will create a new reference on each render, hence running select on every rerender
});

// or, more optimized, tbh, this might only be needed if your `select` function is very heavy computationally

const filter = useFilterStore((state) => state.status);
const select = useCallback(() => { /* Logic here */ }, [filter]) // Only change `select`'s reference when `filter` changes
const { data: filteredTodos = [] } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select,
});
wise-white
wise-whiteOP•9mo ago
It works just the way I wanted! Thank you so much for your help!

Did you find this page helpful?