Skip to content

Commit a25c27d

Browse files
author
stevegalili
committed
create a Jotai examples dir in cookbook with related utils, add state management recipes dir
1 parent 01d319c commit a25c27d

File tree

9 files changed

+5928
-10697
lines changed

9 files changed

+5928
-10697
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react';
2+
import { render, screen, userEvent } from '@testing-library/react-native';
3+
import { TodoItem, TodoList, todosAtom } from './TodoList';
4+
import { renderWithAtoms } from './test-utils';
5+
6+
jest.useFakeTimers();
7+
test('renders an empty to do list', () => {
8+
render(<TodoList />);
9+
expect(screen.getByText(/no todos, start by adding one/i)).toBeOnTheScreen();
10+
});
11+
12+
const INITIAL_TODOS: TodoItem[] = [{ id: '1', text: 'Buy bread' }];
13+
14+
test('renders a to do list with 1 items initially, and adds a new item', async () => {
15+
renderWithAtoms<TodoItem[]>(<TodoList />, {
16+
initialValues: [
17+
[todosAtom, INITIAL_TODOS],
18+
// optional: add any other Jotai atoms and their corresponding initial values
19+
],
20+
});
21+
expect(screen.getByText(/buy bread/i)).toBeOnTheScreen();
22+
expect(screen.getAllByLabelText('todo-item')).toHaveLength(1);
23+
24+
const user = userEvent.setup();
25+
const addTodoButton = screen.getByRole('button', { name: /add a random to-do/i });
26+
await user.press(addTodoButton);
27+
28+
expect(screen.getByText(/buy almond milk/i)).toBeOnTheScreen();
29+
expect(screen.getAllByLabelText('todo-item')).toHaveLength(2);
30+
});

examples/cookbook/jotai/TodoList.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as React from 'react';
2+
import { FlatList, Pressable, Text, View } from 'react-native';
3+
import { atom, useAtom } from 'jotai';
4+
5+
export type TodoItem = {
6+
id: string;
7+
text: string;
8+
};
9+
10+
export const todosAtom = atom<TodoItem[]>([]);
11+
12+
export function TodoList() {
13+
const [todos, setTodos] = useAtom(todosAtom);
14+
15+
const handleAddTodo = () =>
16+
setTodos((prev) => [
17+
...prev,
18+
{
19+
id: Math.random().toString(36).slice(2, 11),
20+
text: 'Buy almond milk',
21+
},
22+
]);
23+
24+
if (!todos.length) {
25+
return <Text>No todos, start by adding one...</Text>;
26+
}
27+
28+
return (
29+
<View>
30+
<FlatList
31+
data={todos}
32+
renderItem={({ item }: { item: TodoItem }) => (
33+
<Text key={item.id} accessibilityLabel={'todo-item'}>
34+
{item.text}
35+
</Text>
36+
)}
37+
/>
38+
<Pressable accessibilityRole="button" onPress={handleAddTodo}>
39+
<Text>Add a random to-do</Text>
40+
</Pressable>
41+
</View>
42+
);
43+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as React from 'react';
2+
import {Suspense} from 'react';
3+
import {render} from '@testing-library/react-native';
4+
import {ActivityIndicator} from "react-native";
5+
import {useHydrateAtoms} from "jotai/utils";
6+
import {IHydrateAtomsProps, InitialValue, InitialValues, IRenderWithAtomsOptions} from "./types";
7+
import {Provider} from "jotai";
8+
9+
const HydrateAtoms = <T, >({initialValues, children}: IHydrateAtomsProps<T>) => {
10+
useHydrateAtoms(initialValues as unknown as InitialValues);
11+
return children;
12+
};
13+
14+
export default function JotaiTestProvider<T>({
15+
initialValues,
16+
children,
17+
}: IHydrateAtomsProps<T>) {
18+
return (
19+
<Provider>
20+
<Suspense fallback={<ActivityIndicator/>}>
21+
<HydrateAtoms initialValues={initialValues}>{children}</HydrateAtoms>
22+
</Suspense>
23+
</Provider>
24+
);
25+
}
26+
27+
const getWrapper =
28+
<T, >(initialValues: Array<InitialValue<T>>) =>
29+
({children}: { children: React.JSX.Element }) => (
30+
<JotaiTestProvider initialValues={initialValues}>
31+
{children}
32+
</JotaiTestProvider>
33+
);
34+
35+
/**
36+
* Renders a React component with Jotai atoms for testing purposes.
37+
*
38+
* @template T - The type of the initial values for the atoms.
39+
* @param component - The React component to render.
40+
* @param options - The render options including the initial atom values.
41+
* @returns The render result from `@testing-library/react-native`.
42+
*/
43+
export const renderWithAtoms = <T, >(
44+
component: React.ReactElement,
45+
options: IRenderWithAtomsOptions<T>,
46+
) => {
47+
const {initialValues} = options;
48+
return render(component, {
49+
wrapper: getWrapper(initialValues),
50+
...options,
51+
});
52+
};

examples/cookbook/jotai/types.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from 'react';
2+
3+
import { PrimitiveAtom } from 'jotai/vanilla/atom';
4+
import { useHydrateAtoms } from 'jotai/utils';
5+
import { RenderOptions as RntlRenderOptions } from '@testing-library/react-native';
6+
7+
type WithInitialValue<Value> = {
8+
init: Value;
9+
};
10+
type UseHydrateAtomsParams = Parameters<typeof useHydrateAtoms>;
11+
export type InitialValues = UseHydrateAtomsParams[0];
12+
export type InitialValue<Value> = [PrimitiveAtom<Value> & WithInitialValue<Value>, Value];
13+
14+
export interface IRenderWithAtomsOptions<T> extends RntlRenderOptions {
15+
initialValues: Array<InitialValue<T>>;
16+
}
17+
18+
export interface IHydrateAtomsProps<T> {
19+
initialValues: Array<InitialValue<T>>;
20+
children: React.JSX.Element;
21+
}

examples/cookbook/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"dependencies": {
1414
"expo": "^50.0.4",
1515
"expo-status-bar": "~1.11.1",
16+
"jotai": "^2.8.4",
1617
"react": "18.2.0",
1718
"react-dom": "18.2.0",
1819
"react-native": "0.73.2",

0 commit comments

Comments
 (0)