CLOVER🍀

That was when it all began.

Learn ReactをVitest、React Testing Libraryを使ってやってみる(Queueing a Series of State Updates)

これは、なにをしたくて書いたもの?

こちらの「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つを飛ばしているのですが、こちらはまあいいかなと。

Render and Commit – React

State as a Snapshot – React

環境

今回の環境はこちら。

$ 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.jsonscriptsの内容。

  "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

Queueing a Series of State Updates / Updating the same state multiple times before the next render / 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

Queueing a Series of State Updates / Updating the same state multiple times before the next render / 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に関する設定を見直したのでその確認をしたくて早めにやったという意図もあったりします。