How can I properly test a Zustand store hook and wait for state updates? I’m trying to test a Zustand store (`groceryStore`) by:

  1. Mounting the store with renderHook.
  2. Calling store methods like addGrocery.
  3. 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.