Skip to main content
Module

x/jotai/tests/utils/splitAtom.test.tsx

👻 Primitive and flexible state management for React
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
import { useEffect, useRef } from 'react'import { fireEvent, render, waitFor } from '@testing-library/react'import { atom, useAtom, useSetAtom } from 'jotai'import type { Atom, PrimitiveAtom } from 'jotai'import { splitAtom } from 'jotai/utils'import { getTestProvider } from '../testUtils'
const Provider = getTestProvider()
type TodoItem = { task: string; checked?: boolean }
const useCommitCount = () => { const commitCountRef = useRef(1) useEffect(() => { commitCountRef.current += 1 }) return commitCountRef.current}
it('no unnecessary updates when updating atoms', async () => { const todosAtom = atom<TodoItem[]>([ { task: 'get cat food', checked: false }, { task: 'get dragon food', checked: false }, ])
const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms, dispatch] = useAtom(splitAtom(listAtom)) return ( <> TaskListUpdates: {useCommitCount()} {atoms.map((anAtom) => ( <TaskItem key={`${anAtom}`} onRemove={() => dispatch({ type: 'remove', atom: anAtom })} itemAtom={anAtom} /> ))} </> ) }
const TaskItem = ({ itemAtom, }: { itemAtom: PrimitiveAtom<TodoItem> onRemove: () => void }) => { const [value, onChange] = useAtom(itemAtom) const toggle = () => onChange((value) => ({ ...value, checked: !value.checked })) return ( <li> {value.task} commits: {useCommitCount()} <input data-testid={`${value.task}-checkbox`} type="checkbox" checked={value.checked || false} onChange={toggle} /> </li> ) }
const { getByTestId, getByText } = render( <Provider> <TaskList listAtom={todosAtom} /> </Provider> )
await waitFor(() => { getByText('TaskListUpdates: 1') getByText('get cat food commits: 1') getByText('get dragon food commits: 1') })
const catBox = getByTestId('get cat food-checkbox') as HTMLInputElement const dragonBox = getByTestId('get dragon food-checkbox') as HTMLInputElement
expect(catBox.checked).toBe(false) expect(dragonBox.checked).toBe(false)
fireEvent.click(catBox)
await waitFor(() => { getByText('TaskListUpdates: 1') getByText('get cat food commits: 2') getByText('get dragon food commits: 1') })
expect(catBox.checked).toBe(true) expect(dragonBox.checked).toBe(false)
fireEvent.click(dragonBox)
await waitFor(() => { getByText('TaskListUpdates: 1') getByText('get cat food commits: 2') getByText('get dragon food commits: 2') })
expect(catBox.checked).toBe(true) expect(dragonBox.checked).toBe(true)})
it('removing atoms', async () => { const todosAtom = atom<TodoItem[]>([ { task: 'get cat food', checked: false }, { task: 'get dragon food', checked: false }, { task: 'help nana', checked: false }, ])
const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms, dispatch] = useAtom(splitAtom(listAtom)) return ( <> {atoms.map((anAtom) => ( <TaskItem key={`${anAtom}`} onRemove={() => dispatch({ type: 'remove', atom: anAtom })} itemAtom={anAtom} /> ))} </> ) }
const TaskItem = ({ itemAtom, onRemove, }: { itemAtom: PrimitiveAtom<TodoItem> onRemove: () => void }) => { const [value] = useAtom(itemAtom) return ( <li> <div>{value.task}</div> <button data-testid={`${value.task}-removebutton`} onClick={onRemove}> X </button> </li> ) }
const { getByTestId, queryByText } = render( <Provider> <TaskList listAtom={todosAtom} /> </Provider> )
await waitFor(() => { expect(queryByText('get cat food')).toBeTruthy() expect(queryByText('get dragon food')).toBeTruthy() expect(queryByText('help nana')).toBeTruthy() })
fireEvent.click(getByTestId('get cat food-removebutton'))
await waitFor(() => { expect(queryByText('get cat food')).toBeFalsy() expect(queryByText('get dragon food')).toBeTruthy() expect(queryByText('help nana')).toBeTruthy() })
fireEvent.click(getByTestId('get dragon food-removebutton'))
await waitFor(() => { expect(queryByText('get cat food')).toBeFalsy() expect(queryByText('get dragon food')).toBeFalsy() expect(queryByText('help nana')).toBeTruthy() })
fireEvent.click(getByTestId('help nana-removebutton'))
await waitFor(() => { expect(queryByText('get cat food')).toBeFalsy() expect(queryByText('get dragon food')).toBeFalsy() expect(queryByText('help nana')).toBeFalsy() })})
it('inserting atoms', async () => { const todosAtom = atom<TodoItem[]>([ { task: 'get cat food' }, { task: 'get dragon food' }, { task: 'help nana' }, ])
const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms, dispatch] = useAtom(splitAtom(listAtom)) return ( <> <ul data-testid="list"> {atoms.map((anAtom) => ( <TaskItem key={`${anAtom}`} onInsert={(newValue) => dispatch({ type: 'insert', value: newValue, before: anAtom, }) } itemAtom={anAtom} /> ))} </ul> <button data-testid="addtaskbutton" onClick={() => dispatch({ type: 'insert', value: { task: 'end' }, }) }> add task </button> </> ) }
let taskCount = 1 const TaskItem = ({ itemAtom, onInsert, }: { itemAtom: PrimitiveAtom<TodoItem> onInsert: (newValue: TodoItem) => void }) => { const [value] = useAtom(itemAtom) return ( <li> <div>{value.task}</div> <button data-testid={`${value.task}-insertbutton`} onClick={() => onInsert({ task: 'new task' + taskCount++ })}> + </button> </li> ) }
const { getByTestId, queryByTestId } = render( <Provider> <TaskList listAtom={todosAtom} /> </Provider> )
await waitFor(() => { expect(queryByTestId('list')?.textContent).toBe( 'get cat food+get dragon food+help nana+' ) })
fireEvent.click(getByTestId('help nana-insertbutton')) await waitFor(() => { expect(queryByTestId('list')?.textContent).toBe( 'get cat food+get dragon food+new task1+help nana+' ) })
fireEvent.click(getByTestId('get cat food-insertbutton')) await waitFor(() => { expect(queryByTestId('list')?.textContent).toBe( 'new task2+get cat food+get dragon food+new task1+help nana+' ) })
fireEvent.click(getByTestId('addtaskbutton')) await waitFor(() => { expect(queryByTestId('list')?.textContent).toBe( 'new task2+get cat food+get dragon food+new task1+help nana+end+' ) })})
it('moving atoms', async () => { const todosAtom = atom<TodoItem[]>([ { task: 'get cat food' }, { task: 'get dragon food' }, { task: 'help nana' }, ])
const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms, dispatch] = useAtom(splitAtom(listAtom)) return ( <ul data-testid="list"> {atoms.map((anAtom, index) => ( <TaskItem key={`${anAtom}`} onMoveLeft={() => { if (index > 0) { dispatch({ type: 'move', atom: anAtom, before: atoms[index - 1] as PrimitiveAtom<TodoItem>, }) } }} onMoveRight={() => { if (index === atoms.length - 1) { dispatch({ type: 'move', atom: anAtom, }) } else if (index < atoms.length - 1) { dispatch({ type: 'move', atom: anAtom, before: atoms[index + 2] as PrimitiveAtom<TodoItem>, }) } }} itemAtom={anAtom} /> ))} </ul> ) }
const TaskItem = ({ itemAtom, onMoveLeft, onMoveRight, }: { itemAtom: PrimitiveAtom<TodoItem> onMoveLeft: () => void onMoveRight: () => void }) => { const [value] = useAtom(itemAtom) return ( <li> <div>{value.task}</div> <button data-testid={`${value.task}-leftbutton`} onClick={onMoveLeft}> &lt; </button> <button data-testid={`${value.task}-rightbutton`} onClick={onMoveRight}> &gt; </button> </li> ) }
const { getByTestId, queryByTestId } = render( <Provider> <TaskList listAtom={todosAtom} /> </Provider> )
await waitFor(() => { expect(queryByTestId('list')?.textContent).toBe( 'get cat food<>get dragon food<>help nana<>' ) })
fireEvent.click(getByTestId('help nana-leftbutton')) await waitFor(() => { expect(queryByTestId('list')?.textContent).toBe( 'get cat food<>help nana<>get dragon food<>' ) })
fireEvent.click(getByTestId('get cat food-rightbutton')) await waitFor(() => { expect(queryByTestId('list')?.textContent).toBe( 'help nana<>get cat food<>get dragon food<>' ) })
fireEvent.click(getByTestId('get cat food-rightbutton')) await waitFor(() => { expect(queryByTestId('list')?.textContent).toBe( 'help nana<>get dragon food<>get cat food<>' ) })})
it('read-only array atom', async () => { const todosAtom = atom<TodoItem[]>(() => [ { task: 'get cat food', checked: false }, { task: 'get dragon food', checked: false }, ])
const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms] = useAtom(splitAtom(listAtom)) return ( <> {atoms.map((anAtom) => ( <TaskItem key={`${anAtom}`} itemAtom={anAtom} /> ))} </> ) }
const TaskItem = ({ itemAtom }: { itemAtom: Atom<TodoItem> }) => { const [value] = useAtom(itemAtom) return ( <li> <input data-testid={`${value.task}-checkbox`} type="checkbox" checked={value.checked || false} readOnly /> </li> ) }
const { getByTestId } = render( <Provider> <TaskList listAtom={todosAtom} /> </Provider> )
const catBox = getByTestId('get cat food-checkbox') as HTMLInputElement const dragonBox = getByTestId('get dragon food-checkbox') as HTMLInputElement
// FIXME is there a better way? await waitFor(() => {})
expect(catBox.checked).toBe(false) expect(dragonBox.checked).toBe(false)})
it('handles scope', async () => { const scope = Symbol() const todosAtom = atom<TodoItem[]>([ { task: 'get cat food', checked: false }, { task: 'get dragon food', checked: false }, ])
const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms] = useAtom(splitAtom(listAtom), scope) return ( <> {atoms.map((anAtom) => ( <TaskItem key={`${anAtom}`} itemAtom={anAtom} /> ))} </> ) }
const TaskItem = ({ itemAtom }: { itemAtom: PrimitiveAtom<TodoItem> }) => { const [value, onChange] = useAtom(itemAtom, scope) const toggle = () => onChange((value) => ({ ...value, checked: !value.checked })) return ( <li> <input data-testid={`${value.task}-checkbox`} type="checkbox" checked={value.checked ?? false} onChange={toggle} /> </li> ) }
const { getByTestId } = render( <Provider scope={scope}> <TaskList listAtom={todosAtom} /> </Provider> )
const catBox = getByTestId('get cat food-checkbox') as HTMLInputElement const dragonBox = getByTestId('get dragon food-checkbox') as HTMLInputElement
expect(catBox.checked).toBe(false) expect(dragonBox.checked).toBe(false)
fireEvent.click(catBox)
// FIXME is there a better way? await waitFor(() => {})
expect(catBox.checked).toBe(true) expect(dragonBox.checked).toBe(false)})
it('no error with cached atoms (fix 510)', async () => { const filterAtom = atom('all') const numsAtom = atom<number[]>([0, 1, 2, 3, 4]) const filteredAtom = atom<number[]>((get) => { const filter = get(filterAtom) const nums = get(numsAtom) if (filter === 'even') { return nums.filter((num) => num % 2 === 0) } return nums }) const filteredAtomsAtom = splitAtom(filteredAtom, (num) => num)
function useCachedAtoms<T>(atoms: T[]) { const prevAtoms = useRef<T[]>(atoms) return prevAtoms.current }
type NumItemProps = { atom: Atom<number> }
const NumItem = ({ atom }: NumItemProps) => { const [readOnlyItem] = useAtom(atom) if (typeof readOnlyItem !== 'number') { throw new Error('expecting a number') } return <>{readOnlyItem}</> }
function Filter() { const [, setFilter] = useAtom(filterAtom) return <button onClick={() => setFilter('even')}>button</button> }
const Filtered = () => { const [todos] = useAtom(filteredAtomsAtom) const cachedAtoms = useCachedAtoms(todos)
return ( <> {cachedAtoms.map((atom) => ( <NumItem key={`${atom}`} atom={atom} /> ))} </> ) }
const { getByText } = render( <Provider> <Filter /> <Filtered /> </Provider> )
fireEvent.click(getByText('button'))})
it('variable sized splitted atom', async () => { const lengthAtom = atom(3) const collectionAtom = atom<number[]>([]) const collectionAtomsAtom = splitAtom(collectionAtom) const derivativeAtom = atom((get) => get(collectionAtomsAtom).map((ca) => get(ca)) )
function App() { const [length, setLength] = useAtom(lengthAtom) const setCollection = useSetAtom(collectionAtom) const [derivative] = useAtom(derivativeAtom) useEffect(() => { setCollection([1, 2, 3].splice(0, length)) }, [length, setCollection]) return ( <div> <button onClick={() => setLength(2)}>button</button> numbers: {derivative.join(',')} </div> ) }
const { findByText, getByText } = render( <Provider> <App /> </Provider> )
await findByText('numbers: 1,2,3')
fireEvent.click(getByText('button')) await findByText('numbers: 1,2')})