From 7f41b3a0437c25eb696a7a6c19732d23d46198fb Mon Sep 17 00:00:00 2001
From: Daniel Chen <305872926+daniel-chen-j@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:04:43 +0100
Subject: [PATCH 1/2] docs: add Zustand cookbook recipe
---
examples/cookbook/app/index.tsx | 3 +-
.../app/state-management/zustand/TaskList.tsx | 52 +++++
.../zustand/__tests__/TaskList.test.tsx | 48 ++++
.../zustand/__tests__/test-utils.tsx | 33 +++
.../app/state-management/zustand/index.tsx | 7 +
.../app/state-management/zustand/state.ts | 36 +++
.../app/state-management/zustand/types.ts | 4 +
examples/cookbook/package.json | 3 +-
.../12.x/cookbook/state-management/_meta.json | 5 +-
.../12.x/cookbook/state-management/zustand.md | 221 ++++++++++++++++++
.../13.x/cookbook/state-management/_meta.json | 5 +-
.../13.x/cookbook/state-management/zustand.md | 221 ++++++++++++++++++
.../14.x/cookbook/state-management/_meta.json | 5 +-
.../14.x/cookbook/state-management/zustand.md | 221 ++++++++++++++++++
14 files changed, 859 insertions(+), 5 deletions(-)
create mode 100644 examples/cookbook/app/state-management/zustand/TaskList.tsx
create mode 100644 examples/cookbook/app/state-management/zustand/__tests__/TaskList.test.tsx
create mode 100644 examples/cookbook/app/state-management/zustand/__tests__/test-utils.tsx
create mode 100644 examples/cookbook/app/state-management/zustand/index.tsx
create mode 100644 examples/cookbook/app/state-management/zustand/state.ts
create mode 100644 examples/cookbook/app/state-management/zustand/types.ts
create mode 100644 website/docs/12.x/cookbook/state-management/zustand.md
create mode 100644 website/docs/13.x/cookbook/state-management/zustand.md
create mode 100644 website/docs/14.x/cookbook/state-management/zustand.md
diff --git a/examples/cookbook/app/index.tsx b/examples/cookbook/app/index.tsx
index 26aa26261..ade46c822 100644
--- a/examples/cookbook/app/index.tsx
+++ b/examples/cookbook/app/index.tsx
@@ -87,8 +87,9 @@ type Recipe = {
const recipes: Recipe[] = [
{ id: 1, title: 'Welcome Screen with Custom Render', path: 'custom-render/' },
{ id: 2, title: 'Task List with Jotai', path: 'state-management/jotai/' },
+ { id: 3, title: 'Task List with Zustand', path: 'state-management/zustand/' },
{
- id: 3,
+ id: 4,
title: 'Phone book with\na Variety of Net. Req. Methods',
path: 'network-requests/',
},
diff --git a/examples/cookbook/app/state-management/zustand/TaskList.tsx b/examples/cookbook/app/state-management/zustand/TaskList.tsx
new file mode 100644
index 000000000..224aa926b
--- /dev/null
+++ b/examples/cookbook/app/state-management/zustand/TaskList.tsx
@@ -0,0 +1,52 @@
+import 'react-native-get-random-values';
+
+import { nanoid } from 'nanoid';
+import * as React from 'react';
+import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
+
+import { useTaskStore } from './state';
+
+export function TaskList() {
+ const tasks = useTaskStore((state) => state.tasks);
+ const newTaskTitle = useTaskStore((state) => state.newTaskTitle);
+ const setNewTaskTitle = useTaskStore((state) => state.setNewTaskTitle);
+ const addTask = useTaskStore((state) => state.addTask);
+
+ const handleAddTask = () => {
+ addTask({
+ id: nanoid(),
+ title: newTaskTitle,
+ });
+ };
+
+ return (
+
+ {tasks.map((task) => (
+
+ {task.title}
+
+ ))}
+
+ {!tasks.length ? No tasks, start by adding one... : null}
+
+ setNewTaskTitle(text)}
+ />
+
+
+ Add Task
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+});
diff --git a/examples/cookbook/app/state-management/zustand/__tests__/TaskList.test.tsx b/examples/cookbook/app/state-management/zustand/__tests__/TaskList.test.tsx
new file mode 100644
index 000000000..3089426d7
--- /dev/null
+++ b/examples/cookbook/app/state-management/zustand/__tests__/TaskList.test.tsx
@@ -0,0 +1,48 @@
+import { render, screen, userEvent } from '@testing-library/react-native';
+import * as React from 'react';
+
+import { addTask, getAllTasks, useTaskStore } from '../state';
+import { TaskList } from '../TaskList';
+import { Task } from '../types';
+import { renderWithStore } from './test-utils';
+
+jest.useFakeTimers();
+
+afterEach(() => {
+ useTaskStore.getState().reset();
+});
+
+test('renders an empty task list', async () => {
+ await render();
+ expect(screen.getByText(/no tasks, start by adding one/i)).toBeOnTheScreen();
+});
+
+const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }];
+
+test('renders a to do list with 1 items initially, and adds a new item', async () => {
+ await renderWithStore(, {
+ initialState: {
+ tasks: INITIAL_TASKS,
+ newTaskTitle: '',
+ },
+ });
+
+ expect(screen.getByText(/buy bread/i)).toBeOnTheScreen();
+ expect(screen.getAllByTestId('task-item')).toHaveLength(1);
+
+ const user = userEvent.setup();
+ await user.type(screen.getByPlaceholderText(/new task/i), 'Buy almond milk');
+ await user.press(screen.getByRole('button', { name: /add task/i }));
+
+ expect(screen.getByText(/buy almond milk/i)).toBeOnTheScreen();
+ expect(screen.getAllByTestId('task-item')).toHaveLength(2);
+});
+
+test('modify store outside of components', () => {
+ useTaskStore.setState({ tasks: INITIAL_TASKS });
+ expect(getAllTasks()).toEqual(INITIAL_TASKS);
+
+ const NEW_TASK = { id: '2', title: 'Buy almond milk' };
+ addTask(NEW_TASK);
+ expect(getAllTasks()).toEqual([...INITIAL_TASKS, NEW_TASK]);
+});
diff --git a/examples/cookbook/app/state-management/zustand/__tests__/test-utils.tsx b/examples/cookbook/app/state-management/zustand/__tests__/test-utils.tsx
new file mode 100644
index 000000000..4242d1dde
--- /dev/null
+++ b/examples/cookbook/app/state-management/zustand/__tests__/test-utils.tsx
@@ -0,0 +1,33 @@
+import { render } from '@testing-library/react-native';
+import * as React from 'react';
+
+import { useTaskStore } from '../state';
+import { Task } from '../types';
+
+export type TaskStoreInitialState = {
+ tasks?: Task[];
+ newTaskTitle?: string;
+};
+
+export interface RenderWithStoreOptions {
+ initialState?: TaskStoreInitialState;
+}
+
+/**
+ * Renders a React component with a Zustand store hydrated for testing.
+ *
+ * Resets the store before each render so tests stay isolated, then optionally
+ * applies `initialState` via `setState`.
+ */
+export async function renderWithStore(
+ component: React.ReactElement,
+ options: RenderWithStoreOptions = {},
+) {
+ useTaskStore.getState().reset();
+
+ if (options.initialState) {
+ useTaskStore.setState(options.initialState);
+ }
+
+ return await render(component);
+}
diff --git a/examples/cookbook/app/state-management/zustand/index.tsx b/examples/cookbook/app/state-management/zustand/index.tsx
new file mode 100644
index 000000000..c5dc45f24
--- /dev/null
+++ b/examples/cookbook/app/state-management/zustand/index.tsx
@@ -0,0 +1,7 @@
+import * as React from 'react';
+
+import { TaskList } from './TaskList';
+
+export default function Example() {
+ return ;
+}
diff --git a/examples/cookbook/app/state-management/zustand/state.ts b/examples/cookbook/app/state-management/zustand/state.ts
new file mode 100644
index 000000000..8808b7611
--- /dev/null
+++ b/examples/cookbook/app/state-management/zustand/state.ts
@@ -0,0 +1,36 @@
+import { create } from 'zustand';
+
+import { Task } from './types';
+
+type TaskState = {
+ tasks: Task[];
+ newTaskTitle: string;
+ setNewTaskTitle: (title: string) => void;
+ addTask: (task: Task) => void;
+ reset: () => void;
+};
+
+const initialState = {
+ tasks: [] as Task[],
+ newTaskTitle: '',
+};
+
+export const useTaskStore = create((set) => ({
+ ...initialState,
+ setNewTaskTitle: (newTaskTitle) => set({ newTaskTitle }),
+ addTask: (task) =>
+ set((state) => ({
+ tasks: [...state.tasks, task],
+ newTaskTitle: '',
+ })),
+ reset: () => set(initialState),
+}));
+
+// Available for use outside React components
+export function getAllTasks(): Task[] {
+ return useTaskStore.getState().tasks;
+}
+
+export function addTask(task: Task) {
+ useTaskStore.getState().addTask(task);
+}
diff --git a/examples/cookbook/app/state-management/zustand/types.ts b/examples/cookbook/app/state-management/zustand/types.ts
new file mode 100644
index 000000000..1adad20ee
--- /dev/null
+++ b/examples/cookbook/app/state-management/zustand/types.ts
@@ -0,0 +1,4 @@
+export type Task = {
+ id: string;
+ title: string;
+};
diff --git a/examples/cookbook/package.json b/examples/cookbook/package.json
index f9396f38f..ea6d98064 100644
--- a/examples/cookbook/package.json
+++ b/examples/cookbook/package.json
@@ -27,7 +27,8 @@
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.25.2",
"react-native-web": "^0.21.0",
- "react-strict-dom": "^0.0.55"
+ "react-strict-dom": "^0.0.55",
+ "zustand": "^5.0.0"
},
"devDependencies": {
"@babel/core": "^7.29.0",
diff --git a/website/docs/12.x/cookbook/state-management/_meta.json b/website/docs/12.x/cookbook/state-management/_meta.json
index beac50b85..4047a6e13 100644
--- a/website/docs/12.x/cookbook/state-management/_meta.json
+++ b/website/docs/12.x/cookbook/state-management/_meta.json
@@ -1 +1,4 @@
-["jotai"]
+[
+ "jotai",
+ "zustand"
+]
diff --git a/website/docs/12.x/cookbook/state-management/zustand.md b/website/docs/12.x/cookbook/state-management/zustand.md
new file mode 100644
index 000000000..14b0da725
--- /dev/null
+++ b/website/docs/12.x/cookbook/state-management/zustand.md
@@ -0,0 +1,221 @@
+# Zustand
+
+## Introduction
+
+Zustand is a small, unopinionated state management library for React. It exposes a hook-based API
+with a vanilla store underneath, so you can read and update state both inside and outside of React
+components without requiring a provider.
+
+## Task List Example
+
+Let's assume we have a simple task list component that uses Zustand for state management. The
+component has a list of tasks, a text input for typing a new task name, and a button to add a new
+task to the list.
+
+```tsx title=state-management/zustand/TaskList.tsx
+import * as React from 'react';
+import { Pressable, Text, TextInput, View } from 'react-native';
+import { nanoid } from 'nanoid';
+import { useTaskStore } from './state';
+
+export function TaskList() {
+ const tasks = useTaskStore((state) => state.tasks);
+ const newTaskTitle = useTaskStore((state) => state.newTaskTitle);
+ const setNewTaskTitle = useTaskStore((state) => state.setNewTaskTitle);
+ const addTask = useTaskStore((state) => state.addTask);
+
+ const handleAddTask = () => {
+ addTask({
+ id: nanoid(),
+ title: newTaskTitle,
+ });
+ };
+
+ return (
+
+ {tasks.map((task) => (
+
+ {task.title}
+
+ ))}
+
+ {!tasks.length ? No tasks, start by adding one... : null}
+
+ setNewTaskTitle(text)}
+ />
+
+
+ Add Task
+
+
+ );
+}
+```
+
+## Starting with a Simple Test
+
+We can test our `TaskList` component using React Native Testing Library's (RNTL) regular `render`
+function. Although it is sufficient to test the empty state of the `TaskList` component, shared
+module-level Zustand stores keep state between tests, so we need a bit more setup for non-empty
+scenarios.
+
+```tsx title=state-management/zustand/__tests__/TaskList.test.tsx
+import * as React from 'react';
+import { render, screen, userEvent } from '@testing-library/react-native';
+import { renderWithStore } from './test-utils';
+import { TaskList } from '../TaskList';
+import { useTaskStore } from '../state';
+import { Task } from '../types';
+
+jest.useFakeTimers();
+
+afterEach(() => {
+ useTaskStore.getState().reset();
+});
+
+test('renders an empty task list', async () => {
+ await render();
+ expect(screen.getByText(/no tasks, start by adding one/i)).toBeOnTheScreen();
+});
+```
+
+## Custom Render Function to Populate the Zustand Store
+
+To test the `TaskList` component with initial tasks, create a custom render helper that resets the
+store and applies an optional `initialState` with `setState`. Resetting in the helper (and in
+`afterEach`) keeps tests isolated despite the module-level store.
+
+```tsx title=state-management/zustand/__tests__/test-utils.tsx
+import * as React from 'react';
+import { render } from '@testing-library/react-native';
+import { useTaskStore } from '../state';
+import { Task } from '../types';
+
+export type TaskStoreInitialState = {
+ tasks?: Task[];
+ newTaskTitle?: string;
+};
+
+export interface RenderWithStoreOptions {
+ initialState?: TaskStoreInitialState;
+}
+
+/**
+ * Renders a React component with a Zustand store hydrated for testing.
+ *
+ * Resets the store before each render so tests stay isolated, then optionally
+ * applies `initialState` via `setState`.
+ */
+export async function renderWithStore(
+ component: React.ReactElement,
+ options: RenderWithStoreOptions = {},
+) {
+ useTaskStore.getState().reset();
+
+ if (options.initialState) {
+ useTaskStore.setState(options.initialState);
+ }
+
+ return await render(component);
+}
+```
+
+## Testing the `TaskList` Component with Initial Tasks
+
+We can now use the `renderWithStore` function to render the `TaskList` component with initial tasks.
+
+```tsx title=state-management/zustand/__tests__/TaskList.test.tsx
+const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }];
+
+test('renders a to do list with 1 items initially, and adds a new item', async () => {
+ await renderWithStore(, {
+ initialState: {
+ tasks: INITIAL_TASKS,
+ newTaskTitle: '',
+ },
+ });
+
+ expect(screen.getByText(/buy bread/i)).toBeOnTheScreen();
+ expect(screen.getAllByTestId('task-item')).toHaveLength(1);
+
+ const user = userEvent.setup();
+ await user.type(screen.getByPlaceholderText(/new task/i), 'Buy almond milk');
+ await user.press(screen.getByRole('button', { name: /add task/i }));
+
+ expect(screen.getByText(/buy almond milk/i)).toBeOnTheScreen();
+ expect(screen.getAllByTestId('task-item')).toHaveLength(2);
+});
+```
+
+## Modifying the Store Outside of React Components
+
+Zustand exposes `getState` / `setState` on the store hook, so selectors and actions can run outside
+React without extra setup.
+
+```tsx title=state-management/zustand/state.ts
+import { create } from 'zustand';
+import { Task } from './types';
+
+type TaskState = {
+ tasks: Task[];
+ newTaskTitle: string;
+ setNewTaskTitle: (title: string) => void;
+ addTask: (task: Task) => void;
+ reset: () => void;
+};
+
+const initialState = {
+ tasks: [] as Task[],
+ newTaskTitle: '',
+};
+
+export const useTaskStore = create((set) => ({
+ ...initialState,
+ setNewTaskTitle: (newTaskTitle) => set({ newTaskTitle }),
+ addTask: (task) =>
+ set((state) => ({
+ tasks: [...state.tasks, task],
+ newTaskTitle: '',
+ })),
+ reset: () => set(initialState),
+}));
+
+// Selectors / actions available outside React components
+export function getAllTasks(): Task[] {
+ return useTaskStore.getState().tasks;
+}
+
+export function addTask(task: Task) {
+ useTaskStore.getState().addTask(task);
+}
+```
+
+## Testing the Store Outside of React Components
+
+You can test `getAllTasks` and `addTask` by setting initial state on the store and asserting on
+`getState` results.
+
+```tsx title=state-management/zustand/__tests__/TaskList.test.tsx
+import { addTask, getAllTasks, useTaskStore } from '../state';
+
+//...
+
+test('modify store outside of components', () => {
+ useTaskStore.setState({ tasks: INITIAL_TASKS });
+ expect(getAllTasks()).toEqual(INITIAL_TASKS);
+
+ const NEW_TASK = { id: '2', title: 'Buy almond milk' };
+ addTask(NEW_TASK);
+ expect(getAllTasks()).toEqual([...INITIAL_TASKS, NEW_TASK]);
+});
+```
+
+## Conclusion
+
+Testing components that use Zustand is straightforward with a small `renderWithStore` helper that
+resets the module store and hydrates initial state via `setState`. The same store API works outside
+React, so you can cover selectors and actions without mounting a component tree.
diff --git a/website/docs/13.x/cookbook/state-management/_meta.json b/website/docs/13.x/cookbook/state-management/_meta.json
index beac50b85..4047a6e13 100644
--- a/website/docs/13.x/cookbook/state-management/_meta.json
+++ b/website/docs/13.x/cookbook/state-management/_meta.json
@@ -1 +1,4 @@
-["jotai"]
+[
+ "jotai",
+ "zustand"
+]
diff --git a/website/docs/13.x/cookbook/state-management/zustand.md b/website/docs/13.x/cookbook/state-management/zustand.md
new file mode 100644
index 000000000..14b0da725
--- /dev/null
+++ b/website/docs/13.x/cookbook/state-management/zustand.md
@@ -0,0 +1,221 @@
+# Zustand
+
+## Introduction
+
+Zustand is a small, unopinionated state management library for React. It exposes a hook-based API
+with a vanilla store underneath, so you can read and update state both inside and outside of React
+components without requiring a provider.
+
+## Task List Example
+
+Let's assume we have a simple task list component that uses Zustand for state management. The
+component has a list of tasks, a text input for typing a new task name, and a button to add a new
+task to the list.
+
+```tsx title=state-management/zustand/TaskList.tsx
+import * as React from 'react';
+import { Pressable, Text, TextInput, View } from 'react-native';
+import { nanoid } from 'nanoid';
+import { useTaskStore } from './state';
+
+export function TaskList() {
+ const tasks = useTaskStore((state) => state.tasks);
+ const newTaskTitle = useTaskStore((state) => state.newTaskTitle);
+ const setNewTaskTitle = useTaskStore((state) => state.setNewTaskTitle);
+ const addTask = useTaskStore((state) => state.addTask);
+
+ const handleAddTask = () => {
+ addTask({
+ id: nanoid(),
+ title: newTaskTitle,
+ });
+ };
+
+ return (
+
+ {tasks.map((task) => (
+
+ {task.title}
+
+ ))}
+
+ {!tasks.length ? No tasks, start by adding one... : null}
+
+ setNewTaskTitle(text)}
+ />
+
+
+ Add Task
+
+
+ );
+}
+```
+
+## Starting with a Simple Test
+
+We can test our `TaskList` component using React Native Testing Library's (RNTL) regular `render`
+function. Although it is sufficient to test the empty state of the `TaskList` component, shared
+module-level Zustand stores keep state between tests, so we need a bit more setup for non-empty
+scenarios.
+
+```tsx title=state-management/zustand/__tests__/TaskList.test.tsx
+import * as React from 'react';
+import { render, screen, userEvent } from '@testing-library/react-native';
+import { renderWithStore } from './test-utils';
+import { TaskList } from '../TaskList';
+import { useTaskStore } from '../state';
+import { Task } from '../types';
+
+jest.useFakeTimers();
+
+afterEach(() => {
+ useTaskStore.getState().reset();
+});
+
+test('renders an empty task list', async () => {
+ await render();
+ expect(screen.getByText(/no tasks, start by adding one/i)).toBeOnTheScreen();
+});
+```
+
+## Custom Render Function to Populate the Zustand Store
+
+To test the `TaskList` component with initial tasks, create a custom render helper that resets the
+store and applies an optional `initialState` with `setState`. Resetting in the helper (and in
+`afterEach`) keeps tests isolated despite the module-level store.
+
+```tsx title=state-management/zustand/__tests__/test-utils.tsx
+import * as React from 'react';
+import { render } from '@testing-library/react-native';
+import { useTaskStore } from '../state';
+import { Task } from '../types';
+
+export type TaskStoreInitialState = {
+ tasks?: Task[];
+ newTaskTitle?: string;
+};
+
+export interface RenderWithStoreOptions {
+ initialState?: TaskStoreInitialState;
+}
+
+/**
+ * Renders a React component with a Zustand store hydrated for testing.
+ *
+ * Resets the store before each render so tests stay isolated, then optionally
+ * applies `initialState` via `setState`.
+ */
+export async function renderWithStore(
+ component: React.ReactElement,
+ options: RenderWithStoreOptions = {},
+) {
+ useTaskStore.getState().reset();
+
+ if (options.initialState) {
+ useTaskStore.setState(options.initialState);
+ }
+
+ return await render(component);
+}
+```
+
+## Testing the `TaskList` Component with Initial Tasks
+
+We can now use the `renderWithStore` function to render the `TaskList` component with initial tasks.
+
+```tsx title=state-management/zustand/__tests__/TaskList.test.tsx
+const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }];
+
+test('renders a to do list with 1 items initially, and adds a new item', async () => {
+ await renderWithStore(, {
+ initialState: {
+ tasks: INITIAL_TASKS,
+ newTaskTitle: '',
+ },
+ });
+
+ expect(screen.getByText(/buy bread/i)).toBeOnTheScreen();
+ expect(screen.getAllByTestId('task-item')).toHaveLength(1);
+
+ const user = userEvent.setup();
+ await user.type(screen.getByPlaceholderText(/new task/i), 'Buy almond milk');
+ await user.press(screen.getByRole('button', { name: /add task/i }));
+
+ expect(screen.getByText(/buy almond milk/i)).toBeOnTheScreen();
+ expect(screen.getAllByTestId('task-item')).toHaveLength(2);
+});
+```
+
+## Modifying the Store Outside of React Components
+
+Zustand exposes `getState` / `setState` on the store hook, so selectors and actions can run outside
+React without extra setup.
+
+```tsx title=state-management/zustand/state.ts
+import { create } from 'zustand';
+import { Task } from './types';
+
+type TaskState = {
+ tasks: Task[];
+ newTaskTitle: string;
+ setNewTaskTitle: (title: string) => void;
+ addTask: (task: Task) => void;
+ reset: () => void;
+};
+
+const initialState = {
+ tasks: [] as Task[],
+ newTaskTitle: '',
+};
+
+export const useTaskStore = create((set) => ({
+ ...initialState,
+ setNewTaskTitle: (newTaskTitle) => set({ newTaskTitle }),
+ addTask: (task) =>
+ set((state) => ({
+ tasks: [...state.tasks, task],
+ newTaskTitle: '',
+ })),
+ reset: () => set(initialState),
+}));
+
+// Selectors / actions available outside React components
+export function getAllTasks(): Task[] {
+ return useTaskStore.getState().tasks;
+}
+
+export function addTask(task: Task) {
+ useTaskStore.getState().addTask(task);
+}
+```
+
+## Testing the Store Outside of React Components
+
+You can test `getAllTasks` and `addTask` by setting initial state on the store and asserting on
+`getState` results.
+
+```tsx title=state-management/zustand/__tests__/TaskList.test.tsx
+import { addTask, getAllTasks, useTaskStore } from '../state';
+
+//...
+
+test('modify store outside of components', () => {
+ useTaskStore.setState({ tasks: INITIAL_TASKS });
+ expect(getAllTasks()).toEqual(INITIAL_TASKS);
+
+ const NEW_TASK = { id: '2', title: 'Buy almond milk' };
+ addTask(NEW_TASK);
+ expect(getAllTasks()).toEqual([...INITIAL_TASKS, NEW_TASK]);
+});
+```
+
+## Conclusion
+
+Testing components that use Zustand is straightforward with a small `renderWithStore` helper that
+resets the module store and hydrates initial state via `setState`. The same store API works outside
+React, so you can cover selectors and actions without mounting a component tree.
diff --git a/website/docs/14.x/cookbook/state-management/_meta.json b/website/docs/14.x/cookbook/state-management/_meta.json
index beac50b85..4047a6e13 100644
--- a/website/docs/14.x/cookbook/state-management/_meta.json
+++ b/website/docs/14.x/cookbook/state-management/_meta.json
@@ -1 +1,4 @@
-["jotai"]
+[
+ "jotai",
+ "zustand"
+]
diff --git a/website/docs/14.x/cookbook/state-management/zustand.md b/website/docs/14.x/cookbook/state-management/zustand.md
new file mode 100644
index 000000000..14b0da725
--- /dev/null
+++ b/website/docs/14.x/cookbook/state-management/zustand.md
@@ -0,0 +1,221 @@
+# Zustand
+
+## Introduction
+
+Zustand is a small, unopinionated state management library for React. It exposes a hook-based API
+with a vanilla store underneath, so you can read and update state both inside and outside of React
+components without requiring a provider.
+
+## Task List Example
+
+Let's assume we have a simple task list component that uses Zustand for state management. The
+component has a list of tasks, a text input for typing a new task name, and a button to add a new
+task to the list.
+
+```tsx title=state-management/zustand/TaskList.tsx
+import * as React from 'react';
+import { Pressable, Text, TextInput, View } from 'react-native';
+import { nanoid } from 'nanoid';
+import { useTaskStore } from './state';
+
+export function TaskList() {
+ const tasks = useTaskStore((state) => state.tasks);
+ const newTaskTitle = useTaskStore((state) => state.newTaskTitle);
+ const setNewTaskTitle = useTaskStore((state) => state.setNewTaskTitle);
+ const addTask = useTaskStore((state) => state.addTask);
+
+ const handleAddTask = () => {
+ addTask({
+ id: nanoid(),
+ title: newTaskTitle,
+ });
+ };
+
+ return (
+
+ {tasks.map((task) => (
+
+ {task.title}
+
+ ))}
+
+ {!tasks.length ? No tasks, start by adding one... : null}
+
+ setNewTaskTitle(text)}
+ />
+
+
+ Add Task
+
+
+ );
+}
+```
+
+## Starting with a Simple Test
+
+We can test our `TaskList` component using React Native Testing Library's (RNTL) regular `render`
+function. Although it is sufficient to test the empty state of the `TaskList` component, shared
+module-level Zustand stores keep state between tests, so we need a bit more setup for non-empty
+scenarios.
+
+```tsx title=state-management/zustand/__tests__/TaskList.test.tsx
+import * as React from 'react';
+import { render, screen, userEvent } from '@testing-library/react-native';
+import { renderWithStore } from './test-utils';
+import { TaskList } from '../TaskList';
+import { useTaskStore } from '../state';
+import { Task } from '../types';
+
+jest.useFakeTimers();
+
+afterEach(() => {
+ useTaskStore.getState().reset();
+});
+
+test('renders an empty task list', async () => {
+ await render();
+ expect(screen.getByText(/no tasks, start by adding one/i)).toBeOnTheScreen();
+});
+```
+
+## Custom Render Function to Populate the Zustand Store
+
+To test the `TaskList` component with initial tasks, create a custom render helper that resets the
+store and applies an optional `initialState` with `setState`. Resetting in the helper (and in
+`afterEach`) keeps tests isolated despite the module-level store.
+
+```tsx title=state-management/zustand/__tests__/test-utils.tsx
+import * as React from 'react';
+import { render } from '@testing-library/react-native';
+import { useTaskStore } from '../state';
+import { Task } from '../types';
+
+export type TaskStoreInitialState = {
+ tasks?: Task[];
+ newTaskTitle?: string;
+};
+
+export interface RenderWithStoreOptions {
+ initialState?: TaskStoreInitialState;
+}
+
+/**
+ * Renders a React component with a Zustand store hydrated for testing.
+ *
+ * Resets the store before each render so tests stay isolated, then optionally
+ * applies `initialState` via `setState`.
+ */
+export async function renderWithStore(
+ component: React.ReactElement,
+ options: RenderWithStoreOptions = {},
+) {
+ useTaskStore.getState().reset();
+
+ if (options.initialState) {
+ useTaskStore.setState(options.initialState);
+ }
+
+ return await render(component);
+}
+```
+
+## Testing the `TaskList` Component with Initial Tasks
+
+We can now use the `renderWithStore` function to render the `TaskList` component with initial tasks.
+
+```tsx title=state-management/zustand/__tests__/TaskList.test.tsx
+const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }];
+
+test('renders a to do list with 1 items initially, and adds a new item', async () => {
+ await renderWithStore(, {
+ initialState: {
+ tasks: INITIAL_TASKS,
+ newTaskTitle: '',
+ },
+ });
+
+ expect(screen.getByText(/buy bread/i)).toBeOnTheScreen();
+ expect(screen.getAllByTestId('task-item')).toHaveLength(1);
+
+ const user = userEvent.setup();
+ await user.type(screen.getByPlaceholderText(/new task/i), 'Buy almond milk');
+ await user.press(screen.getByRole('button', { name: /add task/i }));
+
+ expect(screen.getByText(/buy almond milk/i)).toBeOnTheScreen();
+ expect(screen.getAllByTestId('task-item')).toHaveLength(2);
+});
+```
+
+## Modifying the Store Outside of React Components
+
+Zustand exposes `getState` / `setState` on the store hook, so selectors and actions can run outside
+React without extra setup.
+
+```tsx title=state-management/zustand/state.ts
+import { create } from 'zustand';
+import { Task } from './types';
+
+type TaskState = {
+ tasks: Task[];
+ newTaskTitle: string;
+ setNewTaskTitle: (title: string) => void;
+ addTask: (task: Task) => void;
+ reset: () => void;
+};
+
+const initialState = {
+ tasks: [] as Task[],
+ newTaskTitle: '',
+};
+
+export const useTaskStore = create((set) => ({
+ ...initialState,
+ setNewTaskTitle: (newTaskTitle) => set({ newTaskTitle }),
+ addTask: (task) =>
+ set((state) => ({
+ tasks: [...state.tasks, task],
+ newTaskTitle: '',
+ })),
+ reset: () => set(initialState),
+}));
+
+// Selectors / actions available outside React components
+export function getAllTasks(): Task[] {
+ return useTaskStore.getState().tasks;
+}
+
+export function addTask(task: Task) {
+ useTaskStore.getState().addTask(task);
+}
+```
+
+## Testing the Store Outside of React Components
+
+You can test `getAllTasks` and `addTask` by setting initial state on the store and asserting on
+`getState` results.
+
+```tsx title=state-management/zustand/__tests__/TaskList.test.tsx
+import { addTask, getAllTasks, useTaskStore } from '../state';
+
+//...
+
+test('modify store outside of components', () => {
+ useTaskStore.setState({ tasks: INITIAL_TASKS });
+ expect(getAllTasks()).toEqual(INITIAL_TASKS);
+
+ const NEW_TASK = { id: '2', title: 'Buy almond milk' };
+ addTask(NEW_TASK);
+ expect(getAllTasks()).toEqual([...INITIAL_TASKS, NEW_TASK]);
+});
+```
+
+## Conclusion
+
+Testing components that use Zustand is straightforward with a small `renderWithStore` helper that
+resets the module store and hydrates initial state via `setState`. The same store API works outside
+React, so you can cover selectors and actions without mounting a component tree.
From c5e442e1b7ced029fc63f8c98ee22c6f57ad4e6f Mon Sep 17 00:00:00 2001
From: Daniel Chen <305872926+daniel-chen-j@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:09:16 +0100
Subject: [PATCH 2/2] chore: update cookbook yarn.lock for zustand
---
examples/cookbook/yarn.lock | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/examples/cookbook/yarn.lock b/examples/cookbook/yarn.lock
index 39c59ece5..b63aa654c 100644
--- a/examples/cookbook/yarn.lock
+++ b/examples/cookbook/yarn.lock
@@ -7880,6 +7880,7 @@ __metadata:
test-renderer: "npm:1.2.0"
typescript: "npm:~6.0.3"
typescript-eslint: "npm:^8.58.1"
+ zustand: "npm:^5.0.0"
languageName: unknown
linkType: soft
@@ -9008,3 +9009,24 @@ __metadata:
checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c
languageName: node
linkType: hard
+
+"zustand@npm:^5.0.0":
+ version: 5.0.14
+ resolution: "zustand@npm:5.0.14"
+ peerDependencies:
+ "@types/react": ">=18.0.0"
+ immer: ">=9.0.6"
+ react: ">=18.0.0"
+ use-sync-external-store: ">=1.2.0"
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+ checksum: 10c0/89de0c8119f1e0861b2f896a0b4df0d67994a373ecee02628d4e7b97cee1885207f646917d3ede395c3a5083316d48846855cfbc84aced7cd23a6db3238930a8
+ languageName: node
+ linkType: hard