これは、なにをしたくて書いたもの?
こちらの「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」の「Adding Interactivity」から「Queueing a Series of State Updates」をやってみます。
Queueing a Series of State Updates – React
基本的には写経のシリーズです。
Learn Reactをやってみる
Learn Reactをただ写経するのではなく、以下の条件で進めていきます。再掲です。
- 環境はViteで構築する
- TypeScriptで書く
- Vitest、React Testing Libraryを使ったテストを書く
今回の対象はこちらのページです。
Queueing a Series of State Updates – React
前回のエントリーからは以下の2つを飛ばしているのですが、こちらはまあいいかなと。
環境
今回の環境はこちら。
$ node --version v22.15.0 $ npm --version 10.9.2
準備
Viteを使って、Node.jsプロジェクトを作成します。
$ npm create vite@latest queueing-a-series-of-state-updates -- --template react-ts $ cd queueing-a-series-of-state-updates
依存ライブラリー、テスト用のライブラリーをインストール。
$ npm i $ 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
インストールしたライブラリーの一覧とバージョンはこちら。
$ npm ls queueing-a-series-of-state-updates@0.0.0 /path/to/queueing-a-series-of-state-updates ├── @eslint/js@9.25.1 ├── @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.2 ├── @types/react@19.1.2 ├── @vitejs/plugin-react@4.4.1 ├── eslint-plugin-react-hooks@5.2.0 ├── eslint-plugin-react-refresh@0.4.20 ├── eslint@9.25.1 ├── globals@16.0.0 ├── jsdom@26.1.0 ├── prettier@3.5.3 ├── react-dom@19.1.0 ├── react@19.1.0 ├── typescript-eslint@8.31.1 ├── typescript@5.7.3 ├── vite@6.3.3 └── vitest@3.1.2
自動生成された不要なファイルは削除しておきます。
$ 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" },
Vitestに関するtsconfigの設定を、これまでから見直しました。
Learn ReactのAdding InteractivityのQueueing a Series of State Updatesをやってみる
テストコードではTesting Libraryのuser-eventを使いますが、セットアップ部分は共通化しておきます。
test/setup.ts
import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ReactNode } from 'react'; export function setup(jsx: ReactNode) { return { user: userEvent.setup(), ...render(jsx), }; }
React batches state updates
Queueing a Series of State Updates / React batches state updates
src/BadCounter.tsx
import { useState } from 'react'; export function BadCounter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }} > +3 </button> </> ); }
test/BadCounter.test.tsx
import { expect, test } from 'vitest'; import { BadCounter } from '../src/BadCounter'; import { setup } from './setup'; import { screen } from '@testing-library/react'; test('Queueing a Series of State Updates', async () => { const { user, container } = setup(<BadCounter />); expect(screen.getByRole('heading')).toHaveTextContent('0'); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByRole('heading')).toHaveTextContent('1'); 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 { BadCounter } from './BadCounter'; export function App() { return <BadCounter />; }
Updating the same state multiple times before the next render
Queueing a Series of State Updates / Updating the same state multiple times before the next render
src/Counter.tsx
import { useState } from 'react'; export function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber((n) => n + 1); setNumber((n) => n + 1); setNumber((n) => n + 1); }} > +3 </button> </> ); }
test/Counter.test.tsx
import { expect, test } from 'vitest'; import { Counter } from '../src/Counter'; import { setup } from './setup'; import { screen } from '@testing-library/react'; test('Queueing a Series of State Updates, Counter test', async () => { const { user, container } = setup(<Counter />); expect(screen.getByRole('heading')).toHaveTextContent('0'); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByRole('heading')).toHaveTextContent('3'); expect(container).toMatchSnapshot(); });
What happens if you update state after replacing it
src/Counter2.tsx
import { useState } from 'react'; export function Counter2() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber((n) => n + 1); }} > Increase the number </button> </> ); }
test/Counter2.test.tsx
import { screen } from '@testing-library/react'; import { expect, test } from 'vitest'; import { Counter2 } from '../src/Counter2'; import { setup } from './setup'; test('Queueing a Series of State Updates, Counter2 test', async () => { const { user, container } = setup(<Counter2 />); expect(screen.getByRole('heading')).toHaveTextContent('0'); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByRole('heading')).toHaveTextContent('6'); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByRole('heading')).toHaveTextContent('12'); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByRole('heading')).toHaveTextContent('18'); expect(container).toMatchSnapshot(); });
What happens if you replace state after updating it
src/Counter3.tsx
import { useState } from 'react'; export function Counter3() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber((n) => n + 1); setNumber(42); }} > Increase the number </button> </> ); }
test/Counter3.test.tsx
import { screen } from '@testing-library/react'; import { expect, test } from 'vitest'; import { Counter3 } from '../src/Counter3'; import { setup } from './setup'; test('Queueing a Series of State Updates, Counter3 test', async () => { const { user, container } = setup(<Counter3 />); expect(screen.getByRole('heading')).toHaveTextContent('0'); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByRole('heading')).toHaveTextContent('42'); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByRole('heading')).toHaveTextContent('42'); expect(container).toMatchSnapshot(); await user.click(screen.getByRole('button')); expect(screen.getByRole('heading')).toHaveTextContent('42'); expect(container).toMatchSnapshot(); });
おわりに
今回は「Learn React」の「Adding Interactivity」から「Queueing a Series of State Updates」をやってみました。
内容はあっさりしているのですが、現在のステートの値を元に更新するといった内容を初めて扱いました。
それから、Vitestに関する設定を見直したのでその確認をしたくて早めにやったという意図もあったりします。