はじめに
Python ブームが去って React ブームが再燃している @guppy0356 です。
Next や Remix を使ってアプリを作ると格好がいいですが、個人だと規模が大きくなりがちで完遂も難しい場合が多いです。
React Hooks をひとつずつ振り返ってスキルアップしていきます ![]()
この記事では useState をピックアップします。
技術スタック
- Vite
- Storybook
- node v20.19.5
useState とは?
useState は、関数コンポーネント内で 「状態(State)」を管理するための React Hook です。
通常のローカル変数はコンポーネントが再レンダリングされるたびに初期化されてしまいますが、useState で管理された値は、React によってレンダリング間で保持されます。
また、useState が返す更新関数(例: setCounter)を実行すると、React はそのコンポーネントを**再レンダリング(Re-render)**するようスケジュールします。つまり、「値の保持」と「UIの更新トリガー」という2つの役割を担っています。
プロジェクト作成
1. プロジェクトを作成
npm create vite@latest use-state -- --template react-ts
cd use-state
npm install
2. サンプルコードをリセット
単純な構造で検証したいので、サンプルコードは削除します。
rm public/vite.svg
rm src/App.css
rm src/assets/react.svg
rm src/index.css
3. useState をつかったカウンターを追加
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 { useState } from "react"
function App() {
const [counter, setCounter] = useState(0)
const handleIncrease = () => {
setCounter(counter => counter + 1)
}
const handleDecrease = () => {
setCounter(counter => counter - 1)
}
return (
<>
<button onClick={handleIncrease}>+</button>
<button onClick={handleDecrease}>-</button>
<div>
{ counter }
</div>
</>
)
}
export default App
npm run dev を実行するとカウンターを表示できます。
4. Storybook を追加
npm create storybook@latest
推奨を選んでください
◆ What configuration should we install?
│ ● Recommended: Component development, docs, and testing features.
│ ○ Minimal: Just the essentials for component development.
5. テストを追加
src/App.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within, expect } from 'storybook/test';
import App from './App';
const meta: Meta<typeof App> = {
component: App,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof App>;
export const Default: Story = {};
export const InteractiveFlow: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('初期表示は0である', async () => {
await expect(canvas.getByText('0')).toBeInTheDocument();
});
await step('プラスボタンを押すとカウントが1に増える', async () => {
const incrementBtn = canvas.getByRole('button', { name: '+' });
await userEvent.click(incrementBtn);
await expect(canvas.getByText('1')).toBeInTheDocument();
});
await step('マイナスボタンを押すとカウントが0に戻る', async () => {
const decrementBtn = canvas.getByRole('button', { name: '-' });
await userEvent.click(decrementBtn);
await expect(canvas.getByText('0')).toBeInTheDocument();
});
},
};
6. テスト実行
npm run test:storybook
すべてのテストがパスしました ![]()
> use-state@0.0.0 test:storybook
> vitest run --project=storybook
No story files found for the specified pattern: src/**/*.mdx
info Using tsconfig paths for react-docgen
info Using tsconfig paths for react-docgen
RUN v4.0.14 /Users/akira/Project/private/use-state
info Using tsconfig paths for react-docgen
✓ storybook (chromium) src/App.stories.tsx (2 tests) 239ms
✓ Default 192ms
✓ Interactive Flow 45ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 05:37:34
Duration 2.07s (transform 0ms, setup 351ms, import 23ms, tests 239ms, environment 0ms)