- Mounting the store with
renderHook.
- Calling store methods like
addGrocery.
- Checking if the state updates correctly.
Example test:
test("Should add a grocery item to the list", (resolve) => {
act(() => {
const theStore = store?.result.current;
theStore?.addGrocery("Bananas", 3);
store?.waitForValueToChange(() => {
try {
expect(theStore?.groceries.length).toBe(1);
resolve();
} catch (e) {
resolve(e);
}
});
});
});
The problem: The second test always returns 0 items. I’m using waitForValueToChange, but it doesn’t seem to trigger correctly after addGrocery.
How should I properly use waitForValueToChange to ensure the state updates before asserting?
I ran into this issue when testing Zustand hooks. The key is that waitForValueToChange should observe a selector before the state change happens, not inside the same act block. For example:
import { renderHook, act } from "@testing-library/react";
import { useGroceryStore } from "./groceryStore";
test("Should add a grocery item to the list", async () => {
const { result, waitForValueToChange } = renderHook(() => useGroceryStore());
await act(async () => {
const initialLength = result.current.groceries.length;
const wait = waitForValueToChange(() => result.current.groceries.length);
result.current.addGrocery("Bananas", 3);
await wait; // wait for the groceries length to update
expect(result.current.groceries.length).toBe(initialLength + 1);
});
});
The trick is calling waitForValueToChange before you trigger the state change. Otherwise, it won’t detect it.
I used to pass a callback with resolve like in your example, but waitForValueToChange returns a promise. Using async/await makes the test much cleaner:
test("Should add a grocery item", async () => {
const { result, waitForValueToChange } = renderHook(() => useGroceryStore());
const wait = waitForValueToChange(() => result.current.groceries.length);
act(() => result.current.addGrocery("Bananas", 3));
await wait;
expect(result.current.groceries.length).toBe(1);
});
No need to wrap expectations in a try/catch with resolve; the test will fail naturally if the assertion fails.
Another variation I often use is to watch the specific part of the store state that will change. This avoids flakiness if other parts of the store update:
const { result, waitForValueToChange } = renderHook(() => useGroceryStore());
await act(async () => {
const wait = waitForValueToChange(() => result.current.groceries);
result.current.addGrocery("Bananas", 3);
await wait;
expect(result.current.groceries).toEqual([{ name: "Bananas", quantity: 3 }]);
});
Watching the full groceries array instead of just its length can also help catch subtle changes like objects being added with the wrong shape.