はじめに
フロントエンド開発において、コードの品質や開発効率を向上させるためには、適切なツールや手法を取り入れることが重要です。
業務で1年ほどReactを使っていく中で、書き方で戸惑った箇所や、あまり触れられないけど重要な部分の知見が溜まってきました。
本記事では、開発環境の構築からReactでの実践的なコード記述に役立つTipsを紹介します。
環境構築のポイント
保存時にESLint & Prettierを自動実行する
最近のフロントエンド開発において、コードの品質と可読性を維持し、開発効率を向上させるためにESLintとPrettierを導入するのが必須と思いますが、せっかく導入しても使われないと意味がありません。
vscodeで開発する際は、ワークスペース設定で保存時にESlintの自動補修を動かしてフォーマット実行までを設定できます。
VSCode の設定例(.vscode/settings.json)
// プロジェクト直下にvscodeディレクトリを作り、setting.jsonを追加する
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionOnSave": {
"source.fixAll.eslint": "always"
}
}
この設定をプロジェクトに含めることで、開発環境を統一し、手順書に個別の設定を記載する手間を省けます。
import時の相対パスを絶対パスにして階層をわかりやすくする
デフォルトではimportする際のパスが相対パスになり、可読性が低いです。
import Button from "../../components/Button"; // 相対パスでの指定
以下の設定をtsconfig.jsonに追加することで、絶対パスを使ったインポートが可能になります。
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["./sec/*"]
}
}
}
この設定を行うと、以下のようにimportされる
// 変更前は、自身から見た位置から開始する相対パス
- import Button from "../../components/Button";
// 変更後、@(./src)からの絶対パスになる
+ import Button from "@/components/Button";
絶対パスで表示されることで、コンポーネントの位置が一目で分かるようになり、プロジェクトが大規模になっても管理しやすくなります。
Reactで抑えるべきポイント
分割代入とスプレッド構文
分割代入とは、配列やオブジェクトから特定の値を簡単に取り出すための構文で、javaScriptのES2015から登場した構文です。
Reactではこれらを使って、状態管理やpropsの取り扱いをシンプルにしていて、よく見る箇所としてはuseStateでプロパティとset関数を取得する際に分割代入が用いらています。
配列の分割代入
const [count, setCount] = useState(0);
//使わない場合は省略することもできる
const [, setCount] = useState(0)
useStateの返り値は配列で、最初の要素が状態の値、2番目の要素がその値を更新する関数です。順番は変更できませんが、名前は自由に付けることができます。ただし、慣習的にセット関数にはsetXXXという名前を付けることが推奨されています。
オブジェクトの分割代入
一番この代入に遭遇する箇所として、コンポーネントに渡すpropsが例に挙げられる。
分割代入を用いない場合は以下のように書けます。
import React from "react";
interface Props {
title: string;
description?: string;
}
function SampleComponent (props: Props) => {
console.log(props);
// Object {title: "タイトル", description: "説明"}
return (
<div>
<h2>{props.title}</h2>
<p>{props.description}</p>
</div>
);
};
function SampleParentComponent () => {
return (
<div>
<SampleComponent title={タイトル} description={説明} />
</div>
);
};
コンポーネントに書いていたtitle,descriptionは1つのオブジェクトにまとめられます。
ここでは単にProps型のpropsオブジェクトに対してドット(.)を使ってプロパティを参照して取り出しています。
// 型などは上記と同じなので省略
function SampleComponent (props: Props) => {
const { title, description } = props; // このオブジェクトを反転させて書いてるように見えるのが分割代入
return (
<div>
<h2>{title}</h2>
<p>{description}</p>
</div>
);
};
分割代入を使う場合のメリットとしては、毎回props.***と記述する必要がなくなる。より重要なのは、代入時にデフォルト値を持たせることもでき、これは結構使うしる点が便利
const { title, description = "初期説明文" } = props;
また、props部分を引数内で直接、分割代入を使って取り出すことも可能です。
function SampleComponent ({ title, description }: Props) => {
console.log(title);
// タイトル
console.log(description);
// 説明
}
プロパティ数が多くなると見づらくなるが、少ない場合はこれが一番簡潔に見える
分割代入時に別名をつける
配列の分割代入時は取り出す際に名前を決めていたが、オブジェクトの分割代入で取り出した変数名を変更したい場合、オブジェクトのプロパティ名を変更して取り出すこともできます。
const user = { name: "Alice", age: 25 };
const { name: userName, age: userAge } = user;
console.log(userName); // Alice
console.log(userAge); // 25
小規模なプロジェクトでは気にするほどでも有りませんが、規模が大きくなるとnameなどの名称被りがあったりするのでそういう時に使えます。
基本的にはプロパティ名は省略して書くことがほとんどです。
//下二つは同じ意味になる
const { name: name, age: age } = user
const { name, age } = user;
これは変数名とプロパティキー名が同じ場合は省略してconst { name, age } = user;
と書けますが、本質的には同じ名前が付けられていることを覚えておくと混乱しないかもしれないです。
スプレッド構文を使った状態更新
スプレッド構文(...)はオブジェクトや配列のコピーを簡単に作成することのできる構文です。
スプレッド構文を使うことで、簡単に元のデータをコピーしつつ、一部だけ変更やマージができる。ReactではuseStateなどで値でなくオブジェクトや配列をセットする際、オブジェクトのプロパティを変更したり配列に値を追加しても、レンダリングが走らない。スプレッド構文はコピーを生成し、その際に新しいオブジェクトIDが振られるため最新のものでレンダリングすることができる。
以下はオブジェクトのプロパティを更新して再レンダリングをトリガーするサンプルで、handleClickは動かず、handleUpdateAgeは意図通りに動作する
import { useState } from "react";
const [user, setUser] = useState({ name: "Alice", age: 25 });
// このhandleClickは動かない
const handleClick = () => {
user.age = 30; // 直接オブジェクトを変更してもレンダリングされn
setUser(user); // 同じオブジェクトをセットするため、Reactが変更を検知しない
};
// このコードは動く
const handleUpdateAge = () => {
setUser((user) => ({ ...user, age: user.age + 1 }));
};
実務では単純な値のみセットすることは少なく、大抵の場合オブジェクトをセットする方が多いのでスプレッド構文は多用することになる
propsと少し異なる、childrenの使い方
React初見だと何が何なのかわからないものに、childrenがあります。
childrenはReactコンポーネント内に渡された要素を受け取る特別なpropsです。
例を挙げると、以下のようなコードがそれ。
const SampleComponent = ({children}) => {
return <div style={{ color: 'red' }}>{children}</div>
}
// 呼び出し元
<SampleComponent>
<p>ここに記述された内容がchildrenとなります。</p>
</SampleComponent>
SampleComponentの属性部分が空にも関わらず、どこからともなく現れるchildren。
タグの属性(style={{ color: 'red' }})とタグの内容(children)部分の違いがあります。
childrenはReactコンポーネントのタグ内に記述された内容を受け取る特別なpropsで、親コンポーネントが子コンポーネントをラップすると、その中に記述された要素は自動的にchildrenとして渡されます。
childrenはタグ内の要素をそのまま受け取るため、上記のようにコンポーネントの中身として表示されます。これにより、コンポーネントの柔軟性が高まります。
上のコードの場合、SampleComponentで何も囲んでいない場合、childrenがundefinedにとなりエラーになるので、以下のようにリファクタリングできる。
const SampleComponent = ({ children }: { children?: React.ReactNode }) => {
return <div>{children ?? <span>デフォルトのテキスト</span>}</div>;
};
<div style="color: red;">
<p>ここに記述された内容がchildrenとなります。</p>
</div>
React v18から暗黙的にpropsに含まれていたchildrenは、使用するなら明示的にpropsの型を定義しなければならなくなった。
type Props = {
children?: React.ReactNode
}
上のように毎回childrenを書くのが大変というときは、公式からPropsWithChildrenが提供されている
import { PropsWithChildren } from "react";
// PropsWithChildrenの中身はPにchildrenかundefindを取るプロパティを追加する
// type PropsWithChildren<P = unknown> = P & { children?: ReactNode | undefined };
type Props = {
label: string;
};
function Component1({ children, label }: PropsWithChildren<Props>) {
// ...
}
PropsWithChildrenは以下のように指定したのとほぼ同じになる
type Props = {
label: string,
children?: React.ReactNode
};
厳密にはなっているけど、果たして分かりやすいのか...?
useState, useEffectの次に覚えておきたいuseProvider
例を挙げると、ユーザー情報をどのコンポーネントからでもカスタムフックで受け取りたい場合、最上位のコンポーネントあたりで以下のようなコンポーネントで囲んでおくことになると思います。
function UserInfoProvider(props: {
userInfo:UserInfo; children: ReactNode
}) {
return (
<UserInfoContext.Provider value={}>
{props.children}
</UserInfoContext.Provider>
);
}
例えばユーザー情報をグローバルに取得したい場合、自作のproviderを作りたいというユースケースや、ライブラリ側で用意しているproviderを使ってアプリのコンポーネントを囲んでおくことを指示される場合がある。
Providerを使うメリットと実装手順
Provider はライブラリの設定やアプリのグローバル状態管理に必須となるため、実務で避けて通れません。実装には以下の手順が必要です。
- Context の作成 (createContext)
- Provider の作成 (useState で状態管理し Context.Provider で提供)
- useContext をラップしたカスタムフック を作る(推奨)
- アプリ全体をProviderでラップ
- 子コンポーネントでuseContextを使い値を取得
複雑ですが、使えるようになると格段に便利になります
他にも以下のような要望はproviderを導入して解決することになります。
- 認証情報の管理(ログインユーザーの状態の保持)
- 言語設定の共有(i18n)
- グローバルな状態管理や、深い階層にあるコンポーネントへのデータ共有
高階関数とコンポーネントの動的生成
mapやfilterに代表される高階関数もReactで多用される。スプレッド構文と同じく、コールバック関数を各要素に適用後、新しい配列を返す特性がReactのレンダリングをトリガーする箇所と噛み合う
// mapでリストを生成するサンプル
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React", completed: false },
{ id: 2, text: "Learn TypeScript", completed: false }
]);
const toggleCompleted = (id: number) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<span style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
{todo.text}
</span>
<button onClick={() => toggleCompleted(todo.id)}>Toggle</button>
</li>
))}
</ul>
);
}
型の指定とユーティリティタイプ
TypeScriptのユーティリティタイプは、型の操作を簡単にするための組み込み型です。特定のプロパティを除外したり、必須化したり、部分型を作成したりすることができます。
最初はジェネリック型やユーティリティタイプの使いどころが分かりませんでしたが、実務でライブラリのコンポーネントを拡張し汎用化する際によく使うと実感し、重要性を理解しました。
例えば、TypeScriptのユーティリティタイプの1つであるOmitがあります。
Omitは2つの引数を取り、第一引数にはオブジェクトの型を渡して、第二引数には第一引数のプロパティを渡します。
Omit を使うと、特定のプロパティを除外した新しい型を作成できます。
たとえば、フォームのフィールド情報を型定義し、その一部をコンポーネント内で固定したい場合に活用できます。
type Field = {
name: string;
remarks?: string;
};
この Field 型を使って、フォームフィールドを表すコンポーネントを作ります。
通常、nameプロパティを必須にすることで、正しく渡されていない場合はエラーになります。
function FieldComponent(props: Field) {
return <div>{props.name}: {props.remarks ?? "備考"}</div>;
}
// 使用例
<FieldComponent remarks="親から渡される備考" /> // ❌ name がないのでエラー
今回は、nameをコンポーネント内部で固定して定義しておき、FieldComponentを複数箇所で使いまわしたいという要件でした。
✅ Omit を使って name を除外し、コンポーネント内で固定
Omit を使うことで、name を外部から渡せないようにし、コンポーネント内で固定できます。
function FieldComponent(props: Omit<Field, "name">) {
const { remarks = "備考" } = props;
return <div>somethingField: {remarks}</div>;
}
// 使用例
<FieldComponent /> // ✅ エラーにならず、"somethingField: 備考" を表示
<FieldComponent remarks="Custom remark" /> // ✅ "somethingField: Custom remark" を表示
この時、Omit<Field, label>は
{
remarks?: string
}
と同義になる
まとめ
今回紹介したTipsやテクニックを実践することで、フロントエンド開発の効率が大幅に向上します。特に、Reactのコンポーネント設計や状態管理を簡潔かつ直感的に行えるようになることで、より洗練されたコードを書くことができます。
これらの基本を抑えた上で、実務に活かすことで、より良いプロダクト開発に貢献できるはずです。