このブログ投稿では、基本的な React アプリケーションを理解して構築するために必要なすべてのことを説明します。 React を使い始めたばかりの初心者でも、スキルを磨きたい経験豊富な開発者でも、このガイドは最適です。
このガイドでは、設計、レイアウト、状態管理など、完全に機能する To Do リスト アプリを構築するプロセス全体について説明します。関数コンポーネントとフックを使用します。 state と props を使用してコンポーネント間でデータを渡す方法と、ユーザー入力を処理してアプリの状態を更新する方法を学びます。
このガイドの終わりまでに、React アプリをゼロから構築する方法をしっかりと理解できるようになり、新たに得た知識を利用して独自の React プロジェクトを構築できるようになります。
それでは、始めましょう!
*これから作成するアプリのコードはこちらで、ライブ バージョンはこちらでご覧いただけます。
コードの記述には TypeScript を使用し、アプリの開発と構築には Vite を使用します。
TypeScriptは、JavaScript に基づいて構築された、厳密に型指定されたプログラミング言語です。実際には、すでに JavaScript を知っている場合、TypeScript の使用方法を学ぶ必要があるのは、型とインターフェイスの使用方法だけです。
型とインターフェイスにより、コードで使用しているデータ型を定義できます。これにより、バグを早期に発見し、問題を回避することができます。
たとえば、関数がnumber
を取ってstring
を渡すと、TypeScript はすぐに文句を言います。
const someFunc = (parameter: number) => {...}; someFunc('1') // Argument of type 'string' is not assignable to parameter of type 'number'.
JavaScript を使用していた場合、後でバグを発見することになるでしょう。
TypeScript は多くの場合自動的に型を推測できるため、常に型を指定する必要はありません。
ここで TypeScript の基本を学ぶことができます。 (または単に型を無視します。)
React アプリケーションを起動する最も一般的な方法は、おそらくcreate-react-appを使用することです。代わりにVite (「ヴィート」と発音) を使用します。しかし、心配する必要はありません。シンプルですが、より効率的です。
webpack (内部で create-react-app によって使用される) などのツールを使用すると、アプリケーション全体を単一のファイルにバンドルしてからブラウザーに提供する必要があります。一方、Vite はブラウザーのネイティブ ES モジュールを利用して、必要に応じてソース コードの一部を提供するRollupでバンドルをより効率的にします。
また、Vite は Hot Module Replacement を使用して開発時間を大幅に短縮できます。つまり、ソース コードに変更が加えられるたびに、アプリケーション全体ではなく、変更のみが更新されます。
それに加えて、Vite は Typescript、JSX および TSX、CSS などのネイティブ サポートを提供します。
create-react-app と同様に、Vite は create-vite というツールを提供しています。これにより、Vanilla JS のオプションを含む基本的なテンプレートを使用したり、React などのライブラリを使用したりして、新しいプロジェクトをすばやく開始できます。
明確にするために言うと、React アプリケーションをビルドするのに Vite や create-react-app のようなツールは必要ありませんが、プロジェクトのセットアップ、コードのバンドル、トランスパイラーの使用などを処理してくれるので、作業が楽になります。
React を使用すると、コードにマークアップを直接追加できます。これは後でプレーンな JavaScript にコンパイルされます。これはJSXと呼ばれます。 JSX を使用している場合、JavaScript の場合は .jsx、TypeScript の場合は .tsx としてファイルを保存できます。
次のようになります。
const element = <h1>Hello, world!</h1>;
HTML に似ていますが、JavaScript ファイルに埋め込まれており、プログラミング ロジックでマークアップを操作できます。中括弧内にある限り、JSX 内に JavaScript コードを追加することもできます。
たとえば、異なる段落要素としてレンダリングしたいテキストの配列がある場合、次のようにすることができます。
const paragraphs = ["First", "Second", "Third"]; paragraphs.map((paragraph) => <p>{paragraph}</p>);
そして、次のようにコンパイルされます。
<p>First</p> <p>Second</p> <p>Third</p>
しかし、それだけをしようとしてもうまくいきません。これは、React がコンポーネントで動作し、これらのコンポーネント内で JSX をレンダリングする必要があるためです。
React コンポーネントは、JavaScriptクラスまたは単純な関数を使用して記述できます。関数コンポーネントは最新であり、現在 React コンポーネントを記述するための推奨される方法であるため、ここでは関数コンポーネントに焦点を当てます。
コンポーネントは、ブラウザーによってコンパイルおよびレンダリングされる JSX を返す関数によって定義されます。上記の例を拡張して、段落要素をレンダリングする場合は、次のようになります。
// Define the component const Component = () => { const paragraphs = ["First", "Second", "Third"]; return ( <> {paragraphs.map((paragraph) => ( <p>{paragraph}</p> ))} </> ); }; // Use the component in the same way you use an HTML element in the JSX const OtherComponent = () => { return <Component />; };
ここで、このコンポーネントを別の情報で再利用したいと思うかもしれません。 props を使用してそれを行うことができます — これは、何らかのデータを保持する単なる JavaScript オブジェクトです。
この例では、配列をハードコーディングする代わりに、それをコンポーネントに渡すことができます。結果は同じになりますが、コンポーネントは再利用可能になります。
TypeScript を使用している場合は、props オブジェクト内のデータの型を指定する必要があります (それらが何であるかについてのコンテキストがないため、TypeScript はそれらを推測できません)。この場合、これは文字列の配列 ( string[]
)。
const Component = (props: { paragraphs: string[] }) => { <> {props.paragraphs.map((paragraph) => ( <p>{paragraph}</p> ))} </>; }; const OtherComponent = () => { const paragraphs = ["First", "Second", "Third"]; return <Component paragraphs={paragraphs} />; };
インタラクティブなコンポーネントを作成したい場合は、コンポーネントの状態に情報を保存する必要があります。これにより、コンポーネントはそれを「記憶」できます。
たとえば、ボタンがクリックされた回数を示す単純なカウンターを定義したい場合、この値を保存および更新する方法が必要です。 React では、 useStateフックを使用してそれを行うことができます (フックは、Reactの状態とライフサイクル機能に「フック」できる関数です)。
初期値でuseState
フックを呼び出すと、値自体とそれを更新する関数を含む配列が返されます。
import { useState } from "react"; const Counter = () => { const [count, setCount] = useState(0); return ( <> <span>{count}</span> <button onClick={() => setCount(count + 1)}>Increment count</button> </> ); };
この知識があれば、React アプリの構築を開始する準備が整いました。
Vite を使用するには、**ノード**とパッケージ マネージャーが必要です。
ノードをインストールするには、システムと構成に応じて、ここでオプションの 1 つを選択するだけです。 Linux または Mac を使用している場合は、 Homebrewを使用してインストールすることもできます。
パッケージ マネージャーはnpmまたはyarnです。この投稿では、 npmを使用します。
次に、プロジェクトを作成します。ターミナルで、プロジェクトが作成されるディレクトリに移動し、create-vite コマンドを実行します。
$ npm create vite@latest
追加のパッケージ (create-vite など) をインストールするように求められる場合があります。 y
と入力し、Enter キーを押して続行します。
Need to install the following packages: [email protected] Ok to proceed? (y)
次に、プロジェクト情報を入力するよう求められます。
プロジェクトの名前を入力します。 my-react-project
を選択しました。
? Project name: › my-react-project
「フレームワーク」としてReactを選択します。
React は技術的にはライブラリであり、フレームワークではありませんが、心配する必要はありません。
? Select a framework: › - Use arrow-keys. Return to submit. Vanilla Vue ❯ React Preact Lit Svelte Others
TypeScript + SWCをバリアントとして選択します。
SWC (Speedy Web Compiler の略) は、Rust で書かれた超高速の TypeScript / JavaScript コンパイラです。彼らは、「シングル スレッドで Babel より 20 倍、4 コアで 70 倍速い」と主張しています。
? Select a variant: › - Use arrow-keys. Return to submit. JavaScript TypeScript JavaScript + SWC ❯ TypeScript + SWC
これで、プロジェクトが作成されました。開発モードで起動するには、プロジェクト ディレクトリに移動し、依存関係をインストールして、dev スクリプト コマンドを実行する必要があります。
cd my-react-project npm install npm run dev
数秒後、次のようなものが表示されます。
VITE v4.0.4 ready in 486 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h to show help
ブラウザーを開いてhttp://localhost:5173 / に移動すると、デフォルトの Vite + React ページが表示されます。
これは、すべてが正常であることを意味し、アプリの作業を開始できます。
コード エディターまたは選択した IDE でプロジェクトを開くと、次のようなファイル構造が表示されます。
ボイラープレート ファイルの一部は使用しないため、削除できます (すべての .svg および .css ファイル)。
App 関数のコードを削除すると、次のようになります。
function App() { return ( ) } export default App
このファイルについては後で説明します。
ここではスタイリングに焦点を当てていませんが、Tailwind CSS を使用します。これは、HTML 要素にクラスを追加してスタイルを設定できるライブラリです。以下の手順に従って、独自のプロジェクトに反映されたスタイルを確認してください。
それ以外の場合は、コード内のクラスを無視できます。
設計プロセスは、アプリの開発に不可欠な部分であり、見落とされるべきではありません。
To Do リスト アプリを作成するには、まずコンポーネントのレイアウトを考える必要があります。
まず、基本的な UI をモックし、関連するコンポーネントの階層を概説します。
デザイナーでない場合は、色や正確な配置に関して完璧または最終的な UI である必要はありません。コンポーネントの構造を考えることがより重要です。
理想的には、単一責任の原則に従って、コンポーネントが 1 つのことだけを担当する必要があります。
下の画像で、紫色の名前はこれから構築するコンポーネントです。それ以外はすべてネイティブ HTML 要素です。お互いの中にいるということは、親子関係が成立する可能性が高いということです。
スケッチができたら、アプリの静的バージョンの構築を開始できます。つまり、UI 要素だけで、インタラクティブ性はまだありません。この部分は非常に簡単で、コツをつかめば、多くのタイピングとほとんど考える必要がありません。
静的バージョンのコードは、このGitHub リポジトリのブランチ「static-version」にあります。完全に機能するアプリのコードはメイン ブランチです。
容器
上で概説したように、アプリのすべてのセクションで再利用されるコンテナを作成します。このコンテナは、さまざまな要素を構成する方法の 1 つを示しています。それらを子として渡すことです。
// src/components/Container.tsx const Container = ({ children, title, }: { children: JSX.Element | JSX.Element[]; title?: string; }) => { return ( <div className="bg-green-600 p-4 border shadow rounded-md"> {title && <h2 className="text-xl pb-2 text-white">{title}</h2>} <div>{children}</div> </div> ); }; export default Container;
JSX.Element | JSX.Element[]
タイプのchildren
パラメータを持つ props オブジェクトを取ります。 JSX.Element | JSX.Element[]
.これは、他の HTML 要素や作成した他のコンポーネントと一緒に構成できることを意味します。コンテナ内のどこにでもレンダリングできます — この場合は 2 番目の div 内です。
このアプリでは、App コンポーネント内で使用するときに各セクション (以下で定義) をレンダリングします。
Container は、オプションでtitle
という名前のstring
prop も取ります。これは、h2 が存在する場合は常に h2 内でレンダリングされます。
// src/App.tsx import Container from "./components/Container"; import Input from "./components/Input"; import Summary from "./components/Summary/Summary"; import Tasks from "./components/Tasks/Tasks"; function App() { return ( <div className="flex justify-center m-5"> <div className="flex flex-col items-center"> <div className="sm:w-[640px] border shadow p-10 flex flex-col gap-10"> <Container title={"Summary"}> <Summary /> </Container> <Container> <Input /> </Container> <Container title={"Tasks"}> <Tasks /> </Container> </div> </div> </div> ); } export default App;
まとめ
最初のセクションは、タスクの総数、保留中のタスクの数、および完了したタスクの数の 3 つの項目 (SummaryItem) を示す要約 (Summary コンポーネント) です。これは、コンポーネントを構成する別の方法です。別のコンポーネントの return ステートメントで使用するだけです。
(ただし、不要な再レンダリングやバグにつながる可能性があるため、別のコンポーネント内でコンポーネントを定義しないことが重要です。)
今のところ、2 つのコンポーネントで静的データのみを使用できます。
// src/components/Summary/SummaryItem.tsx const SummaryItem = ({ itemName, itemValue, }: { itemName: string; itemValue: number; }) => { return ( <article className="bg-green-50 w-36 rounded-sm flex justify-between p-2"> <h3 className="font-bold">{itemName}</h3> <span className="bg-green-900 text-white px-2 rounded-sm"> {itemValue} </span> </article> ); }; export default SummaryItem; // src/components/Summary/Summary.tsx import SummaryItem from "./SummaryItem"; const Summary = () => { return ( <> <div className="flex justify-between"> <SummaryItem itemName={"Total"} itemValue={3} /> <SummaryItem itemName={"To do"} itemValue={2} /> <SummaryItem itemName={"Done"} itemValue={1} /> </div> </> ); }; export default Summary;
SummaryItem は、string 型のitemName
とnumber
型のitemValue
の 2 つの props を取ることに気付くでしょう。これらの props は、Summary コンポーネント内で SummaryItem コンポーネントが使用されるときに渡され、SummaryItem JSX でレンダリングされます。
タスク
同様に、タスク セクション (最後のセクション) には、TaskItem コンポーネントをレンダリングする Tasks コンポーネントがあります。
今のところ静的データもあります。後でタスク名とステータスを props として TaskItem コンポーネントに渡して、再利用可能で動的にする必要があります。
// src/components/Tasks/TaskItem.tsx const TaskItem = () => { return ( <div className="flex justify-between bg-white p-1 px-3 rounded-sm"> <div className="flex gap-2 items-center"> <input type="checkbox" /> Task name </div> <button className="bg-green-200 hover:bg-green-300 rounded-lg p-1 px-3"> Delete </button> </div> ); }; export default TaskItem; // src/components/Tasks/Tasks.tsx import TaskItem from "./TaskItem"; const Tasks = () => { return ( <div className="flex flex-col gap-2"> <TaskItem /> </div> ); }; export default Tasks;
入力
最後に、入力コンポーネントは、ラベル、テキスト型の入力、「タスクを追加」ボタンを備えたフォームです。今のところ何もしませんが、すぐに変更します。
// src/components/Input.tsx const InputContainer = () => { return ( <form action="" className="flex flex-col gap-4"> <div className="flex flex-col"> <label className="text-white">Enter your next task:</label> <input className="p-1 rounded-sm" /> </div> <button type="button" className="bg-green-100 rounded-lg hover:bg-green-200 p-1" > Add task </button> </form> ); }; export default InputContainer;
React にインタラクティブ性を追加するには、コンポーネントの状態に情報を保存する必要があります。
しかし、それを行う前に、時間の経過とともにデータをどのように変化させたいかを考える必要があります。このデータの最小限の表現を特定し、この状態を保存するためにどのコンポーネントを使用する必要があるかを特定する必要があります。
状態の最小限の表現
状態には、アプリをインタラクティブにするために必要なすべての情報が含まれている必要がありますが、それ以上のものは含まれていません。別の値から値を計算できる場合は、そのうちの 1 つだけを状態に保つ必要があります。これにより、コードが冗長になるだけでなく、矛盾する状態値に関連するバグが発生しにくくなります。
この例では、合計タスク、保留中のタスク、および完了したタスクの値を追跡する必要があると考えるかもしれません。
ただし、タスクを追跡するには、各タスクとそのステータス (保留中または完了) を表すオブジェクトを含む 1 つの配列があれば十分です。
const tasks = [ { name: "task one", done: false, }, { name: "task two", done: true, }, ];
このデータを使用すると、配列メソッドを使用して、レンダリング時に必要な他のすべての情報をいつでも見つけることができます。また、矛盾の可能性も回避します。たとえば、合計 4 つのタスクがあるのに、保留中のタスクが 1 つと完了済みのタスクが 1 つしかないなどです。
インタラクティブにするために、フォーム (入力コンポーネント内) にも状態が必要です。
州が住むべき場所
次のように考えてみてください。状態に保存するデータにアクセスする必要があるのはどのコンポーネントですか?単一のコンポーネントの場合、状態はこのコンポーネント自体に存在できます。データが必要なコンポーネントが複数ある場合は、これらのコンポーネントの共通の親を見つける必要があります。
この例では、入力コンポーネントを制御するために必要な状態は、そこでのみアクセスする必要があるため、このコンポーネントに対してローカルにすることができます。
// src/components/Input.tsx import { useState } from "react"; const InputContainer = () => { const [newTask, setNewTask] = useState(""); // Initialize newTask and setNewTask return ( <form action="" className="flex flex-col gap-4"> <div className="flex flex-col"> <label className="text-white">Enter your next task:</label> <input className="p-1 rounded-sm" type="text" value={newTask} // Set the input value to newTask onChange={(e) => setNewTask(e.target.value)} // Set newTask to the input value whenever the user types something /> </div> <button type="submit" className="bg-green-100 rounded-lg hover:bg-green-200 p-1" > Add task </button> </form> ); }; export default InputContainer;
これは、入力にnewTask
値を表示し、入力が変化するたびに (つまり、ユーザーが何かを入力したときに) setNewTask
関数を呼び出すことを行っています。
UI にすぐに変化が見られるわけではありませんが、これは、入力を制御し、後で使用するためにその値にアクセスできるようにするために必要です。
ただし、タスクを追跡する状態は、SummaryItem コンポーネント (合計、保留中、および完了したタスクの数を表示する必要があります) および TaskItem コンポーネント (必要なタスク) でアクセスする必要があるため、別の方法で処理する必要があります。タスク自体を表示します)。この情報は常に同期している必要があるため、同じ状態である必要があります。
コンポーネント ツリーを見てみましょう (これにはReact 開発ツールを使用できます)。
最初の共通の親コンポーネントは App であることがわかります。これが、タスクの状態が存在する場所です。
状態が整ったら、データを使用する必要があるコンポーネントにデータを props として渡すだけです。
(親状態に変更を加えて永続化する方法についてはまだ心配していません。それは次の部分です。)
// src/App.tsx import { useState } from "react"; import { v4 as uuidv4 } from "uuid"; import Container from "./components/Container"; import Input from "./components/Input"; import Summary from "./components/Summary/Summary"; import Tasks from "./components/Tasks/Tasks"; export interface Task { name: string; done: boolean; id: string; } const initialTasks = [ { name: "task one", done: false, id: uuidv4(), }, { name: "task two", done: true, id: uuidv4(), }, ]; function App() { const [tasks, setTasks] = useState<Task[]>(initialTasks); return ( <div className="flex justify-center m-5"> <div className="flex flex-col items-center"> <div className="border shadow p-10 flex flex-col gap-10 sm:w-[640px]"> <Container title={"Summary"}> <Summary tasks={tasks} /> </Container> <Container> <Input /> </Container> <Container title={"Tasks"}> <Tasks tasks={tasks} /> </Container> </div> </div> </div> ); } export default App;
ここでは、タスクの値をダミー データ ( initialTasks
) で初期化しています。これは、アプリが終了する前に視覚化できるようにするためです。後で空の配列に変更できるため、新しいユーザーがアプリを新たに開いたときにタスクが表示されません。
name
プロパティとdone
プロパティに加えて、id もタスク オブジェクトに追加します。これはすぐに必要になります。
タスク オブジェクトの値の型を使用してinterface
を定義し、それをuseState
関数に渡します。 tasks
の初期値を空の配列に変更したり、props として渡したりすると、TypeScript はそれを推測できないため、この場合はこれが必要です。
最後に、タスクを小道具として Summary および Tasks コンポーネントに渡していることに注意してください。これらのコンポーネントは、それに対応するために変更する必要があります。
// src/components/Summary/Summary.tsx import { Task } from "../../App"; import SummaryItem from "./SummaryItem"; const Summary = ({ tasks }: { tasks: Task[] }) => { const total = tasks.length; const pending = tasks.filter((t) => t.done === false).length; const done = tasks.filter((t) => t.done === true).length; return ( <> <div className="flex flex-col gap-1 sm:flex-row sm:justify-between"> <SummaryItem itemName={"Total"} itemValue={total} /> <SummaryItem itemName={"To do"} itemValue={pending} /> <SummaryItem itemName={"Done"} itemValue={done} /> </div> </> ); }; export default Summary;
Summary コンポーネントを更新して、 tasks
を小道具として受け入れるようになりました。また、値total
、 pending
、 done
も定義しました。これらは、以前の静的なitemValue
の代わりに、SummaryItem コンポーネントに props として渡されます。
// src/components/Tasks/Tasks.tsx import { Task } from "../../App"; import TaskItem from "./TaskItem"; const Tasks = ({ tasks }: { tasks: Task[] }) => { return ( <div className="flex flex-col gap-2"> {tasks.map((t) => ( <TaskItem key={t.id} name={t.name} /> ))} </div> ); }; export default Tasks; // src/components/Tasks/TaskItem.tsx import { useState } from "react"; const TaskItem = ({ name }: { name: string }) => { const [done, setDone] = useState(false); return ( <div className="flex justify-between bg-white p-1 px-3 rounded-sm gap-4"> <div className="flex gap-2 items-center"> <input type="checkbox" checked={done} onChange={() => setDone(!done)} /> {name} </div> <button className="bg-green-200 hover:bg-green-300 rounded-lg p-1 px-3"> Delete </button> </div> ); }; export default TaskItem;
Tasks コンポーネントの場合、 task
もプロップとして取り、そのname
プロパティを TaskItem コンポーネントにマップします。その結果、 tasks
配列内の各オブジェクトの TaskItem コンポーネントを取得します。また、 name
を小道具として受け入れるように TaskItem コンポーネントを更新します。
子コンポーネントのリストを取得するたびに一意のキーを渡す必要があるため、ここで id が役に立ちます。キーを追加しないと、 rerender でバグが発生する可能性があります。 (本番アプリでは、ID はバックエンドから取得される可能性が最も高いでしょう。)
今のところの結果は次のとおりです。
サマリー番号とダミー データを反映したタスク名が既に表示されています。しかし、タスクを追加または削除する方法がまだありません。
逆データ フローの追加
アプリを完成させるには、App コンポーネントの状態 (タスク データがある場所) を Input および TaskItem 子コンポーネントから変更する方法が必要です。
そのために、 useState
フックによって生成された関数を使用してイベント ハンドラーを定義し、それらを props として渡します。これを行ったら、子コンポーネントからの適切なユーザー操作中にそれらを呼び出すだけです。
バグの原因となるため、状態を更新するときは常に状態を変更しないでください。状態オブジェクトを更新するときは、常に新しい状態オブジェクトに置き換えてください。
以下は、ハンドラーが宣言され、小道具として Input および Tasks コンポーネントに渡された最終的な App コンポーネントです。
handleSubmit
は、古いタスクと新しいタスクを含む新しい配列を返します。 toggleDoneTask
は、指定されたid
に対して、反対のdone
プロパティを持つ新しい配列を返します。 handleDeleteTask
は、指定されたid
のタスクを含まない新しい配列を返します。
// src/App.tsx import { FormEvent, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import Container from "./components/Container"; import Input from "./components/Input"; import Summary from "./components/Summary/Summary"; import Tasks from "./components/Tasks/Tasks"; export interface Task { name: string; done: boolean; id: string; } function App() { const [tasks, setTasks] = useState<Task[]>([]); const handleSubmit = (e: FormEvent<HTMLFormElement>, value: string) => { e.preventDefault(); const newTask = { name: value, done: false, id: uuidv4(), }; setTasks((tasks) => [...tasks, newTask]); }; const toggleDoneTask = (id: string, done: boolean) => { setTasks((tasks) => tasks.map((t) => { if (t.id === id) { t.done = done; } return t; }) ); }; const handleDeleteTask = (id: string) => { setTasks((tasks) => tasks.filter((t) => t.id !== id)); }; return ( <div className="flex justify-center m-5"> <div className="flex flex-col items-center"> <div className="border shadow p-10 flex flex-col gap-10 sm:w-[640px]"> <Container title={"Summary"}> <Summary tasks={tasks} /> </Container> <Container> <Input handleSubmit={handleSubmit} /> </Container> <Container title={"Tasks"}> <Tasks tasks={tasks} toggleDone={toggleDoneTask} handleDelete={handleDeleteTask} /> </Container> </div> </div> </div> ); } export default App;
これは、 handleSubmit
を使用して App コンポーネントの状態を更新する最終的な入力コンポーネントです。
// src/components/Input.tsx import { FormEvent, useState } from "react"; const InputContainer = ({ handleSubmit, }: { handleSubmit: (e: FormEvent<HTMLFormElement>, value: string) => void; }) => { const [newTaskName, setNewTaskName] = useState(""); return ( <form action="" className="flex flex-col gap-4" onSubmit={(e) => { handleSubmit(e, newTaskName); setNewTaskName(""); }} > <div className="flex flex-col"> <label className="text-white">Enter your next task:</label> <input className="p-1 rounded-sm" type="text" value={newTaskName} onChange={(e) => setNewTaskName(e.target.value)} /> </div> <button type="submit" className="bg-green-100 rounded-lg hover:bg-green-200 p-1" > Add task </button> </form> ); }; export default InputContainer;
これは、App から TaskItem に props を渡すように更新した最終的な Tasks コンポーネントです。また、「まだタスクがありません!」を返す三項演算子も追加しました。タスクがないとき。
// src/components/Tasks/Tasks.tsx import { Task } from "../../App"; import TaskItem from "./TaskItem"; const Tasks = ({ tasks, toggleDone, handleDelete, }: { tasks: Task[]; toggleDone: (id: string, done: boolean) => void; handleDelete: (id: string) => void; }) => { return ( <div className="flex flex-col gap-2"> {tasks.length ? ( tasks.map((t) => ( <TaskItem key={t.id} name={t.name} done={t.done} id={t.id} toggleDone={toggleDone} handleDelete={handleDelete} /> )) ) : ( <span className="text-green-100">No tasks yet!</span> )} </div> ); }; export default Tasks;
これが最後の TaskItem コンポーネントで、 toggleDone
とhandleDelete
を使用して App コンポーネントの状態を更新します。
// src/components/Tasks/TaskItem.tsx const TaskItem = ({ name, done, id, toggleDone, handleDelete, }: { name: string; done: boolean; id: string; toggleDone: (id: string, done: boolean) => void; handleDelete: (id: string) => void; }) => { return ( <div className="flex justify-between bg-white p-1 px-3 rounded-sm gap-4"> <div className="flex gap-2 items-center"> <input type="checkbox" checked={done} onChange={() => toggleDone(id, !done)} /> {name} </div> <button className="bg-green-200 hover:bg-green-300 rounded-lg p-1 px-3" type="button" onClick={() => handleDelete(id)} > Delete </button> </div> ); }; export default TaskItem;
いくつかのタスクを追加した後の最終的なアプリは次のとおりです。
一緒にコーディングしている場合は、次の手順に従って独自のアプリをデプロイできます。
ここで実行したすべてのコードを含むリポジトリと、ここでアプリのライブ バージョンを見つけることができます。
結論として、to-do リスト アプリを作成することは、React とその原則について学び、理解を深めるための優れた方法です。プロセスを小さなステップに分割し、ベスト プラクティスに従うことで、機能的なアプリを比較的短時間で作成できます。
以下について説明しました。
コンポーネント、状態、および逆データ フローの主要な概念。
アプリの設計とアーキテクチャ。
単一責任原則などのベスト プラクティス
このガイドで概説されている手順に従うことで、シンプルな React アプリを構築し、それを自分のプロジェクトに適用する方法をしっかりと理解できるはずです。
ハッピーコーディング!