Skip to content

Commit 19c4db8

Browse files
committed
feat: add more opps
1 parent 31a1d17 commit 19c4db8

File tree

7 files changed

+105
-39
lines changed

7 files changed

+105
-39
lines changed

examples/cookbook/app/state/zustand/TaskList.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { useTasksStore } from './state';
66
export default function TaskList() {
77
const tasks = useTasksStore((state) => state.tasks);
88
const addTask = useTasksStore((state) => state.addTask);
9+
910
const [newTaskTitle, setNewTaskTitle] = React.useState('');
1011

1112
const handleAddTask = () => {
1213
addTask({
1314
id: generateId(),
1415
title: newTaskTitle,
16+
completed: false,
1517
});
1618
setNewTaskTitle('');
1719
};
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import * as React from 'react';
2-
import { render, screen, userEvent } from '@testing-library/react-native';
2+
import { screen, userEvent } from '@testing-library/react-native';
33
import TaskList from '../TaskList';
4-
import { addTask, getAllTasks, newTaskTitleAtom, store, tasksAtom } from '../state';
4+
import { renderWithState } from '../test-utils';
5+
import { Task } from '../types';
56

67
jest.useFakeTimers();
78

89
test('renders an empty task list', () => {
9-
render(<TaskList />);
10+
renderWithState(<TaskList />);
1011
expect(screen.getByText(/no tasks, start by adding one/i)).toBeOnTheScreen();
1112
});
1213

13-
const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }];
14+
const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread', completed: false }];
1415

1516
test('renders a to do list with 1 items initially, and adds a new item', async () => {
16-
renderWithAtoms(<TaskList />, {
17-
initialValues: [
18-
[tasksAtom, INITIAL_TASKS],
19-
[newTaskTitleAtom, ''],
20-
],
17+
renderWithState(<TaskList />, {
18+
initialState: {
19+
tasks: INITIAL_TASKS,
20+
},
2121
});
2222

2323
expect(screen.getByText(/buy bread/i)).toBeOnTheScreen();
@@ -30,13 +30,3 @@ test('renders a to do list with 1 items initially, and adds a new item', async (
3030
expect(screen.getByText(/buy almond milk/i)).toBeOnTheScreen();
3131
expect(screen.getAllByTestId('task-item')).toHaveLength(2);
3232
});
33-
34-
test('modify store outside of components', () => {
35-
// Set the initial to do items in the store
36-
store.set(tasksAtom, INITIAL_TASKS);
37-
expect(getAllTasks()).toEqual(INITIAL_TASKS);
38-
39-
const NEW_TASK = { id: '2', title: 'Buy almond milk' };
40-
addTask(NEW_TASK);
41-
expect(getAllTasks()).toEqual([...INITIAL_TASKS, NEW_TASK]);
42-
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as React from 'react';
2+
import { createStore } from 'zustand';
3+
import { tasksStoreCreator, TasksStoreProvider } from './state';
4+
import TaskList from './TaskList';
5+
6+
const store = createStore(tasksStoreCreator);
7+
8+
export default function App() {
9+
return (
10+
<TasksStoreProvider store={store}>
11+
<TaskList />
12+
</TasksStoreProvider>
13+
);
14+
}

examples/cookbook/app/state/zustand/state.ts

-12
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as React from 'react';
2+
import { createContext, useContext, useRef } from 'react';
3+
import { createStore, StateCreator, StoreApi, useStore } from 'zustand';
4+
import { Task } from './types';
5+
6+
export interface TasksState {
7+
tasks: Task[];
8+
addTask: (task: Task) => void;
9+
deleteTask: (id: string) => void;
10+
toggleTask: (id: string) => void;
11+
clearCompleted: () => void;
12+
}
13+
14+
export const tasksStoreCreator: StateCreator<TasksState> = (set) => ({
15+
tasks: [],
16+
addTask: (task: Task) => {
17+
set((state) => ({ tasks: [...state.tasks, task] }));
18+
},
19+
deleteTask: (id: string) => {
20+
set((state) => ({ tasks: state.tasks.filter((todo) => todo.id !== id) }));
21+
},
22+
toggleTask: (id: string) => {
23+
set((state) => ({
24+
tasks: state.tasks.map((task) =>
25+
task.id === id ? { ...task, completed: !task.completed } : task,
26+
),
27+
}));
28+
},
29+
clearCompleted: () => {
30+
set((state) => ({ tasks: state.tasks.filter((task) => !task.completed) }));
31+
},
32+
});
33+
34+
// Use store with context
35+
// See: https://docs.pmnd.rs/zustand/guides/testing#testing-components
36+
37+
export type TasksStoreApi = StoreApi<TasksState>;
38+
39+
const TasksStoreContext = createContext<TasksStoreApi | undefined>(undefined);
40+
41+
export interface TasksStoreProviderProps extends React.PropsWithChildren {
42+
// Optionally pass a pre-created store (for testing)
43+
store?: TasksStoreApi;
44+
}
45+
46+
export function TasksStoreProvider({ store, children }: TasksStoreProviderProps) {
47+
const storeRef = useRef<TasksStoreApi>();
48+
if (!storeRef.current) {
49+
// Inject passed store or create a new one
50+
storeRef.current = store ?? createStore(tasksStoreCreator);
51+
}
52+
53+
return (
54+
<TasksStoreContext.Provider value={storeRef.current}>{children}</TasksStoreContext.Provider>
55+
);
56+
}
57+
58+
export type UseTasksStoreContextSelector<T> = (store: TasksState) => T;
59+
60+
export function useTasksStore<T>(selector: UseTasksStoreContextSelector<T>) {
61+
const store = useContext(TasksStoreContext);
62+
if (!store) {
63+
throw new Error('useTasksStore must be used within TasksStoreProvider');
64+
}
65+
66+
return useStore(store, selector);
67+
}
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import * as React from 'react';
22
import { render } from '@testing-library/react-native';
3-
import { TasksState } from './state';
3+
import { createStore } from 'zustand';
4+
import { TasksState, tasksStoreCreator, TasksStoreProvider } from './state';
45

56
export interface RenderWithState {
6-
initialState: Partial<TasksState>;
7+
initialState?: Partial<TasksState>;
78
}
89

910
/**
10-
* Renders a React component with Jotai atoms for testing purposes.
11+
* Renders a React component with Zustand state for testing purposes.
1112
*
1213
* @param component - The React component to render.
13-
* @param options - The render options including the initial atom values.
14+
* @param options - The render options including the initial state.
1415
* @returns The render result from `@testing-library/react-native`.
1516
*/
16-
export const renderWithState = <T,>(component: React.ReactElement, options: RenderWithState) => {
17-
return render(
18-
<HydrateAtomsWrapper initialValues={options.initialValues}>{component}</HydrateAtomsWrapper>,
19-
);
17+
export const renderWithState = (component: React.ReactElement, options?: RenderWithState) => {
18+
const store = createStore(tasksStoreCreator);
19+
if (options?.initialState) {
20+
store.setState(options.initialState);
21+
}
22+
23+
return render(<TasksStoreProvider store={store}>{component}</TasksStoreProvider>);
2024
};
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type Task = {
22
id: string;
33
title: string;
4+
completed: boolean;
45
};

0 commit comments

Comments
 (0)