Skip to content

Commit d31c05a

Browse files
vanGalileastevegalilimdjastrzebski
authored
docs: add jotai to cookbook (#1634)
* create a Jotai examples dir in cookbook with related utils, add state management recipes dir * add docs with examples * extract state from component to state, utils, simplify types and custom render func. * refactor: tweaks & cleanup --------- Co-authored-by: stevegalili <steve.galili@mywheels.nl> Co-authored-by: Maciej Jastrzębski <mdjastrzebski@gmail.com>
1 parent 01d319c commit d31c05a

File tree

10 files changed

+2507
-1262
lines changed

10 files changed

+2507
-1262
lines changed
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
import { render, screen, userEvent } from '@testing-library/react-native';
3+
import { renderWithAtoms } from './test-utils';
4+
import { TaskList } from './TaskList';
5+
import { addTask, getAllTasks, newTaskTitleAtom, store, tasksAtom } from './state';
6+
import { Task } from './types';
7+
8+
jest.useFakeTimers();
9+
10+
test('renders an empty task list', () => {
11+
render(<TaskList />);
12+
expect(screen.getByText(/no tasks, start by adding one/i)).toBeOnTheScreen();
13+
});
14+
15+
const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }];
16+
17+
test('renders a to do list with 1 items initially, and adds a new item', async () => {
18+
renderWithAtoms(<TaskList />, {
19+
initialValues: [
20+
[tasksAtom, INITIAL_TASKS],
21+
[newTaskTitleAtom, ''],
22+
],
23+
});
24+
25+
expect(screen.getByText(/buy bread/i)).toBeOnTheScreen();
26+
expect(screen.getAllByTestId('task-item')).toHaveLength(1);
27+
28+
const user = userEvent.setup();
29+
await user.type(screen.getByPlaceholderText(/new task/i), 'Buy almond milk');
30+
await user.press(screen.getByRole('button', { name: /add task/i }));
31+
32+
expect(screen.getByText(/buy almond milk/i)).toBeOnTheScreen();
33+
expect(screen.getAllByTestId('task-item')).toHaveLength(2);
34+
});
35+
36+
test('modify store outside of components', () => {
37+
// Set the initial to do items in the store
38+
store.set(tasksAtom, INITIAL_TASKS);
39+
expect(getAllTasks()).toEqual(INITIAL_TASKS);
40+
41+
const NEW_TASK = { id: '2', title: 'Buy almond milk' };
42+
addTask(NEW_TASK);
43+
expect(getAllTasks()).toEqual([...INITIAL_TASKS, NEW_TASK]);
44+
});

examples/cookbook/jotai/TaskList.tsx

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
import { Pressable, Text, TextInput, View } from 'react-native';
3+
import { useAtom } from 'jotai';
4+
import { nanoid } from 'nanoid';
5+
import { newTaskTitleAtom, tasksAtom } from './state';
6+
7+
export function TaskList() {
8+
const [tasks, setTasks] = useAtom(tasksAtom);
9+
const [newTaskTitle, setNewTaskTitle] = useAtom(newTaskTitleAtom);
10+
11+
const handleAddTask = () => {
12+
setTasks((tasks) => [
13+
...tasks,
14+
{
15+
id: nanoid(),
16+
title: newTaskTitle,
17+
},
18+
]);
19+
setNewTaskTitle('');
20+
};
21+
22+
return (
23+
<View>
24+
{tasks.map((task) => (
25+
<Text key={task.id} testID="task-item">
26+
{task.title}
27+
</Text>
28+
))}
29+
30+
{!tasks.length ? <Text>No tasks, start by adding one...</Text> : null}
31+
32+
<TextInput
33+
accessibilityLabel="New Task"
34+
placeholder="New Task..."
35+
value={newTaskTitle}
36+
onChangeText={(text) => setNewTaskTitle(text)}
37+
/>
38+
39+
<Pressable accessibilityRole="button" onPress={handleAddTask}>
40+
<Text>Add Task</Text>
41+
</Pressable>
42+
</View>
43+
);
44+
}

examples/cookbook/jotai/state.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { atom, createStore } from 'jotai';
2+
import { Task } from './types';
3+
4+
export const tasksAtom = atom<Task[]>([]);
5+
export const newTaskTitleAtom = atom('');
6+
7+
// Available for use outside React components
8+
export const store = createStore();
9+
10+
// Selectors
11+
export function getAllTasks(): Task[] {
12+
return store.get(tasksAtom);
13+
}
14+
15+
// Actions
16+
export function addTask(task: Task) {
17+
store.set(tasksAtom, [...getAllTasks(), task]);
18+
}
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import { useHydrateAtoms } from 'jotai/utils';
4+
import { PrimitiveAtom } from 'jotai/vanilla/atom';
5+
6+
// Jotai types are not well exported, so we will make our life easier by using `any`.
7+
export type AtomInitialValueTuple<T> = [PrimitiveAtom<T>, T];
8+
9+
export interface RenderWithAtomsOptions {
10+
initialValues: AtomInitialValueTuple<any>[];
11+
}
12+
13+
/**
14+
* Renders a React component with Jotai atoms for testing purposes.
15+
*
16+
* @param component - The React component to render.
17+
* @param options - The render options including the initial atom values.
18+
* @returns The render result from `@testing-library/react-native`.
19+
*/
20+
export const renderWithAtoms = <T,>(
21+
component: React.ReactElement,
22+
options: RenderWithAtomsOptions,
23+
) => {
24+
return render(
25+
<HydrateAtomsWrapper initialValues={options.initialValues}>{component}</HydrateAtomsWrapper>,
26+
);
27+
};
28+
29+
export type HydrateAtomsWrapperProps = React.PropsWithChildren<{
30+
initialValues: AtomInitialValueTuple<unknown>[];
31+
}>;
32+
33+
/**
34+
* A wrapper component that hydrates Jotai atoms with initial values.
35+
*
36+
* @param initialValues - The initial values for the Jotai atoms.
37+
* @param children - The child components to render.
38+
* @returns The rendered children.
39+
40+
*/
41+
function HydrateAtomsWrapper({ initialValues, children }: HydrateAtomsWrapperProps) {
42+
useHydrateAtoms(initialValues);
43+
return children;
44+
}

examples/cookbook/jotai/types.ts

+4
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/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"dependencies": {
1414
"expo": "^50.0.4",
1515
"expo-status-bar": "~1.11.1",
16+
"jotai": "^2.8.4",
17+
"nanoid": "^3.3.7",
1618
"react": "18.2.0",
1719
"react-dom": "18.2.0",
1820
"react-native": "0.73.2",

0 commit comments

Comments
 (0)