はじめに
Reactコンポーネントの基本的な使い方メモ
参考
Reactとは
- JavaScriptライブラリ
- JSXや最新JavaScriptの仕様などをViteなどビルドツールを使用してコンパイルする必要がある
React特徴
-
コンポーネントベース
- 機能単位でデザインとロジックがカプセル化されて、コンポーネントを作成しそれらを組み合わせることで複雑なUIを構成するという設計思想
-
仮想DOM
- ReactElementツリーとHTMLElementツリーが形成される
- Reactは状態が変更されるとコンポーネントが再レンダリングされて、新しいReactElementが作成され、それに伴い新しいHTMLElementが作成される
- 前のHTMLElementと新しいHTMLElementを比較して差分のあった箇所のみのHTMLElementが更新される
- つまり、DOM更新を最小限に抑えることができる
-
単方向データフロー
- データが親コンポーネントから子コンポーネントに対して一方向にproopsとして渡していく
- 一方向にする設計していることで、コードの複雑性を下げている
- 親コンポーネントのステートを更新する場合は、
コンポーネントの再レンダリングが発生するタイミング
- propsが更新された時
- stateが更新された時
- 親コンポーネントが再レンダリングされた時、子要素コンポーネントが再レンダリング。親コンポーネント以下のコンポーネントはすべて再レンダリングされる
- 参照しているcontextが更新された時
コンポーネントのライフサイクル
- Mountingフェーズ
- コンポーネントの初期化(state確保、useMemo()、useCallback()実行など)
- コンポーネントが初期化され、実行された出力が仮想DOMに出力 -> HTML更新
-
その後にuseEffect()が実行される。
useEffect()でstateなどが更新された場合は再レンダリングのためUpdateingフェーズに移行する
大事なのはまず初期値が表示 -> 副作用実施(useEffectなど) -> その中でstateなどが更新 -> updateingフェーズにいって再レンダリングされる。
useEffect()でstateを使うことは多いので、初期表示でも再レンダリングされいて、updatingフェーズが実行されている
- Updatingフェーズ
- 上記4つのケース(コンポーネントの再レンダリングが発生するタイミング)を検知して、コンポーネントが再実行される時
- 差分があればその部分のブラウザ表示を差し替える
- useEffect()などの依存配列が更新されていれば、再度実行される
- クリーンアップも実行される
- Unmountingフェーズ
- コンポーネントが仮想DOMから削除され、ブラウザ表示からも消える時
- クリーンアップ実行
クリーンアップされるタイミング
- コンポーネントがアンマウントされる時
- Updateingフェーズ時(再レンダリング)
Reactコンポーネントとは
propsを引数として受け取り、戻り値としてReactElementsを返す関数のこと
JSX(JavaScript XML)
JSX(JavaScript XML)は、Reactのコンポーネントを記述するための構文拡張です。これにより、JavaScriptの中でXMLまたはHTMLのようなタグ構文を使用して、UIのコンポーネントツリーを表現できます。
JavaScriptの拡張構文なため、変数に代入したり、オブジェクトのプロパティ値にしたり、関数の引数や戻り値にしたりできる
const element = <h1>Hello, world!</h1>;
props
- コンポーネント間でデータを渡すためのものです。propsは、親コンポーネントから子コンポーネントへとデータを渡す際に使用されます。
-
{属性名: 属性値}の形式のオブジェクト
として渡される。これがコンポーネントが関数で実装されている場合、propsのオブジェクトを第一引数として受け取る - 型はinterfaceではなく、型エイリアス(Type)で定義する。
- うっかり合成などしないようにするため
- propsの型をちゃんと定義することで、正確にマウントされるようにする
レンダープロップ(render Prop)
- 関数をプロップとして渡し、その関数をコンポーネント内で呼び出してUIを動的に構築するという方法
- 通常のコンポーネントは、props に値(数値、文字列、オブジェクトなど)を渡して制御しますが、レンダープロップでは関数を渡して、コンポーネントの内部ロジックを外部から制御できます
- ロジックの再利用: 状態管理や振る舞いをコンポーネントにカプセル化しつつ、UI部分はカスタマイズ可能
import { useState } from "react";
const Counter = ({ render }) => {
const [count, setCount] = useState(0);
return render(count, () => setCount(count + 1));
};
export default function App() {
return (
<Counter render={(count, increment) => (
<div>
<p>カウント: {count}</p>
<button onClick={increment}>増やす</button>
</div>
)} />
);
}
JSXは最終的にReactElementオブジェクト
の生成式になる
const element = <h1 className="greeting">Hello, world!</h1>;
Babel等によってコンパイル
const element = React.createElement(
'h1',
{ className: 'greeting' },
'Hello, world!'
);
JSXはReactElementオブジェクト=コンポーネントReactElementオブジェクト
Reactはコンポーネントのツリー構造で構成されている。それが仮想DOMと呼ばれる
イベント
イベントの伝搬を抑制
これを入れないと親要素のイベントハンドラが実行されてしまう
下位の要素で発生したイベントを上位の要素に伝搬するため
e.stopPropagation()
規定のイベントをキャンセル
e.preventDefault()
フォーム管理 イベントの発生タイミング
- change: フォーカスを外した時(=っ変更が確定した時)はonBlur属性で代用するしかない
スプレッド構文でのオブジェクトの更新
const obj = { name: "Alice", age: 25 };
// age の値を 30 に変更
const updatedObj = { ...obj, age: 30 };
コンポーネント開発
Suspence
- コンポーネントの描画待ちを検知する
- 待ちを検知して一時的にフォールバックコンテンツを表示させる
- 例えば、遅延ロードで、そのときに動的にインポートするときに使うとか
- LazyComponent の読み込みが 完了するまで Loading... が表示される
import React, { Suspense, lazy } from "react";
// 動的インポートでコンポーネントを遅延ロード
const LazyComponent = lazy(() => import("./LazyComponent"));
const App = () => {
return (
<div>
<h1>React.lazy + Suspense Example</h1>
{/* Suspense を使って遅延ロード中にフォールバックを表示 */}
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
</div>
);
};
export default App;
- 非同期処理の終了を待ち受ける
子要素から投げられたPromiseを補足すると、フォールバックが表示される
import React, { Suspense } from "react";
const fetchData = () => {
throw new Promise((resolve) =>
setTimeout(() => resolve("Hello from API!"), 2000)
);
};
const DataComponent = () => {
const data = fetchData(); // ここで `throw new Promise()` して `Suspense` に処理を委ねる
return <div>Data: {data}</div>;
};
const App = () => (
<Suspense fallback={<p>Loading...</p>}>
<DataComponent />
</Suspense>
);
export default App;
createPortal
- ReactDOM.createPortal を使うと、現在のコンポーネントの外にある 別の DOM ノードへ要素をレンダリングできます
- 例えば、モーダルやツールチップを body の直下に描画するのに便利
import React from "react";
import ReactDOM from "react-dom";
const Modal = ({ children }: { children: React.ReactNode }) => {
return ReactDOM.createPortal(
<div className="modal">{children}</div>,
document.getElementById("modal-root") as HTMLElement // `modal-root` に出力
);
};
const App = () => {
return (
<div>
<h1>React Portal Example</h1>
<Modal>
<p>This is a modal!</p>
</Modal>
</div>
);
};
export default App;
//別
<body>
<div id="root"></div>
<div id="modal-root"></div> <!-- ここにポータルの内容が描画される -->
</body>
Error Boundary
- 配下のコンポーネントで発生したエラーを捕捉、記録して代わりのUIを表示する
- コンポーネントツリーの中で throw された例外をキャッチして処理する仕組み
- エラーが発生する可能性がある領域を括り、fallback属性にエラー発生時のコンテンツを指定する
import React from "react";
import { ErrorBoundary } from "react-error-boundary";
const ErrorFallback = ({ error }: { error: Error }) => (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
);
const BuggyComponent = () => {
throw new Error("This component crashed!");
};
const App = () => (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<BuggyComponent />
</ErrorBoundary>
);
export default App;
あくまでレンダリング過程でのエラーを捕捉するもので、イベントハンドラー、非同期コードなどのエラーは補足の対象外
その場合はuseErrorBoundaryを使用する
import React from "react";
import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
const ErrorFallback = ({ error }: { error: Error }) => (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
);
const BuggyButton = () => {
const { showBoundary } = useErrorBoundary(); // `ErrorBoundary` にエラーを渡す関数
const handleClick = () => {
try {
throw new Error("Button click failed!");
} catch (error) {
if (error instanceof Error) {
showBoundary(error); // `ErrorBoundary` にエラーを渡す
}
}
};
return <button onClick={handleClick}>Click Me</button>;
};
const App = () => (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<BuggyButton />
</ErrorBoundary>
);
export default App;
React Query
isLoading や isError で状態管理が簡単
通常の useEffect + useState なら手動で管理する必要があるが
React Query なら isLoading, isError で簡単に判定できる
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
const fetchData = async () => {
const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/1");
return data;
};
const Component = () => {
const { data, isLoading, isError } = useQuery({ queryKey: ["todo"], queryFn: fetchData });
if (isLoading) return <p>Loading...</p>;
if (isError) return <p>Error fetching data</p>;
return <div>{JSON.stringify(data)}</div>;
};
export default Component;
React Query + Suspense + ErrorBoundary の連携
isLoading や isError を使わずに、エラーとローディングを 自動的に処理 できます
import React, { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
import axios from "axios";
const queryClient = new QueryClient();
const fetchData = async () => {
const { data } = await axios.get("https://jsonplaceholder.typicode.com/todos/1");
return data;
};
const DataComponent = () => {
const { data } = useQuery({ queryKey: ["todo"], queryFn: fetchData, suspense: true });
return <div>{JSON.stringify(data)}</div>;
};
const ErrorFallback = ({ error }: { error: Error }) => (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
);
const App = () => (
<QueryClientProvider client={queryClient}>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<p>Loading...</p>}>
<DataComponent />
</Suspense>
</ErrorBoundary>
</QueryClientProvider>
);
export default App;
Hooks
Hookとは
- コンポーネントから利用できる便利な関数
- Reactには様々な機能を持つフックがある
ルーティング
useNavigate
- ルーター経由でのページ移動を可能とする
- プログラムからのページ移動
const navigate = useNavigate();
const handleClick = () => navigate('/');
// 一つ前のページに戻る
navigate(-1)
パラメータの取得
// ルートパラメータの取得
const params = useParqms();
params.isbn
//または
params['isbn']
const { isbn } = useParams();
//クエリ情報受取
const [params, setParams] = useSearchParams(初期値)
params.get
params.getAll
params.has
//state属性を使った受け渡し
const {state: isbn = 'xxxxxxx'} = useLocation();
<NavLink to="/" state="xxxxxxxx">
基本的に、ページを区別するならアドレスにも反映させるべきなので、ルートパラメータを使用すべき
Outlet Context
- 親要素の値を子要素のルートで使用できる
useState()
コンポーネントに状態を持たせる
// state = count
const [count, setCount] = useState(stateの初期値);
- useStateは、ステートの初期値を引数として受け取り、ステートの現在の値と、そのステートを更新する関数をペアの配列として返します。この例では、それぞれcountとsetCountに分割代入されています。
- stateの値が更新されると、コンポーネントが再レンダリングされる
- stateのリフトアップ
- 親コンポーネントが自身の状態を変更する関数を子コンポーネントに渡して、フォーム操作で発火されるイベントへその関数を仕込んでおく。変更できる手続きが限定される=バグが減る、テストが書きやすい
- コンポーネントのレンダリングに直接関係しない値は基本的にstateにするべきではない
useEffect()
- コンポーネントに副作用を追加するフック。副作用を伴う処理はuseEfeect()に記述する
- 副作用とはAPI通信、非同期処理など
- (コンポーネントのstate を変化させ、出力を変えてしまう処理のこと)
- ブラウザーAPI,文書ツリーへのアクセスを伴う操作
基本的にReactの外側との連携時に利用する
これ以外の状況でuseEffectを利用したくなかったら、他の方法でだいたいできないかを検討する
useEffect(副作用処理、依存配列)
特別な理由がない限りは依存配列は常に明示すべき
- 依存配列ありの場合
- その変数に変更があれば実行される
- 依存配列なしの場合
- コンポーネントが再描画されるたびに実行される
- 空の配列を指定したとき
- コンポーネントの初期描画時に
1度だけ
実行される
- コンポーネントの初期描画時に
- 副作用関数の戻り値として関数を返すようにします。コンポーネントの破棄時にクリーンアップ関数を呼び出すようになる
クリーンアップ関数の呼び出されるタイミング
- updateフェーズ(再描画)とアンマウントフェーズ
useEffectの実行されるタイミング
- まずブラウザの初期値が表示される
- そのあとにuseEffect()が実行される
- useEffect()のなかでstateなどが更新されると、またページが再描画される(Updateフェーズ)
useLayoutEffect()
- useLaoyoutEffectが実行、次に画面が描画される
- ただ副作用処理が終わるまで画面になにも表示されなくなるので安易には使えない。他に方法がないときなどに使用するべき
useRef
基本的に要素へのフォーカス/スクロール、アニメーションの操作のようなごく限定された状況にとどめておくこと
- refオブジェクトを返す。これはコンポーネントが生成されてから破棄されるまで維持される、変更可能なオブジェクト
- 関数コンポーネントにインスタンス変数のようなしくみを組み込むことができる
- refオブジェクトを利用することでDomの参照やコンポーネント内で値を保持できる。値を保持できるのはuseStateと同じ
- useStateとは異なり、useRefで生成した値を更新してもコンポーネントは再レンダリングされない
- コンポーネント内で値を保持したいが、値を更新しても再レンダリングしたくない場合に利用する
const ref= useRef(0);
// currentプロパティが値を保持している
console.log(ref.current); // 0
//Domの参照
const ref = userRef(null);
<input ref={ref} type=text/> // ref.currentでDomを参照できる
メモ化
useMemo()
- 計算結果の値をメモ化
// 依存配列の値は、依存している値が更新されれば値が再計算されるような値
useMemo(関数,依存配列)
- 不要な再レンダリングを防ぐ。パフォーマンス最適化のために、必要なときにだけ計算してその結果をメモして再利用する。
- 依存配列が変わったら、下記の場合、maxCountが変化すると、getPrimesが再計算される
- 値をキャッシュして、使い回す。
// before
const primes = getPrimes(maxCount);
// after
const primes = useMemo(() => getPrimes(maxCount), [maxCount]);
memo()
-
コンポーネント(コンポーネントのレンダリング結果)
をメモ化する - 新しく渡されたPropsと前回のPropsを比較し、等価であれば再レンダリングせずにメモ化したコンポーネントを再利用する
- 親コンポーネントが再レンダリング(例えばstateの更新で)されても、メモ化された子コンポーネントは再レンダリングしない
- コールバック関数を渡されたコンポーネントは必ず再レンダリングされる。この場合、useCallBack()で渡すことで再レンダリングを防ぐことができる
export const ChildArea = memo( (props) => {
const {open} = props;
return (
<>
{open ? (<div><p>子コンポーネント</p></div>) : null }
</>
);
})
useCallback()
- useCallbackは関数をメモ化します
// 依存配列はコールバック関数が依存している配列
// 依存している値が更新されれば関数が再生成される
userCallback(コールバック関数,依存配列);
- 関数の再生成を避けることで、例えば子コンポーネントに渡す関数が不要に再生成されることを防ぐ場合に使用します。この場合渡す関数をuseCallback()でメモ化して子コンポーネントに渡す。
- その時のコンポーネントはmem()でコンポーネントをメモ化していないといけないみたい
useReducer
- stateとdispachを返す。コンポーネント内でstateの管理に用いられる
- useStateでは煩雑になるstate管理をuseReducerを利用する
- 使い方はRedeuxと似ている
- dispathは関数の同一性が保たれ、変化しないことが保証されている(再レンダリングされても同じということ)
Context/useContext
- Propsを利用せずに各コンポーネントに値を共有するReactの仕組み
- stateの値を共有して、参照するコンポーネントが全て反映されるなど
- Reduxとの違い
- ReduxはReducerで作業できるが、Contextはその機構がなく、やるなら自前で実装する必要がある
//Provider利用
const MyContext = createContext();
const value = {name: "aaa"}
<MyContext.Provider value={value}>
<Child/>
</MyContext.Provider>
// Contextオブジェクトから値を取得
// valueが利用できる
const myContext = useContext(Mycontext);
Reactでのグローバルな状態管理
Redux
Redux Tool Kit
Reduxを使いやすくしたもの
カスタムフック
- 自作のhook
- コンポーネントからロジック箇所をhookに切りだす。再利用、テストの容易さ、見通しがよくなる。
CSS
InlineStyle
export const InlineStyle = () => {
const containerStyle = {
border: "solid 2px #000",
//キャメルケース
borderRadius: "20px"
};
return (
<div style={containerStyle}>
<p> Inline Style </p>
</div>
)
}
CSSModules
- cssコンポーネントを用意して、cssを記述する
- 使う側でimportして利用する
import classes from "./CssModules.module.scss";
export const CssModules = () => {
return (
<div className={classes.containerStyle}>
<p> css module </p>
</div>
);
};
styled-jsx
import React from 'react';
function MyComponent() {
return (
<>
<div className="container">
Hello, styled-jsx!
</div>
<style jsx>{`
.container {
font-size: 20px;
color: blue;
padding: 10px;
border: 2px solid black;
}
`}
</style>
</>
);
}
styled-components
import React from 'react';
import styled from 'styled-components';
// スタイル付きのコンポーネントを作成
const StyledButton = styled.button`
background-color: blue;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
&:hover {
background-color: darkblue;
}
`;
function MyComponent() {
return (
<div>
<StyledButton>Click me!</StyledButton>
</div>
);
}