この記事について
この記事では自分が React を勉強したことをざっくりまとめる。
今後自分が参考にするために残すのであまりまとまっていないと思うし、足りてない部分も多くあると思う。
新しいことを勉強したら逐一足していく。
基本的には O’reilly の React ハンズオン と reactpractice.dev から学んだことを残す。前者はインプット、後者はアウトプットとして使用した。
React is なに
Javascript のライブラリ。JSX 1 と組み合わせることで UI を宣言的に記述でき、コンポーネントを簡単に作れる。
あと State 管理などインタラクティブなサイトを作成するのに便利な関数が用意されている。
React の基本
基本は以下のような感じ
import React from 'react'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<App />
)
ReactDOM は React 要素を DOM 要素に変換する。
これによりブラウザ上でレンダリングできるようになる。
createRoot はルートのノードを作ることを表している。そしてレンダリングに render() 関数を使用している。 ただのノードを作るなら createElement が使える。
例えば以下のように記載すると <h1>Baked Salmon</h1> と表現するのと同じ。
const dish = React.createElement("h1", null, "Baked Salmon")
ただあまりこのようにしてノードを作ることはあまりなく、基本的に JSX で記述する
コンポーネント
コンポーネントの実体は関数。返り値として要素を返す。
↓イメージ
function App({items}) (
return React.createElement(
items.map((ingredient, i) => React.createElement("li", {key: i}, ingredient))
)
)
ただ先述の通り基本的に JSX を使う。JSX で記述する際は文字リテラルまたは Javascript を渡す必要がる。JSX 内に Javascript を記述するときは {} で囲む必要がある。
以下では JSX の中に Javascript 式を記載し、さらにその中に JSX を記載している。
<ul>
{props.items.map((ingredient, i) => (
<li key="{i}">{ingredient}</li>
))}
<ul>
また、コンポーネントに値を渡す場合は以下のように記載する。
<App key={i} name={name}>
関数も値の一つなので関数も渡すことができる
<App onDoubleClick={e => alert("double click")}>
受け取るときは props 経由で受け取る。この props には渡した key, name などのほかにも children などの値も入っている。
他の値は基本必要ないので受け取るときはデストラクチャリングを使用して以下のように受け取る。デクストラクチャリングせずに props.name とかでも受け取れる。
function App({key, name}) {
...
}
また、返り値は一つの要素しか返せないので複数返したいときはフラグメントを使う。
function App() {
retunr (
<>
<h1>Hello World</h1>
<p>Hi!</p>
</>
)
}
また、React コンポーネントに CSS を適用するには style プロパティを使う。
export default function App() {
return <Component style={{ backgroundColor: "lightblue"}} />;
}
ステート管理
useState
コンポーネントの描画後に変更されるデータをステートと呼ぶ。インタラクティブなコンポーネントを作成するのに必要不可欠。
例:
import { useState } from "react";
export default function App() {
const [state, setState] = useState(0)
...
}
useState はステートの初期値を受け取り、配列を返す。
一つ目は、ステートの現在値、二つ目はステートを変更する関数。この関数を使ってステートを変更した場合、フックされてそのコンポーネントが再描画される。
このようなステートは複数箇所で宣言されるとデバッグが難しくなることから、最上位のルートコンポーネントで一括で管理することが効率的。子コンポーネントにはプロパティとしてステートやステートを変更する関数を渡せば良い。
また、ステートを持たない関数を純粋関数という。
useRef
useRef は値の参照を保持する。例えばコンポーネントの参照を持っていればアンマウント 2 されてもそのコンポーネントを変更できる。(保持し続けてしまうのでサイトから離れたら解放しないといけない)
あとはフォームの入力などで使える。
import React, { useRef } from "react";
export default function AddColorForm( { onNewColor = f => f }) {
const txtTitle = useRef();
const hexColor = useRef();
const submit = e => {
e.preventDefault();
const title = txtTitle.current.value;
const color = color.current.value;
onNewColor(title, color);
txtTitle.current.value;
hexColor.current.value;
}
return (
<form onSubmit={submit}>
<input ref={txtTitle} type="text" />
<input ref={hexColor} type="color" />
<button>ADD</button>
</form>
)
}
参照先の値を ref オブジェクトの current フィールドでアクセスできるようになる。
ただこのようなやり方は DOM の値を直接書き換えており、宣言型プログラミングのパターンに反している。このようにアクセスするコンポーネントを制御されていないコンポーネント (uncontrolled component) と呼ぶ
できる限り制御されたコンポーネントを使用することが推奨されている。
制御されたコンポーネントに書き換えると以下のようになる。
import React, { useState } from "react";
export default function AddColorForm( { onNewColor = f => f }) {
const [title, setTitle] = useState("");
const [color, setColor] = useState("#000000");
const submit = e => {
e.preventDefault();
onNewColor(title, color);
setTitle("");
setColor("");
}
return (
<form onSubmit={submit}>
<input value={title} onChange={e => setTitle(e.target.value)} type="text" />
<input value={color} onChange={e => setColor(e.target.value)} type="color" />
<button>ADD</button>
</form>
)
}
onChange プロパティにイベントハンドラを設定している。イベントハンドラは event オブジェクトを引数にとり、target フィールドは DOM への参照になるので event.target.value で値が取れる。
カスタムフック
フックは React コンポーネントの内部で使用されることを想定しているので、カスタムフックの内部で別のフック (以下だと useState) を呼び出すとカスタムフックの呼び出し元のコンポーネントが再描画される。
import { useState } from "react";
export const useInput = initialValue => {
const [value, setValue] = useState(initialValue);
return [
{ value, onchange: e => setValue(e.target.value) },
() => setValue(initialValue)
];
};
コードの可読性のためにもこのようなカスタムフックを積極的に使うことが推奨されている。
コンテキスト
アプリケーションの規模が大きくなり、コンポーネントの数が増えるとステートの管理が大変になる。特にステートを最上位のコンポーネントから下位のコンポーネントに渡す時、中間のコンポーネントにもプロパティとして渡さなくてはならない。
そうなると、可読性も下がるし、管理が大変なことになる。
そこで登場するのがコンテキスト。
コンテキストにデータを渡し、データを使うときはコンテキストから取り出せば良い。データを渡すのをコンテキストプロバイダー、データを受け取るのをコンテキストコンシューマーと呼ばれる。
import { render } from "react-dom";
import { createContext } from "react";
import data from "./data.json"
import App from "./App";
export const TestContext = createContext();
render(
<TestContext.Provider value={{ data }} >
<App />
</TestContext.Provider>
document.getElementById("root")
)
createContext 関数でコンテキストを作成し、Provider コンポーネントでステートを使用するコンポーネントを囲む。
こうすることで App 配下のコンポーネントはステート: data を使うことができる。
データの取得は以下のように行う。
import { useContext } from "react";
import { TestContext } from "./";
export default function Test() {
const { data } = useContext(TestContext);
...
}
useContext 関数を作成したコンテキストを引数に実行してステートを取得する
以下のようにプロバイダをカスタムすることもできる。
export const TestContext = createContext();
export default function TestProvider ({ children }) {
const [data, setData] = useState(somedata);
const changeData = item => setData({...data, item})
return (
<TestContext.Provider value={{ data, changeData }}>
{children}
</TestContext.Provider>
)
}
フック
useState, useRef については説明したけど他にもフックがあるので紹介
useEffect
コンポーネントが描画された後に何か処理を行いたい時に使う。
つまり描画とは関係ないような副次的な処理を記載する。
import {useState, useEffect} from "react";
function App() {
const [checked, setChecked] = useState(false)
useEffect(() => {
alert(`checked: $(checked.toString())`);
}, [checked])
return (
...
)
}
useEffect の2番目の引数には配列を渡すことができ、この配列の中の変数が変更された時のみ useEffect 内の関数を実行できる。 この配列は依存配列と呼ばれる。
配列を [] にすると、初回のみ実行される。配列自体を渡さないと毎回実行される。
また、useEffect 関数内で return することもできるが、これはコンポーネントがアンマウント 2 された時に実行される。クリーンアップ処理として使用される。
useMemo
メモ化された値を取得することができる。メモ化とはキャッシュのようなもの。ある引数の計算結果をメモ化しておき、同じ引数で呼び出されたらそのキャッシュを返す。
import { useMemo } from "react"
function WordCount({ children = ""}) {
const words = useMemo(() => children.split(" "), [children])
...
}
useMemo は useEffect と同様に第二引数に依存配列を指定できる。 上の例だと children の値が変われば第一引数の関数が実行されるが、変わらなければキャッシュされた値が返される。
useCallback
useCallback はメモ化された関数を返す。
const fn = useCallback(() => {
console.log("hello world")
}, [])
useCallback を使わない場合、描画の度に毎回新しいインスタンスが生成され、異なる関数を生成してしまう。
メモ化することで依存配列のデータが変わらない限り同じ関数として使用することができる。
useLayoutEffect
ブラウザのペイント処理(描画結果がピクセルとして画面に表示される処理)よりも前に処理を行いたいときに使う。
例えばブラウザウィンドウのサイズをもとにコンポーネントのサイズを動的に変えるカスタムフックに使用できる。
function useWindowSize() {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const resize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
};
useLayoutEffect(() => {
window.addEventListener("resize", resize);
resize();
return () => window.removeEventListener("resize", resize);
}, []);
return [width, height];
}
useReducer
useState に reducer を組み合わせようなもの。reducer とは現在のステート値を受け取って新しいステート値を返すもの。
function Numbers() {
const [number, setNumber] = useReducer(
(number, newNumber) => number + newNumber,
0
);
return <h1 onClick={() => setNumber(1)}>{number}</h1>;
}
第一引数に reducer の関数、第二引数に初期値をとる。
reducer は、同じ引数で呼び出された場合、必ず同じ戻り値を返さなければならない点に注意。
これは複雑なステートを管理する時に役にたつ。
以下のような複雑なデータをステート管理する時を考える。
const firstUser = {
id: "0391-3233-3201",
firstName: "Bill",
lastName: "Wilson",
city: "Missoula",
state: "Montana",
email: "bwilson@mtnwilsons.com",
admin: false
};
これを useState で管理しようとすると、データを書き換えるとき、以下のようにスプレット構文を使わなければならない。
setUser({ ...user, admin: true });
これを useReducer で管理することでスプレット構文を使用しなくてもデータを書き換えることができる。
function User() {
const [user, setUser] = useReducer(
(user, newDetails) => ({ ...user, ...newDetails }),
firstUser
);
...
setUser({ admin: true });
}
データの取得
取得
react に限らず javascript では fetch で外部のデータにアクセスすることが多い
fetch(`https://api.github.com/users/${login}`)
.then(response => response.json())
.catch(console.error);
ただ、axios では Node.js でも使用できるのでこちらの方が使われるらしい
データの保存
window.localStorage もしくは window.sessionStorage を使用する。 sessionStorage はページを閉じると消え、localStorage は明示的に削除されるまで残り続ける。
const saveJSON = (key, data) =>
localStorage.setItem(key, JSON.stringify(data));
const loadJSON = key =>
key && JSON.parse(localStorage.getItem(key));
非同期リクエストの管理
非同期リクエストの場合、「保留中」、「成功」、「失敗」の状態がある。
それぞれの状態を管理することが UX の向上につながる。
例えば以下のように State で状態を管理することも挙げられる。
const [data, setData] = useState();
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!login) return;
setLoading(true);
fetch(`https://api.github.com/users/${login}`)
.then(data => data.json())
.then(setData)
.then(() => setLoading(false))
.catch(setError);
}, [login]);
if (error)
return <pre>{JSON.stringify(error, null, 2)}</pre>;
if (loading) return <h1>loading...</h1>;
if (!data) return null;
このような状態管理を行ってくれる React Query もある。
または自分でカスタムフックを作成してもよい
import React, { useState, useEffect } from "react";
export function useFetch(uri) {
const [data, setData] = useState();
const [error, setError] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!uri) return;
fetch(uri)
.then(data => data.json())
.then(setData)
.then(() => setLoading(false))
.catch(setError);
}, [uri]);
return {
loading,
data,
error
};
}
さらに、このカスタムフックを使った再利用可能なコンポーネントも作成できる。 各状態に合わせて、レンダープロップを渡し、表示させることができる。
function Fetch({
uri,
renderSuccess,
loadingFallback = <p>loading...</p>,
renderError = error => (
<pre>{JSON.stringify(error, null, 2)}</pre>
)
}) {
const { loading, data, error } = useFetch(uri);
if (error) return renderError(error);
if (loading) return loadingFallback;
if (data) return renderSuccess({ data });
}
これは後述の Suspense に近い考え方。
仮想リスト
何も考慮しないと画面に映らない部分も描画しようとしてしまうのでパフォーマンスが劣化してしまう。
仮想リストを使用することでユーザーのスクロールに合わせてコンポーネントをマウント、アンマウントする。
react では react-window, react-virtualized ライブラリがある。react native だと FlatList。
例:
import React from "react";
import { FixedSizeList } from "react-window";
import faker from "faker";
const bigList = [...Array(5000)].map(() => ({
name: faker.name.findName(),
email: faker.internet.email(),
avatar: faker.internet.avatar()
}));
export default function App() {
const renderRow = ({ index, style }) => (
<div style={{ ...style, ...{ display: "flex" } }}>
<img
src={bigList[index].avatar}
alt={bigList[index].name}
width={50}
/>
<p>
{bigList[index].name} - {bigList[index].email}
</p>
</div>
);
return (
<FixedSizeList
height={window.innerHeight}
width={window.innerWidth - 20}
itemCount={bigList.length}
itemSize={50}
>
{renderRow}
</FixedSizeList>
);
}
サスペンス
Suspense
非同期な処理をするコンポーネントのロード中にレンダリングするコンポーネントを記載できる。
例:
import React, { Suspense, lazy } from 'react';
const MyLazyComponent = lazy(() => import('./MyLazyComponent'));
function App() {
return (
<div>
<h1>My Application</h1>
<Suspense fallback={<div>Loading component...</div>}>
<MyLazyComponent />
</Suspense>
</div>
);
}
なお、lazy はそのコンポーネントが初回描画されるまでダウンロードを遅らせるもの。
また、React アプリケーションの中で Promise を throw する場合は Suspense コンポーネントを記述する必要がある。
このように Promise を throw して、Suspense コンポーネントと強調するために設計された関数をサスペンスデータソースという。
const loadStatus = (function() {
let error, response;
const promise = new Promise(resolves =>
setTimeout(resolves, 3000)
)
.then(() => (response = "success"))
.catch(e => (error = e));
return function() {
if (error) throw error;
if (response) return response;
throw pending;
};
})();
Error Boundary
非同期処理がエラーを起こした際、画面全体に影響が出てしまう。
この Error Boundary を使用することで発生したエラーはそのコンポーネント内に留めることができる。
ErrorBoundary は React が提供しているわけではないので自分で作成しなければならないが、公式ドキュメントに記載例がある。
これを実装し、以下のようにコンポーネントをラップすると Error Boundary ができる。
<ErrorBoundary fallback={<p>Something went wrong</p>}>
<Suspense fallback={<div>Loading component...</div>}>
<MyLazyComponent />
</Suspense>
</ErrorBoundary>
ルーティング
Route
ページのルーティングには react-router, react-router-dom というルーティングライブラリを使用する。
ルーティングを使用するにはコンポーネントツリーのルートに近い場所で Router コンポーネントでラップする。
Router は現在ユーザーが閲覧しているページの位置を子コンポーネントに伝える。
import React from "react";
import { render } from "react-dom";
import App from "./App";
import { BrowserRouter as Router } from "react-router-dom";
render(
<Router>
<App />
</Router>,
document.getElementById("root")
);
App 内では、Route コンポーネントで window.location の値が変わった場合にどのコンポーネントを描画するかを決定できる。
import React from "react";
import { Routes, Route } from "react-router-dom";
import {
Home,
About,
Events,
Products,
Contact
} from "./pages";
function App() {
return (
<div>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/about"
element={<About />}
/>
<Route
path="/events"
element={<Events />}
/>
<Route
path="/products"
element={<Products />}
/>
<Route
path="/contact"
element={<Contact />}
/>
</Routes>
</div>
);
}
その他のルーティング機能はドキュメント参照
サーバーサイド React
描写を全てクライアントサイドで行うとスピードや SEO の観点で問題が出る。
そこで初回の描写をサーバーサイドで行うこと (サーバーサイドレンダリング) でこれらの問題を解決できる。
これを**アイソモーフィック (isomorphic) **な Web アプリという。
混同されやすい概念としてユニバーサルがある。これはコードを書き換えることなく複数のプラットフォーム (ブラウザや Node.js環境など) で実行可能なアプリケーション。
基本的な概念の用語を以下にまとめる。
CSR (client-side rendering)
ブラウザでアプリケーションをDOMとして描画すること。
SSR (server-side rendering)
サーバーでアプリケーションを静的なHTMLとして描画すること。CSR と SSR の両方に対応している場合、アイソモーフィックなアプリケーションと呼ぶ
ハイドレーション (Hydration)
サーバーで描画された HTML をブラウザで再利用すること。アイソモーフィックなアプリケーションでは、SSR により生成された静的な HTML がブラウザでロードされてDOM ツリーが構築されると、以降は CSR によりアプリケーションが描画される。
プリレンダリング (Prerendering)
アプリケーションをビルド時に静的なHTMLに変換すること。静的なコンテンツとしてデプロイされるので、CDN によりキャッシュされ、非常に高速にロードされる。
SSR として使用されるフレームワークの代表例としては Express や Next.js がある。それぞれは以下を参照。
その他ツール
vite
Vite は、JSX, TSX コードをバンドルして、ブラウザが読める HTML, CSS, JS として提供するビルドツール。
また、ホットリロード、開発サーバーを実行するためのスクリプト、テスト用のスクリプトを追加するための設定など、React コードを実際にブラウザで実行するために必要な設定など、面倒な作業を行ってくれる。
裏で rollup といったビルドツールが動いてたりする。(この辺は勉強中)
コンポーネント作成に便利そうなツール
dnd kit
コンポーネントをドラッグ、ドロップできる。
HeadlessUI
ドロップダウンやポップオーバーなど一般的に使うコンポーネントを tailwind CSS とともに使用できる。
Floating UI
コンポーネントの Floating が得意なライブラリ。