Skip to content

Commit 31a1d17

Browse files
committed
chore: Zustand wip
1 parent 2f4568a commit 31a1d17

File tree

8 files changed

+165
-1
lines changed

8 files changed

+165
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from 'react';
2+
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
3+
import { generateId } from '../../utils';
4+
import { useTasksStore } from './state';
5+
6+
export default function TaskList() {
7+
const tasks = useTasksStore((state) => state.tasks);
8+
const addTask = useTasksStore((state) => state.addTask);
9+
const [newTaskTitle, setNewTaskTitle] = React.useState('');
10+
11+
const handleAddTask = () => {
12+
addTask({
13+
id: generateId(),
14+
title: newTaskTitle,
15+
});
16+
setNewTaskTitle('');
17+
};
18+
19+
return (
20+
<View style={styles.screen}>
21+
{tasks.map((task) => (
22+
<Text key={task.id} testID="task-item">
23+
{task.title}
24+
</Text>
25+
))}
26+
27+
{!tasks.length ? <Text>No tasks, start by adding one...</Text> : null}
28+
29+
<TextInput
30+
accessibilityLabel="New Task"
31+
placeholder="New Task..."
32+
value={newTaskTitle}
33+
onChangeText={(text) => setNewTaskTitle(text)}
34+
/>
35+
36+
<Pressable accessibilityRole="button" onPress={handleAddTask}>
37+
<Text>Add Task</Text>
38+
</Pressable>
39+
</View>
40+
);
41+
}
42+
43+
const styles = StyleSheet.create({
44+
screen: {
45+
flex: 1,
46+
justifyContent: 'center',
47+
alignItems: 'center',
48+
},
49+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as React from 'react';
2+
import { render, screen, userEvent } from '@testing-library/react-native';
3+
import TaskList from '../TaskList';
4+
import { addTask, getAllTasks, newTaskTitleAtom, store, tasksAtom } from '../state';
5+
6+
jest.useFakeTimers();
7+
8+
test('renders an empty task list', () => {
9+
render(<TaskList />);
10+
expect(screen.getByText(/no tasks, start by adding one/i)).toBeOnTheScreen();
11+
});
12+
13+
const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }];
14+
15+
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+
],
21+
});
22+
23+
expect(screen.getByText(/buy bread/i)).toBeOnTheScreen();
24+
expect(screen.getAllByTestId('task-item')).toHaveLength(1);
25+
26+
const user = userEvent.setup();
27+
await user.type(screen.getByPlaceholderText(/new task/i), 'Buy almond milk');
28+
await user.press(screen.getByRole('button', { name: /add task/i }));
29+
30+
expect(screen.getByText(/buy almond milk/i)).toBeOnTheScreen();
31+
expect(screen.getAllByTestId('task-item')).toHaveLength(2);
32+
});
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,12 @@
1+
import { create } from 'zustand';
2+
import { Task } from './types';
3+
4+
export interface TasksState {
5+
tasks: Task[];
6+
addTask: (task: Task) => void;
7+
}
8+
9+
export const useTasksStore = create<TasksState>((set) => ({
10+
tasks: [],
11+
addTask: (task: Task) => set((state) => ({ tasks: [...state.tasks, task] })),
12+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import { TasksState } from './state';
4+
5+
export interface RenderWithState {
6+
initialState: Partial<TasksState>;
7+
}
8+
9+
/**
10+
* Renders a React component with Jotai atoms for testing purposes.
11+
*
12+
* @param component - The React component to render.
13+
* @param options - The render options including the initial atom values.
14+
* @returns The render result from `@testing-library/react-native`.
15+
*/
16+
export const renderWithState = <T,>(component: React.ReactElement, options: RenderWithState) => {
17+
return render(
18+
<HydrateAtomsWrapper initialValues={options.initialValues}>{component}</HydrateAtomsWrapper>,
19+
);
20+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type Task = {
2+
id: string;
3+
title: string;
4+
};

examples/cookbook/app/utils.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import 'react-native-get-random-values';
2+
import { nanoid } from 'nanoid';
3+
4+
export function generateId() {
5+
return nanoid();
6+
}

examples/cookbook/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"react-native-get-random-values": "~1.8.0",
2626
"react-native-safe-area-context": "4.8.2",
2727
"react-native-screens": "~3.29.0",
28-
"react-native-web": "~0.19.6"
28+
"react-native-web": "~0.19.6",
29+
"zustand": "^4.5.4"
2930
},
3031
"devDependencies": {
3132
"@babel/core": "^7.20.0",

examples/cookbook/yarn.lock

+30
Original file line numberDiff line numberDiff line change
@@ -10332,6 +10332,7 @@ __metadata:
1033210332
react-native-web: "npm:~0.19.6"
1033310333
react-test-renderer: "npm:18.2.0"
1033410334
typescript: "npm:^5.3.0"
10335+
zustand: "npm:^4.5.4"
1033510336
languageName: unknown
1033610337
linkType: soft
1033710338

@@ -11680,6 +11681,15 @@ __metadata:
1168011681
languageName: node
1168111682
linkType: hard
1168211683

11684+
"use-sync-external-store@npm:1.2.0":
11685+
version: 1.2.0
11686+
resolution: "use-sync-external-store@npm:1.2.0"
11687+
peerDependencies:
11688+
react: ^16.8.0 || ^17.0.0 || ^18.0.0
11689+
checksum: 10c0/ac4814e5592524f242921157e791b022efe36e451fe0d4fd4d204322d5433a4fc300d63b0ade5185f8e0735ded044c70bcf6d2352db0f74d097a238cebd2da02
11690+
languageName: node
11691+
linkType: hard
11692+
1168311693
"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1":
1168411694
version: 1.0.2
1168511695
resolution: "util-deprecate@npm:1.0.2"
@@ -12182,3 +12192,23 @@ __metadata:
1218212192
checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f
1218312193
languageName: node
1218412194
linkType: hard
12195+
12196+
"zustand@npm:^4.5.4":
12197+
version: 4.5.4
12198+
resolution: "zustand@npm:4.5.4"
12199+
dependencies:
12200+
use-sync-external-store: "npm:1.2.0"
12201+
peerDependencies:
12202+
"@types/react": ">=16.8"
12203+
immer: ">=9.0.6"
12204+
react: ">=16.8"
12205+
peerDependenciesMeta:
12206+
"@types/react":
12207+
optional: true
12208+
immer:
12209+
optional: true
12210+
react:
12211+
optional: true
12212+
checksum: 10c0/479af491ffa1f1eb2c38b3ba25dc4e14339e8b35a60033d3f6c165b22f8be8163f7e1370015ded9c6e28548cd25af84a73fb40b5fad0bd7882d16ddd5ed613c6
12213+
languageName: node
12214+
linkType: hard

0 commit comments

Comments
 (0)