はじめに
こちらの記事でNext.jsの導入方法について教えて頂き、ローカル環境にNext.js + TypeScriptをセットアップする事ができました。
せっかくセットアップしたのでTODOアプリを作りながらReactの機能を確認したいと思います。
TODOアプリの仕様は「はじめに」を書いている段階では特に考えてないです。
記事の内容
- Tailwindを導入できた
- ComponentにPropsを渡せた
- Componentの中でPropsを使用できた
- Componentをループを使って表示できた
- React Iconsを導入できた
1.Nextの構成
セットアップ完了時、以下のようになフォルダ構成になりました。
Nextのルーティングはpages内のディレクトリ構造とファイル名がそのまま反映されるみたいです。この辺はNuxt(Vueの方)に似ています。上記の場合index.tsxがデフォルト画面としてアプリのルートURLにアクセスした時に表示されます。
pagesディレクトリの残り2つの_app.tsx,_document.tsxについてはこちらが大変参考になりました。
またAPIも使用しないためapiディレクトリについても割愛します。
今後このTodoアプリが発展して、外部APIにリクエストを送るようになったら使いたい…
Nextセットアップの記事でcomponentsディレクトリを作成し、そこに画面を形成する部品(Button)を作成しています。
余談ですが、昨今のJavascriptsフレームワークは色々なUIをComponentとして定義する中で画面やレイアウト、UIパーツが複雑なネスト構造になりがちです。
そのため大規模なアプリはUIデザインパターンのようなルールに則って構造を整理するみたいです。
Atomic Designなど有名で参考にした事があります。(がこの記事ではあまり気にせずに作ります)
2.Tailwindの導入
Tailwindの公式ドキュメントに従います。
パッケージ管理ソフトを使用してTailwindのパッケージをインストールします。
node -v
v19.9.0
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
tailwind.config.jsを設定します。
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
// srcディレクトリの全てのファイルにTailwindを適用する
theme: {
extend: {},
},
plugins: [],
};
global.cssは多分名前からしてグローバルに適用されるスタイルと思われるので、邪魔な部分は消してTailwindの読み込みだけを残しました。
@tailwind base;
@tailwind components;
@tailwind utilities;
確認のためindex.tsxに簡単な記述をして画面表示させてみます。
export default function Home() {
return (
<div>
<h1 className="text-xl font-bold text-green-400">Hello World</h1>
// フォントサイズ大きめ、緑で太字のHello World
</div>
);
}
微調整の際flexボックスやmarginに関するTailwindのUnitiesクラスを設定する際はチートシートが非常に便利です。
開発モードでNext.jsアプリケーションを実行してみます。
npm run dev
> next-tutorial@0.1.0 dev
> next dev
▲ Next.js 14.0.2
- Local: http://localhost:3000
✓ Ready in 4.1s
3.見た目を探す
TODOアプリはやる事リストを追加したり削除したり完了させたりするアプリにしたいです。
私は見た目から入った方がやる気がでるのでTailwindが提供するかっこいいUIデザインから良さそうなコンポーネントを探します。
こちらを参考にさせて頂きテンプレを探しました。
今回はこのUIをTODO項目に使う事にします。
上記をcomponentsにTodoItemコンポーネントとして定義します。
const TodoItem = () => {
return (
<div className="flex w-full border border-gray-300 max-w-sm overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800">
<div className="flex items-center justify-center w-12 bg-emerald-500">
<svg
className="w-6 h-6 text-white fill-current"
viewBox="0 0 40 40"
xmlns="http://www.w3.org/2000/svg">
<path d="M20 3.33331C10.8 3.33331 3.33337 10.8 3.33337 20C3.33337 29.2 10.8 36.6666 20 36.6666C29.2 36.6666 36.6667 29.2 36.6667 20C36.6667 10.8 29.2 3.33331 20 3.33331ZM16.6667 28.3333L8.33337 20L10.6834 17.65L16.6667 23.6166L29.3167 10.9666L31.6667 13.3333L16.6667 28.3333Z" />
</svg>
</div>
<div className="px-4 py-2 -mx-3">
<div className="mx-3">
<span className="font-semibold text-emerald-500 dark:text-emerald-400">
完了
</span>
<p className="text-sm text-gray-600 dark:text-gray-200">
TodoアイテムとFormを作成する
</p>
</div>
</div>
</div>
);
};
export default TodoItem;
4.Componentを使う
index.tsxでTodoItemをインポートして表示してみます。
import TodoItem from "../components/Todo/TodoItem";
export default function Home() {
return (
<div>
<h1 className="text-xl font-bold text-green-400">Hello World</h1>
<TodoItem />
</div>
);
}
同じようにTodoを作成するためのFormをTaillwindのテンプレから作成しました。
const TodoItem = () => {
return (
<div className="w-80 p-1 overflow-hidden border border-solid border-gray-200 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div className="px-4 py-2 -mx-3">
<div className="mx-3">
<span className="font-semibold text-emerald-500 dark:text-emerald-400">
Success
</span>
<p className="text-sm text-gray-600 dark:text-gray-200">
Your account was registered!
</p>
</div>
</div>
</div>
);
};
export default TodoItem;
アプリのイメージを固めたいので、とにかくいい感じの見た目を作ってみます。
チートシートを見ながらレイアウトやデザインを整えます。
import TodoItem from "../components/Todo/TodoItem";
import TodoForm from "@/components/Todo/TodoForm";
export default function Home() {
return (
<>
<div className="w-100 flex justify-center">
<div className="my-5">
<h1 className="text-xl font-bold text-green-400">Hello World</h1>
<TodoItem />
<TodoItem />
<TodoItem />
<TodoForm />
</div>
</div>
</>
);
}
6.Componentにデータを渡す
このままではTODOの内容が全て同じ固定値なので与えられたデータに沿って表示を変更できるようにしたいです。
Componentに値を渡す時にその値はPropsという呼び方をするそうです。
セットアップ記事で作成されたButtonコンポーネントにお手本が書いてありました。
import styles from "./button.module.css";
type ButtonProp = {
children: string;
};
const Button = (props: ButtonProp): JSX.Element => {
return (
<button type="button" className={styles.red}>
{props.children}
</button>
);
};
export default Button;
Propsの型定義をしてその値をComponentで受け取る形となっています。
同じようにTodoItemでもPropsを定義しその値を受け取り表示してみます。
受け取り側(子)で型定義をしてPropsで利用方法を設定する
// propsとしてtitle,contentを受け付ける設定
type TodoItemProps = {
title: string;
content: string;
};
const TodoItem = (props: TodoItemProps) => {
return (
<div className="flex w-full border border-gray-300 max-w-sm overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800">
<div className="flex items-center justify-center w-12 bg-emerald-500">
<svg
className="w-6 h-6 text-white fill-current"
viewBox="0 0 40 40"
xmlns="http://www.w3.org/2000/svg">
<path d="M20 3.33331C10.8 3.33331 3.33337 10.8 3.33337 20C3.33337 29.2 10.8 36.6666 20 36.6666C29.2 36.6666 36.6667 29.2 36.6667 20C36.6667 10.8 29.2 3.33331 20 3.33331ZM16.6667 28.3333L8.33337 20L10.6834 17.65L16.6667 23.6166L29.3167 10.9666L31.6667 13.3333L16.6667 28.3333Z" />
</svg>
</div>
<div className="px-4 py-2 w-80">
<div className="mx-3">
<span className="font-semibold text-emerald-500 dark:text-emerald-400">
完了
</span>
<p className="me-1 mb-0 text-gray-700">{props.title}</p>
<span className="text-sm text-gray-600 dark:text-gray-200 me-1">
{props.content}
</span>
</div>
</div>
</div>
);
};
export default TodoItem;
渡す側(親)でとりあえず固定値のTodo型データを渡す
import TodoItem from "../components/Todo/todoItem";
import TodoForm from "@/components/Todo/todoForm";
const todoItem = {
title: "タイトル",
content: "TODO内容はここに記載します。",
};
export default function Home() {
return (
<>
<div className="w-100 flex justify-center">
<div className="my-5">
<h1 className="text-xl font-bold text-green-400">Hello World</h1>
<TodoItem {...todoItem} />
<TodoForm />
</div>
</div>
</>
);
}
<p>{props.title}</p>
<span>{props.content}</span>
また親側ではオブジェクトをSpread構文で展開して渡しています
// ↓この記述でtodoオブジェクトの中身を子のpropsに展開して渡しています
// (propsのtitleとcontentに分割して渡している)
<TodoItem {...todoItem} />
// 渡し方/受け取り方大全のサンプル
const info = { name: 'John', age: 25, gender: 'male'}
<MyComponent {...info} />
// ↑の書き方は↓と同義。
<MyComponent name={info.name} age={info.age} gender={info.gender} />
固定値で渡したデータがTodoItemで表示されました。やったー(^^)/
7.配列をループしてComponentを表示する
TodoItemコンポーネントをリストとして表示するTodoItemListコンポーネントを作成しました。
import TodoItem, { TodoItemProps } from "./todoItem";
// propsにdataという名前で受け皿を作っとく
type TodoItemListProps = {
data: Array<TodoItemProps>;
};
const TodoItemList = (props: TodoItemListProps) => {
return props.data.map((todoItem: TodoItemProps, i) => {
return <TodoItem key={i} {...todoItem} />;
});
};
export default TodoItemList;
このTodoItemListに配列としてTodoItemを渡したいです。
オブジェクトではないのでスプレッド構文を使わず、子コンポーネント側で設定したdataに対して渡そうと思います。
import TodoForm from "@/components/Todo/todoForm";
import TodoItemList from "@/components/Todo/TodoItemList";
import { TodoItemProps } from "@/components/Todo/todoItem";
// const todoItem = {
// title: "タイトル",
// content: "TODO内容はここに記載します。",
// };
const todoItemList: TodoItemProps[] = [
{
title: "タイトル",
content: "TODO内容はここに記載します。",
},
{
title: "タイトル2",
content: "TODO内容の二番目",
},
{
title: "タイトル3",
content: "TODO内容の3番目",
},
];
export default function Home() {
return (
<>
<div className="w-100 flex justify-center">
<div className="my-5">
<h1 className="text-xl font-bold text-green-400">Hello World</h1>
<TodoItemList data={todoItemList} />
<TodoForm />
</div>
</div>
</>
);
}
親側で渡されたprops.data配列を
<TodoItemList data={todoItemList} />
子側でループさせてTodoItemを描画しています
Vue.js(Vue2まで)でこれをやる際はv-forの記述を覚えないといけないですがReactではjs関数で実現できるようです。
return props.data.map((todoItem: TodoItemProps, i) => {
// React DOMをループする際はkeyにインデックスを渡さないといけない
return <TodoItem key={i} {...todoItem} />;
})
TodoItemListをどのコンポーネントが保持するかは
親がTodo配列を子に渡して、子はTodoを孫に渡し表示されました。やったー!(^^)
8.状態で見た目を変える
全てのTodoが完了になっているため、「完了」「実行中」「未対応」の3つの状態を設定して状態に応じて見た目を変更したいと思います。
React Iconの導入
状態に応じてTodoに表示するIconの色などを変更したいです。
現状SVGで扱いにくいのでReact Iconを導入してチェックアイコンを選びます。
導入方法についてこちらを参考にさせて頂きました。
npm install react-icons --save
React IconsのサイトからImportとJSXの記述を確認できます。
FontAwesome、Bootstrap Iconなどから選び放題で素晴らしいですが今回はチェックアイコンだけもらいます。
TodoItemに状態に関するprops.statusを設定し、状態に応じて見た目を変更する修正を行いました。
import { FaCheckCircle } from "react-icons/fa";
import { FaRegCheckCircle } from "react-icons/fa";
type Status = "Done" | "Progress" | "Incomplete";
export type TodoItemProps = {
title: string;
content: string;
status: Status;
};
const TodoItem = (props: TodoItemProps): JSX.Element => {
// 状態に応じてクラス名を取得する
let statusClassName = {
text: "",
textColor: "",
bgColor: "",
};
switch (props.status) {
case "Done":
statusClassName.text = "完了";
statusClassName.textColor = "text-emerald-500";
statusClassName.bgColor = "bg-emerald-500";
break;
case "Progress":
statusClassName.text = "実行中";
statusClassName.textColor = "text-blue-600";
statusClassName.bgColor = "bg-blue-600";
break;
case "Incomplete":
statusClassName.text = "未対応";
statusClassName.textColor = "text-gray-600";
statusClassName.bgColor = "bg-gray-600";
break;
}
return (
<div className="flex w-full border border-gray-300 max-w-sm overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800">
<div
className={`flex items-center justify-center w-12 ${statusClassName.bgColor}`}>
{props.status === "Done" ? (
<FaCheckCircle className="w-6 h-6 text-white fill-current" />
) : (
<></>
)}
</div>
<div className="px-4 py-2 w-80">
<div className="mx-3">
<span className={`font-semibold ${statusClassName.textColor}`}>
{statusClassName.text}
</span>
<p className="me-1 mb-0 text-gray-700">{props.title}</p>
<span className="text-sm text-gray-600 dark:text-gray-200 me-1">
{props.content}
</span>
</div>
</div>
</div>
);
};
export default TodoItem;
親側でもデータに状態を追記して子に渡してあげれば見た目が変わるはず…
import TodoForm from "@/components/Todo/todoForm";
import TodoItemList from "@/components/Todo/TodoItemList";
// const todoItem = {
// title: "タイトル",
// content: "TODO内容はここに記載します。",
// };
const todoItemList = [
{
title: "タイトル",
content: "TODO内容はここに記載します。",
status: "Done",
},
{
title: "タイトル2",
content: "TODO内容の二番目",
status: "Progress",
},
{
title: "タイトル3",
content: "TODO内容の3番目",
status: "Incomplete",
},
];
export default function Home() {
return (
<>
<div className="w-100 flex justify-center">
<div className="my-5">
<h1 className="text-xl font-bold text-green-400">Hello World</h1>
<TodoItemList data={todoItemList} />
<TodoForm />
</div>
</div>
</>
);
}
上記の修正についてポイントに感じた点をいくつか書き出します。
-
三項定理の利用
アイコン表示の部分では状態が"Done"の時はチェックアイコンを返しますが、それ以外には空になっています。
本当は「実行中」と「未対応」に応じた何かを返したかったのですが保留です。
{props.status === "Done" ? (
<FaCheckCircle className="w-6 h-6 text-white fill-current" />
) : (
<></>
)}
-
And演算子の利用
書いてて気づいたのですが上記の記述はFalseで何も返さない場合こちらの方がスマートです。
↓Reactで初めてこの書き方を見て&演算子の性質みたいなものを再確認できて勉強になりました。
{props.status === "Done" && (
<FaCheckCircle className="w-6 h-6 text-white fill-current" />
)}
-
テンプレートリテラルの利用
クラス名の部分で状態に応じて取得したクラス名を設定するのに利用しました。
場合によっては${ }の中で三項定理や&&、関数など使うと思います。
<span className={`font-semibold ${statusClassName.textColor}`}>
まとめ
長くなってきてしまったので一旦区切ります。
- Tailwindを導入できた
- ComponentにPropsを渡せた
- Componentの中でPropsを使用できた
- Componentをループを使って表示できた
- React Iconsを導入できた
非効率だったり推奨されていない書き方がありましたら教えて頂きたいです<(_ _)>