これは、なにをしたくて書いたもの?
こちらの「Learn React」をやってみるシリーズの続きです。
Learn ReactをVitest、React Testing Library、jest-domを使ってやってみる(Describing the UI) - CLOVER🍀
Learn ReactをVitest、React Testing Library、jest-domを使ってやってみる(Keeping Components Pure) - CLOVER🍀
Learn ReactをVitest、React Testing Libraryを使ってやってみる(Understanding Your UI as a Tree) - CLOVER🍀
Learn ReactをVitest、React Testing Libraryを使ってやってみる(Responding to Events) - CLOVER🍀
Learn ReactをVitest、React Testing Libraryを使ってやってみる(State: A Component's Memory) - CLOVER🍀
Learn ReactをVitest、React Testing Libraryを使ってやってみる(Queueing a Series of State Updates) - CLOVER🍀
Learn ReactをVitest、React Testing Libraryを使ってやってみる(Updating Objects in State) - CLOVER🍀
今回は、「Learn React」の「Adding Interactivity」から「Updating Arrays in State」をやってみます。
Updating Arrays in State – React
「Adding Interactivity」の最後ですね。
Learn Reactをやってみる
Learn Reactをただ写経するのではなく、以下の条件で進めていきます。再掲です。
- 環境はViteで構築する
- TypeScriptで書く
- Vitest、React Testing Libraryを使ったテストを書く
今回の対象はこちらのページです。
Updating Arrays in State – React
環境
今回の環境はこちら。
$ node --version v22.15.1 $ npm --version 10.9.2
準備
Viteを使って、Node.jsプロジェクトを作成します。
$ npm create vite@latest updating-arrays-in-state -- --template react-ts $ cd updating-arrays-in-state
依存ライブラリー、テスト用のライブラリーをインストール。
$ npm i $ npm i immer use-immer $ npm i -D vitest jsdom @testing-library/react @testing-library/dom @testing-library/user-event @testing-library/jest-dom @types/react @types/react-dom $ npm i -D prettier
今回はuse-immerを使用します。
インストールしたライブラリーの一覧とバージョンはこちら。
$ npm ls updating-arrays-in-state@0.0.0 /path/to/updating-arrays-in-state ├── @eslint/js@9.27.0 ├── @testing-library/dom@10.4.0 ├── @testing-library/jest-dom@6.6.3 ├── @testing-library/react@16.3.0 ├── @testing-library/user-event@14.6.1 ├── @types/react-dom@19.1.5 ├── @types/react@19.1.4 ├── @vitejs/plugin-react@4.4.1 ├── eslint-plugin-react-hooks@5.2.0 ├── eslint-plugin-react-refresh@0.4.20 ├── eslint@9.27.0 ├── globals@16.1.0 ├── immer@10.1.1 ├── jsdom@26.1.0 ├── prettier@3.5.3 ├── react-dom@19.1.0 ├── react@19.1.0 ├── typescript-eslint@8.32.1 ├── typescript@5.8.3 ├── use-immer@0.11.0 ├── vite@6.3.5 └── vitest@3.1.3
自動生成された不要なファイルは削除しておきます。
$ find src -type f | grep -vE 'main.tsx|vite-env.d.ts' | xargs rm -rf $ rmdir src/assets
Viteで作成したプロジェクトから、追加・変更した設定ファイルを載せておきます。
tsconfig.json
{ "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }, { "path": "./tsconfig.test.json" } ] }
tsconfig.test.json
{ "extends": "./tsconfig.app.json", "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.test.tsbuildinfo", "types": ["@testing-library/jest-dom"] }, "include": [ "src", "test", "vitest.config.ts", "vitest.setup.ts" ] }
vitest.config.ts
import { defineConfig } from 'vite'; export default defineConfig({ test: { environment: 'jsdom', setupFiles: ['./vitest.setup.ts'], }, });
vitest.setup.ts
import '@testing-library/jest-dom/vitest';
.prettierrc.json
{ "singleQuote": true, "printWidth": 100 }
package.json
のscripts
の内容。
"scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", "typecheck": "tsc -b", "typecheck:watch": "tsc -b --w", "format": "prettier --write src/**/*.{ts,tsx,css} test/**/*.{ts,tsx}", "test": "vitest run", "test:update": "vitest run -u", "test:watch": "vitest watch" },
Learn ReactのAdding InteractivityのUpdating Arrays in Stateをやってみる
テストコードではTesting Libraryのuser-eventを使いますが、セットアップ部分は共通化しておきます。
`test/setup.ts{
import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { ReactNode } from 'react'; export function setup(jsx: ReactNode) { return { user: userEvent.setup(), ...render(jsx), }; }
Adding to an array
Updating Arrays in State / Adding to an array
配列の中身を直接変更しても、うまくいかないという悪い例から。
src/BadList.tsx
import { useState } from 'react'; type Artist = { id: number; name: string; }; let nextId = 0; export function BadList() { const [name, setName] = useState(''); //const [artists, setArtists] = useState<Artist[]>([]); const [artists] = useState<Artist[]>([]); return ( <> <h1>Inspiring sculptors:</h1> <input value={name} onChange={(e) => setName(e.target.value)} /> <button onClick={() => { artists.push({ id: nextId++, name: name, }); }} > Add </button> <ul> {artists.map((artist) => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
test/BadList.test.tsx
import { expect, test } from 'vitest'; import { setup } from './setup'; import { BadList } from '../src/BadList'; import { screen, within } from '@testing-library/react'; test('Updating Arrays in State, BadList test', async () => { const { user, container } = setup(<BadList />); expect(within(screen.getByRole('list')).queryAllByRole('listitem')).toHaveLength(0); expect(container).toMatchSnapshot(); const text = screen.getByRole('textbox'); const button = screen.getByRole('button'); await user.type(text, 'a'); await user.click(button); expect(within(screen.getByRole('list')).queryAllByRole('listitem')).toHaveLength(0); expect(container).toMatchSnapshot(); await user.type(text, 'b'); await user.click(button); expect(within(screen.getByRole('list')).queryAllByRole('listitem')).toHaveLength(1); expect(screen.getByRole('listitem')).toHaveTextContent('a'); expect(container).toMatchSnapshot(); });
手動での動作確認は、こんな感じでAppコンポーネントに組み込むコンポーネントを変えながら行っています。
src/main.tsx
import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App.tsx'; createRoot(document.getElementById('root')!).render( <StrictMode> <App /> </StrictMode>, );
src/App.tsx
import { BadList } from './BadList'; export function App() { return <BadList />; }
改善版。
src/List.tsx
import { useState } from 'react'; type Artist = { id: number; name: string; }; let nextId = 0; export function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState<Artist[]>([]); return ( <> <h1>Inspiring sculptors:</h1> <input value={name} onChange={(e) => setName(e.target.value)} /> <button onClick={() => { setArtists([...artists, { id: nextId++, name: name }]); }} > Add </button> <ul> {artists.map((artist) => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
test/List.test.tsx
import { screen, within } from '@testing-library/react'; import { expect, test } from 'vitest'; import { List } from '../src/List'; import { setup } from './setup'; test('Updating Arrays in State, List test', async () => { const { user, container } = setup(<List />); expect(within(screen.getByRole('list')).queryAllByRole('listitem')).toHaveLength(0); expect(container).toMatchSnapshot(); const text = screen.getByRole('textbox'); const button = screen.getByRole('button'); await user.type(text, 'a'); await user.click(button); expect(within(screen.getByRole('list')).getAllByRole('listitem')).toHaveLength(1); expect(within(screen.getByRole('list')).getByRole('listitem')).toHaveTextContent('a'); expect(container).toMatchSnapshot(); await user.type(text, 'b'); await user.click(button); expect(within(screen.getByRole('list')).getAllByRole('listitem')).toHaveLength(2); expect( within(screen.getByRole('list')) .getAllByRole('listitem') .map((e) => e.textContent), ).toStrictEqual(['a', 'ab']); expect(container).toMatchSnapshot(); await user.type(text, 'c'); await user.click(button); expect(within(screen.getByRole('list')).getAllByRole('listitem')).toHaveLength(3); expect( within(screen.getByRole('list')) .getAllByRole('listitem') .map((e) => e.textContent), ).toStrictEqual(['a', 'ab', 'abc']); expect(container).toMatchSnapshot(); });
Removing from an array
Updating Arrays in State / Removing from an array
配列から要素の削除。
src/List2.tsx
import { useState } from 'react'; type Artist = { id: number; name: string; }; const initialArtists: Artist[] = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye' }, { id: 2, name: 'Louise Nevelson' }, ]; export function List2() { const [artists, setArtists] = useState(initialArtists); return ( <> <h1>Inspiring sculptors:</h1> <ul> {artists.map((artist) => ( <li key={artist.id}> {artist.name}{' '} <button onClick={() => { setArtists(artists.filter((a) => a.id !== artist.id)); }} > Delete </button> </li> ))} </ul> </> ); }
test/List2.test.tsx
import { screen, within } from '@testing-library/react'; import { expect, test } from 'vitest'; import { List2 } from '../src/List2'; import { setup } from './setup'; test('Updating Arrays in State, List2 test', async () => { const { user, container } = setup(<List2 />); expect(screen.getAllByRole('listitem')).toHaveLength(3); expect( screen.getAllByRole('listitem').flatMap((e) => Array.from(e.childNodes) .filter((e) => e.nodeType === Node.TEXT_NODE) .map((e) => e.textContent) .join(''), ), ).toStrictEqual(['Marta Colvin Andrade ', 'Lamidi Olonade Fakeye ', 'Louise Nevelson ']); expect(container).toMatchSnapshot(); await user.click(within(screen.getAllByRole('listitem')[1]).getByRole('button')); expect(screen.getAllByRole('listitem')).toHaveLength(2); expect( screen.getAllByRole('listitem').flatMap((e) => Array.from(e.childNodes) .filter((e) => e.nodeType === Node.TEXT_NODE) .map((e) => e.textContent) .join(''), ), ).toStrictEqual(['Marta Colvin Andrade ', 'Louise Nevelson ']); expect(container).toMatchSnapshot(); });
Transforming an array
Updating Arrays in State / Transforming an array
配列の要素を変更(変換)。
src/ShapeEditor.tsx
import { useState } from 'react'; type Shape = { id: number; type: string; x: number; y: number; }; const initialShapes: Shape[] = [ { id: 0, type: 'circle', x: 50, y: 100 }, { id: 1, type: 'square', x: 150, y: 100 }, { id: 2, type: 'circle', x: 250, y: 100 }, ]; export function ShapeEditor() { const [shapes, setShapes] = useState(initialShapes); const handleClick = () => { const nextShapes = shapes.map((shape) => { if (shape.type === 'square') { return shape; } else { return { ...shape, y: shape.y + 50, }; } }); setShapes(nextShapes); }; return ( <> <button onClick={handleClick}>Move circles down!</button> {shapes.map((shape) => ( <div key={shape.id} style={{ background: 'purple', position: 'absolute', left: shape.x, top: shape.y, borderRadius: shape.type === 'circle' ? '50%' : '', width: 20, height: 20, }} data-testid={'shape-' + shape.id} /> ))} </> ); }
test/ShapeEditor.test.tsx
import { screen } from '@testing-library/react'; import { expect, test } from 'vitest'; import { ShapeEditor } from '../src/ShapeEditor'; import { setup } from './setup'; test('Updating Arrays in State, ShapeEditor test', async () => { const { user, container } = setup(<ShapeEditor />); expect(screen.getByTestId('shape-0')).toHaveStyle({ left: '50px', top: '100px' }); expect(screen.getByTestId('shape-1')).toHaveStyle({ left: '150px', top: '100px' }); expect(screen.getByTestId('shape-2')).toHaveStyle({ left: '250px', top: '100px' }); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByTestId('shape-0')).toHaveStyle({ left: '50px', top: '150px' }); expect(screen.getByTestId('shape-1')).toHaveStyle({ left: '150px', top: '100px' }); expect(screen.getByTestId('shape-2')).toHaveStyle({ left: '250px', top: '150px' }); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByTestId('shape-0')).toHaveStyle({ left: '50px', top: '200px' }); expect(screen.getByTestId('shape-1')).toHaveStyle({ left: '150px', top: '100px' }); expect(screen.getByTestId('shape-2')).toHaveStyle({ left: '250px', top: '200px' }); expect(container).toMatchSnapshot(); });
Replacing items in an array
Updating Arrays in State / Replacing items in an array
配列の要素を置換。
src/CounterList.tsx
import { useState } from 'react'; const initialCounters: number[] = [0, 0, 0]; export function CounterList() { const [counters, setCounters] = useState(initialCounters); const handleIncrementClick = (index: number) => { const nextCounters = counters.map((c, i) => { if (i === index) { return c + 1; } else { return c; } }); setCounters(nextCounters); }; return ( <ul> {counters.map((counter, i) => ( <li key={i}> {counter} <button onClick={() => { handleIncrementClick(i); }} > +1 </button> </li> ))} </ul> ); }
test/CounterList.test.tsx
import { screen, within } from '@testing-library/react'; import { expect, test } from 'vitest'; import { CounterList } from '../src/CounterList'; import { setup } from './setup'; test('Updating Arrays in State, CounterList test', async () => { const { user, container } = setup(<CounterList />); expect( screen.getAllByRole('listitem').flatMap((e) => Array.from(e.childNodes) .filter((e) => e.nodeType === Node.TEXT_NODE) .map((e) => e.textContent), ), ).toStrictEqual(['0', '0', '0']); expect(container).toMatchSnapshot(); await user.click(within(screen.getAllByRole('listitem')[0]).getByRole('button')); expect( screen.getAllByRole('listitem').flatMap((e) => Array.from(e.childNodes) .filter((e) => e.nodeType === Node.TEXT_NODE) .map((e) => e.textContent), ), ).toStrictEqual(['1', '0', '0']); expect(container).toMatchSnapshot(); await user.click(within(screen.getAllByRole('listitem')[2]).getByRole('button')); expect( screen.getAllByRole('listitem').flatMap((e) => Array.from(e.childNodes) .filter((e) => e.nodeType === Node.TEXT_NODE) .map((e) => e.textContent), ), ).toStrictEqual(['1', '0', '1']); expect(container).toMatchSnapshot(); await user.click(within(screen.getAllByRole('listitem')[1]).getByRole('button')); expect( screen.getAllByRole('listitem').flatMap((e) => Array.from(e.childNodes) .filter((e) => e.nodeType === Node.TEXT_NODE) .map((e) => e.textContent), ), ).toStrictEqual(['1', '1', '1']); expect(container).toMatchSnapshot(); await user.click(within(screen.getAllByRole('listitem')[0]).getByRole('button')); expect( screen.getAllByRole('listitem').flatMap((e) => Array.from(e.childNodes) .filter((e) => e.nodeType === Node.TEXT_NODE) .map((e) => e.textContent), ), ).toStrictEqual(['2', '1', '1']); expect(container).toMatchSnapshot(); });
Inserting into an array
Updating Arrays in State / Inserting into an array
配列に要素を挿入。
src/List3.tsx
import { useState } from 'react'; type Artist = { id: number; name: string; }; const initialArtists: Artist[] = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye' }, { id: 2, name: 'Louise Nevelson' }, ]; let nextId = initialArtists.length; export function List3() { const [name, setName] = useState(''); const [artists, setArtists] = useState(initialArtists); const handleClick = () => { const insertAt = 1; const nextArtists = [ ...artists.slice(0, insertAt), { id: nextId++, name: name }, ...artists.slice(insertAt), ]; setArtists(nextArtists); }; return ( <> <h1>Inspiring sculptors:</h1> <input value={name} onChange={(e) => setName(e.target.value)} /> <button onClick={handleClick}>Insert</button> <ul> {artists.map((artist) => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
test/List3.test.tsx
import { screen } from '@testing-library/react'; import { expect, test } from 'vitest'; import { List3 } from '../src/List3'; import { setup } from './setup'; test('Updating Arrays in State, List3 test', async () => { const { user, container } = setup(<List3 />); expect(screen.getAllByRole('listitem')).toHaveLength(3); expect(screen.getAllByRole('listitem').map((e) => e.textContent)).toStrictEqual([ 'Marta Colvin Andrade', 'Lamidi Olonade Fakeye', 'Louise Nevelson', ]); expect(container).toMatchSnapshot(); await user.type(screen.getByRole('textbox'), 'Hello World'); await user.click(screen.getByRole('button')); expect(screen.getAllByRole('listitem')).toHaveLength(4); expect(screen.getAllByRole('listitem').map((e) => e.textContent)).toStrictEqual([ 'Marta Colvin Andrade', 'Hello World', 'Lamidi Olonade Fakeye', 'Louise Nevelson', ]); expect(container).toMatchSnapshot(); await user.clear(screen.getByRole('textbox')); await user.type(screen.getByRole('textbox'), 'Foo Bar'); await user.click(screen.getByRole('button')); expect(screen.getAllByRole('listitem')).toHaveLength(5); expect(screen.getAllByRole('listitem').map((e) => e.textContent)).toStrictEqual([ 'Marta Colvin Andrade', 'Foo Bar', 'Hello World', 'Lamidi Olonade Fakeye', 'Louise Nevelson', ]); expect(container).toMatchSnapshot(); });
Making other changes to an array
Updating Arrays in State / Making other changes to an array
配列の要素を変更すると、予期しない副作用を生むという話。
src/List4.tsx
import { useState } from 'react'; type Artwork = { id: number; title: string; }; const initialList: Artwork[] = [ { id: 0, title: 'Big Bellies' }, { id: 1, title: 'Lunar Landscape' }, { id: 2, title: 'Terracotta Army' }, ]; export function List4() { const [list, setList] = useState(initialList); const handleClick = () => { const nextList = [...list]; nextList.reverse(); setList(nextList); }; return ( <> <button onClick={handleClick}>Reverse</button> <ul> {list.map((artwork) => ( <li key={artwork.id}>{artwork.title}</li> ))} </ul> </> ); }
test/List4.test.tsx
import { screen } from '@testing-library/react'; import { expect, test } from 'vitest'; import { List4 } from '../src/List4'; import { setup } from './setup'; test('Updating Arrays in State, List4 test', async () => { const { user, container } = setup(<List4 />); expect(screen.getAllByRole('listitem')).toHaveLength(3); expect(screen.getAllByRole('listitem').map((e) => e.textContent)).toStrictEqual([ 'Big Bellies', 'Lunar Landscape', 'Terracotta Army', ]); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getAllByRole('listitem')).toHaveLength(3); expect(screen.getAllByRole('listitem').map((e) => e.textContent)).toStrictEqual([ 'Terracotta Army', 'Lunar Landscape', 'Big Bellies', ]); expect(container).toMatchSnapshot(); });
Updating objects inside arrays
Updating Arrays in State / Updating objects inside arrays
src/BucketList.tsx
import { useState } from 'react'; type Artwork = { id: number; title: string; seen: boolean; }; const initialList: Artwork[] = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export function BucketList() { const [myList, setMyList] = useState(initialList); const [yourList, setYourList] = useState(initialList); const handleToggleMyList = (artworkId: number, nextSeen: boolean) => { setMyList( myList.map((artwork) => { if (artwork.id === artworkId) { return { ...artwork, seen: nextSeen }; } else { return artwork; } }), ); }; const handleToggleYourList = (artworkId: number, nextSeen: boolean) => { setYourList( yourList.map((artwork) => { if (artwork.id === artworkId) { return { ...artwork, seen: nextSeen }; } else { return artwork; } }), ); }; return ( <> <h1>Art Bucket List</h1> <h2>My list of art to see:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>Your list of art to see:</h2> <ItemList artworks={yourList} onToggle={handleToggleYourList} /> </> ); } function ItemList({ artworks, onToggle, }: { artworks: Artwork[]; onToggle: (artworkId: number, nextSeen: boolean) => void; }) { return ( <ul> {artworks.map((artwork) => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={(e) => { onToggle(artwork.id, e.target.checked); }} /> {artwork.title} </label> </li> ))} </ul> ); }
test/BucketList.test.tsx
import { screen, within } from '@testing-library/react'; import { expect, test } from 'vitest'; import { BucketList } from '../src/BucketList'; import { setup } from './setup'; test('Updating Arrays in State, BadBucketList test', async () => { const { user, container } = setup(<BucketList />); expect(screen.getAllByRole('list')).toHaveLength(2); expect(within(screen.getAllByRole('list')[0]).getAllByRole('listitem')).toHaveLength(3); expect(within(screen.getAllByRole('list')[1]).getAllByRole('listitem')).toHaveLength(3); expect( within(screen.getAllByRole('list')[0]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([false, false, true]); expect( within(screen.getAllByRole('list')[1]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([false, false, true]); expect(container).toMatchSnapshot(); await user.click(within(screen.getAllByRole('list')[0]).getAllByRole('checkbox')[0]); expect( within(screen.getAllByRole('list')[0]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([true, false, true]); expect( within(screen.getAllByRole('list')[1]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([false, false, true]); expect(container).toMatchSnapshot(); await user.click(within(screen.getAllByRole('list')[1]).getAllByRole('checkbox')[1]); expect( within(screen.getAllByRole('list')[0]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([true, false, true]); expect( within(screen.getAllByRole('list')[1]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([false, true, true]); expect(container).toMatchSnapshot(); });
Write concise update logic with Immer
Updating Arrays in State / Updating objects inside arrays / Write concise update logic with Immer
Immerを使って配列の要素を更新する例。
src/BucketList2.tsx
import { useImmer } from 'use-immer'; type Artwork = { id: number; title: string; seen: boolean; }; const initialList: Artwork[] = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export function BucketList2() { const [myList, updateMyList] = useImmer(initialList); const [yourList, updateYourList] = useImmer(initialList); const handleToggleMyList = (id: number, seen: boolean) => { updateMyList((draft) => { const artwork = draft.find((a) => a.id === id); artwork!.seen = seen; }); }; const handleToggleYourList = (id: number, seen: boolean) => { updateYourList((draft) => { const artwork = draft.find((a) => a.id === id); artwork!.seen = seen; }); }; return ( <> <h1>Art Bucket List</h1> <h2>My list of art to see:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>Your list of art to see:</h2> <ItemList artworks={yourList} onToggle={handleToggleYourList} /> </> ); } function ItemList({ artworks, onToggle, }: { artworks: Artwork[]; onToggle: (artworkId: number, nextSeen: boolean) => void; }) { return ( <ul> {artworks.map((artwork) => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={(e) => { onToggle(artwork.id, e.target.checked); }} /> {artwork.title} </label> </li> ))} </ul> ); }
test/BucketList2.test.tsx
import { screen, within } from '@testing-library/react'; import { expect, test } from 'vitest'; import { BucketList2 } from '../src/BucketList2'; import { setup } from './setup'; test('Updating Arrays in State, BadBucketList2 test', async () => { const { user, container } = setup(<BucketList2 />); expect(screen.getAllByRole('list')).toHaveLength(2); expect(within(screen.getAllByRole('list')[0]).getAllByRole('listitem')).toHaveLength(3); expect(within(screen.getAllByRole('list')[1]).getAllByRole('listitem')).toHaveLength(3); expect( within(screen.getAllByRole('list')[0]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([false, false, true]); expect( within(screen.getAllByRole('list')[1]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([false, false, true]); expect(container).toMatchSnapshot(); await user.click(within(screen.getAllByRole('list')[0]).getAllByRole('checkbox')[0]); expect( within(screen.getAllByRole('list')[0]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([true, false, true]); expect( within(screen.getAllByRole('list')[1]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([false, false, true]); expect(container).toMatchSnapshot(); await user.click(within(screen.getAllByRole('list')[1]).getAllByRole('checkbox')[1]); expect( within(screen.getAllByRole('list')[0]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([true, false, true]); expect( within(screen.getAllByRole('list')[1]) .getAllByRole('checkbox') .map((e: HTMLElement) => (e as HTMLInputElement).checked), ).toStrictEqual([false, true, true]); expect(container).toMatchSnapshot(); });
おわりに
今回は「Learn React」の「Adding Interactivity」から「Updating Arrays in State」をやってみました。
Reactでの配列の扱いを丁寧に見れたと思います。あまりしっかりやらなくてもよいかな?と思って始めたのですが、
JavaScriptやTypeScriptの復習にもなったので良かったです。
今回で「Adding Interactivity」はおしまいですね。次からは「Managing State」に入っていきます。
次のテーマからは、少しペースを変えようと思います。