作るもの
信号機のように3つのライト(赤・黄・緑)が1秒ごとに切り替わるUIをReact + TypeScriptで実装します。
こんな感じ
概要
- Reactの
useEffect
とuseState
を使って1秒ごとに状態を更新 - TypeScriptの型定義で安全に状態を扱う
-
setTimeout
で1秒後に状態を切り替え、再レンダリングを誘発 -
clearTimeout
で不要なタイマーをクリーンアップ
コードの全体像
import { NextPage } from 'next';
import { useEffect, useState } from 'react';
const Page: NextPage = () => {
const CYCLE = { red: 'green', green: 'yellow', yellow: 'red' } as const;
const [light, setlight] = useState<keyof typeof CYCLE>('red');
useEffect(() => {
const timeID = setTimeout(() => {
const nextLight = CYCLE[light];
setlight(nextLight);
console.log('再レンダリング');
}, 1000);
return () => clearTimeout(timeID);
}, [light]);
return (
<div className="mx-auto max-w-4xl">
<div className="mt-10 flex justify-center gap-x-8">
<div className={`size-12 rounded-full ${light === 'red' ? 'bg-red-700' : 'bg-gray-700'}`} />
<div className={`size-12 rounded-full ${light === 'green' ? 'bg-green-700' : 'bg-gray-700'}`} />
<div className={`size-12 rounded-full ${light === 'yellow' ? 'bg-yellow-500' : 'bg-gray-700'}`} />
</div>
</div>
);
};
export default Page;
コードを日本語化して見てみる
-
CYCLE
:信号の色の遷移を定義した定数オブジェクト -
useState
:現在の信号の色(light)を保持。初期値は'red' -
useEffect
:light
が変わるたびに1秒後の色に更新するタイマーをセット -
setTimeout
:1秒後にlight
を次の色に切り替え -
clearTimeout
:次のレンダリング前に前のタイマーを解除し、バグやリークを防ぐ -
return
:JSXで3つのライトを表現。light
の状態に応じて色を変える
各コードの詳細
型定義の2つについて
const CYCLE = { red: 'green', green: 'yellow', yellow: 'red' } as const;
-
as const
:オブジェクトをリテラル型として扱い、値が固定されるようにする。 - 結果的に
CYCLE
の値が"green"などの文字列リテラルとして認識される。
const [light, setlight] = useState<keyof typeof CYCLE>('red');
-
typeof CYCLE
:オブジェクトCYCLE
の型を取得 -
keyof
:その型のキー('red' | 'green' | 'yellow')を取り出す -
light
はこの3つのどれかしか入れられなくなり、型安全になる
useEffect内のロジック
-
setTimeout
:1秒後に次の色へ切り替える関数を実行 -
CYCLE[light]
:現在の色に応じて次の色を取得 -
setlight(nextLight)
:状態を更新して再レンダリングを誘発 -
clearTimeout(timeID)
:レンダリング前に前のタイマーをキャンセル
おまけ:カスタムフック化
自分は以下のような Next.js + TypeScript 環境を使っています。
hooks
ディレクトリに切り出すことで再利用性が上がります。
// src/hooks/useTrafficLight.ts
export const useTrafficLight = () => {
const CYCLE = { red: 'green', green: 'yellow', yellow: 'red' } as const;
const [light, setlight] = useState<keyof typeof CYCLE>('red');
useEffect(() => {
const timer = setTimeout(() => {
setlight(CYCLE[light]);
}, 1000);
return () => clearTimeout(timer);
}, [light]);
return { light };
};
// src/pages/index.tsx
import { useTrafficLight } from '@/hooks/useTrafficLight';
const Page = () => {
const { light } = useTrafficLight();
return (
<div className="mx-auto max-w-4xl">
<div className="mt-10 flex justify-center gap-x-8">
<div className={`size-12 rounded-full ${light === 'red' ? 'bg-red-700' : 'bg-gray-700'}`} />
<div className={`size-12 rounded-full ${light === 'green' ? 'bg-green-700' : 'bg-gray-700'}`} />
<div className={`size-12 rounded-full ${light === 'yellow' ? 'bg-yellow-500' : 'bg-gray-700'}`} />
</div>
</div>
);
};
export default Page;
疑問点などありましたらコメントいただけると幸いです🙇