Vueユーザーが真剣にReactにチャレンジする学習メモ
概要
Vueを普段使っているのですが、いい加減Reactをちゃんと勉強しておかなければ…という気持ちになりチャレンジしていました。Vueユーザ目線でのReact学習録です。
分量がある程度増えたら記事にしたいかも。
ターゲット
- Vue 3 Composition APIを普段使っている(もしくはある程度把握している)
- React Hooksを使ったReactのコーディングに興味がある
- Typescriptを使う
対象外
- Hooks以前のReact(そもそも知らないので)
- Next.js関連
[1]
Reactとは?ReactはUI構築のためのJavascriptライブラリ。Meta(旧Facebook)が中心となり開発している。
- 宣言的(Declarative)View によって、予測可能なコードでシンプル、デバッグが行いやすい。
- コンポーネントベース(Component-Based) であり、カプセル化されたコンポーネントでステートを管理しているため、複雑なUIを作ることが容易である
- Learn once, Write Anywhere であり、Reactのコードを様々な技術スタックで使用可能(React Nativeなど)。
ReactはSimple、VueはEasyという言説をよく見かけるが、これがどこから言われ始めたのか、調べても発見できなかった…
JSXの概要
Vueの .vue
ファイルのように、Reactでは .jsx
を使用する。
Typescriptの場合は .tsx
。
JSXは Javascript XML
の略で、React用のJavascript拡張構文。
HTMLとJavascriptのハイブリッドのような形式になっている。
.vue
との大きな違い
根本的に違う点として、.vue
はVueコンポーネントを記述するが、.jsx
はJavascriptの拡張としてHTMLを取り扱う、という点。HTML関連の要素は関数の返り値として上位コンポーネントに渡す。
function App() {
return (
<>
<p>hello :)</p>
</>
);
}
最上位ではReactDOM.createRoot().render()
で実際にDOMをrenderする。
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
JSXの基本の書き方
React v16.8 以降の React Hooks に基づく。
Hook時代のReactはそれ以前と異なり、クラスを作らず関数ベースでコンポーネントを取り扱う。
(オブジェクト指向ベースではなく、関数ベース風のスタイルに変更された[1])
コンポーネント名
PascalCaseで命名する。
Vueと異なり、ケバブケースは容認されていない(ファイル名ではなく、functionの名前として命名するため当然ではあるが…)
- React:
function Sample() { ... }
(関数名) - Vue:
Sample.vue
(ファイル名)
HTMLの記述
Reactは 関数の返り値 として渡す。
function Sample() {
return (
<p>hoge fuga</p>
);
}
Vueは <template>内に記述するので、この時点でだいぶ根幹から違う。
<template>
<p>foo bar</p>
</template>
CSSの記述
ReactのJSXはあくまでJavascript拡張構文なので、標準でCSSを書くような機能は存在しない。
幾つかソリューションは存在する[2][3][4]ようだが、どれを使うのかはまちまちのように見える。
1. CSS Modules (CSS)
恐らく最もメジャー?
foo.module.css
にスタイルを記述して、コンポーネント内でそのcssをimportする方式。
-
Good👍
- 普通のCSSを書ける
- コンポーネント単位でローカルスコープになる
- Reactでよく使われるフレームワーク、Next.jsで標準サポートされている
-
Bad😡
- 近い将来に廃止予定であるとアナウンスされている[5]
- コンポーネントの数だけ
module.css
が作られていくので、ファイル数が膨大になる
サンプルコード
import styles from "./Sample.module.css";
export const Sample = () => {
return (
<>
<p className={styles.sample}>foo bar</p>
</>
);
};
.sample {
font-weight: bold;
}
2. styled-components (CSS-in-JS)
スタイル付きのコンポーネントを生成し、それを使用してHTMLを組んでいく。
styled-components
のインストールが必要。
-
Good👍
-
styled-component
でCSSの要素が定義されているので、IDEで補完が効く
-
-
Bad😡
-
return()
節のHTMLを見てもパッと見で<h1>
なのか<p>
なのかといったHTMLタグがさっぱりわからなくなり、別コンポーネントを参照しているかどうかも判別できなくなる
-
サンプルコード
import { styled } from "styled-components";
export const Sample = () => {
const SampleStyle = styled.p`
font-weight: bold;
`;
return (
<>
<SampleStyle>foo bar</SampleStyle>
</>
);
};
3. vanilla-extract (CSS-in-TS...?)
エンジニア仲間の方に教えてもらったライブラリ。
styled-component
と思想が異なり、スタイルそのものを取り扱う。
CSS+Typescriptの考え方が根幹にあり、テーマの作成やCSSの取り扱いが型安全に行える。
@vanilla-extract/css
のインストールが必要。
-
Good👍
- Typescriptフレンドリーで、型がちゃんとあるので補完も効く
- JSX側はスタイルを当てるだけなので、ごちゃごちゃにならない
-
Bad😡
-
<filename>.css.ts
ファイルに書かなければならないルールがあり、JSXの中でstyleを定義することはできないため、ファイル数は増える
-
サンプルコード
import { sampleStyle } from "./Sample.css.ts";
export const Sample = () => {
return (
<>
<p className={sampleStyle}>foo bar</p>
</>
);
};
import { style } from "@vanilla-extract/css";
export const sampleStyle = style({
fontWeight: "bold",
});
その他
- CSSフレームワークとして、 Tailwind CSSやUnoCSS を使う
- この辺りはあまりVueと大差がない
- 普通に
<head>
でCSSを読み込ませる- ❌何のためにコンポーネント化したのか分からないので、やりたくはない
- Kuma UI が少し気になる
Vueの場合
普通にCSS-in-JSができるため、特に考える必要はない
<style scoped>
</style>
Javascript
Reactの場合、そもそもJSXがJavascriptの拡張構文なので、ただJSをそのまま書くだけ。
export const Sample = () => {
const message = "foo bar"
const test = () => {
console.log("baz")
}
return (
<>
<p onClick={test}>{message}</p>
</>
);
};
Vueは <script>
節の中に書く。
<script setup lang="ts">
</script>
他コンポーネントの呼び出し
ReactもVueも同じ。importしてHTMLタグのようにして展開。
import { Sample } from "./Sample.tsx";
function App() {
return (
<>
<Sample />
</>
);
}
<script setup lang="ts">
import Sample from "./Sample.vue";
</script>
<template>
<Sample />
</template>
Typescript関係
この辺からTypescript周りも確認。
ReactのHTMLをreturnする関数の返り値は React.FC
(React.FunctionComponent) 。
import { FC } from "react";
export const Sample: FC = () => {
return (
<>
</>
);
};
変数をHTML内で展開
書くの忘れてた。
Reactは {foo}
で展開する。
export const Sample: FC = () => {
const hoge = "fuga"
return (
<>
<p>{hoge}</p>
</>
);
};
Vueは {{foo}}
で展開する。うっかり癖で二重にしそう。
<template>
<p>{{hoge}}</p>
</template>
Props (変数渡し)
Vueとさほど変わらない。
import { FC } from "react";
type Props = {
message: string;
};
export const Sample: FC<Props> = ({ message }) => {
return (
<>
<p>{message}</p>
</>
);
};
import { Sample } from "./Components/Sample.tsx";
function App() {
const text = "hi!"
return (
<>
<Sample message={text}/>
</>
);
}
Vueの場合はこれ。
:message
で v-bind
で渡す。
<script setup lang="ts">
const props = defineProps<{
message: string
}>();
</script>
<template>
<p>{{props.message}}</p>
</template>
<script setup lang="ts">
import Sample from "./components/Sample.vue";
const msg = "hi!"
</script>
<template>
<Sample :message="msg" />
</template>
Props (関数渡し・引数なし)+ onClickの実装
例えばクリック時にアラートを出すような実装を行うなら、以下のように書く。
律儀に返り値のMouseEventも引っかけてみます。
ReactのonClickで返ってくる MouseEvent
は、 Javascript標準のMouseEvent
ではなく、React.MouseEvent
なので型定義の際には注意が必要かも。
import { FC, MouseEventHandler } from "react";
type Props = {
onClickButton: MouseEventHandler<HTMLButtonElement>;
};
export const Sample: FC<Props> = ({ onClickButton }) => {
return (
<>
<button onClick={onClickButton}>Click Event</button>
</>
);
};
import { Sample } from "./Components/Sample.tsx";
import { MouseEvent } from "react";
function App() {
const alertbox = (_: MouseEvent<HTMLButtonElement>) => {
alert("foo bar baz!");
};
return (
<>
<Sample onClickButton={alertbox} />
</>
);
}
VueもpropsでFunction
渡しがサポートされているので同じ感じ。
onClickの場合、v-on:click
で渡せる。
Reactと違い、Vueの返り値のMouseEventは React.MouseEvent
のような独自クラスではなく、Javascriptの標準クラスを使用する。
<script setup lang="ts">
const props = defineProps<{
onClickButton: (event: MouseEvent) => void
}>();
</script>
<template>
<button v-on:click="props.onClickButton()">Push!</button>
</template>
<script setup lang="ts">
import Sample from "./components/Sample.vue";
const alertbox = (_: MouseEvent) => {
alert("foo bar baz!")
}
</script>
<template>
<Sample :on-click-button="alertbox" />
</template>
Props (関数渡し・引数有り)
クリックするとコンソールに2の二乗を表示するプログラムを実装。
onClickにはあくまで関数を渡す必要があります。
なのでonClickにはアロー関数などを渡す必要があるのですが、中に書いてしまうと再描画の度に関数を生成するのであまり適切ではない[1]ようです。HooksではuseCallback
を使うことが推奨のようですが、propsの説明なのでここでは一旦省きます。
import { FC } from "react";
type Props = {
value: number;
onClickButton: (num: number) => void;
};
export const Sample: FC<Props> = ({ value, onClickButton }) => {
return (
<>
<button onClick={() => onClickButton(value)}>Click Event</button>
</>
);
};
import { Sample } from "./Components/Sample.tsx";
function App() {
const squared = (num: number) => {
console.log(num * num);
};
return (
<>
<Sample value={2} onClickButton={squared} />
</>
);
}
Vueの場合、v-on
が吸収してくれるのでそんなことを気にしなくてもただ関数と引数を書くだけです。
<script setup lang="ts">
const props = defineProps<{
value: number
onClickButton: (num: number) => void
}>();
</script>
<template>
<button v-on:click="props.onClickButton(props.value)">Push!</button>
</template>
<script setup lang="ts">
import Sample from "./components/Sample.vue";
const squared = (num: number) => {
console.log(num*num);
}
</script>
<template>
<Sample :value="2" :on-click-button="squared" />
</template>
Slot (概念が存在しない)
Reactでは、Vueのslotのような概念はない。渡されたものはpropのchildren
として飛んでくる。
そのため名前付きslotのような機能はない(近しいことはできなくもない[1])。
import { FC, ReactNode } from "react";
type Props = {
children: ReactNode;
};
export const Sample: FC<Props> = (props) => {
return (
<>
<button>{props.children}</button>
</>
);
};
import { Sample } from "./Components/Sample.tsx";
function App() {
return (
<>
<Sample>
<strong>aiueo</strong>
</Sample>
</>
);
}
Vueはいたってシンプル。
<template>
<button>
<slot/>
</button>
</template>
<script setup lang="ts">
import Sample from "./components/Sample.vue";
</script>
<template>
<Sample>
<strong>aiueo</strong>
</Sample>
</template>
if
React: HTML内でのif
論理演算子&&
や三項演算子?
が使える。
if文は使えない。
export const Sample: FC = () => {
const flag = true;
return (
<>
{flag && <p>foo</p>}
{flag ? <p>bar</p> : <p>baz</p>}
</>
);
};
React: returnの外でのif
当然と言えば当然だが、ただのJSの関数なので使える。
import { FC } from "react";
export const Sample: FC = () => {
const flag = true;
if (flag) {
return <p>foo</p>
} else {
return <p>bar</p>
}
};
v-if
Vueの場合 もはやサンプルを書く必要すらないかも
<script setup lang="ts">
const flag = true
</script>
<template>
<p v-if="flag">foo</p>
<p v-else>bar</p>
</template>
for
React
こっちもHTMLの中にfor文は書けないし、return文をforで回すわけにもいかない。
なので Array.map
などを使って回す。
Vueのv-for
と同様に、key
を渡すことが推奨されている。
export const Sample: FC = () => {
const users = ["Taro", "Jiro", "Foo", "Bar", "John"];
return (
<>
<ul>
{users.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
</>
);
};
Vue
v-for
を使う。こっちも説明の必要はないが念の為。
<script setup lang="ts">
const users = ["Taro", "Jiro", "Foo", "Bar", "John"];
</script>
<template>
<ul>
<li v-for="user of users">{{user}}</li>
</ul>
</template>
Hook
ここから本題。たぶんVueと比較するのが難しそうな気がしている。
Hookとは
React 16.8から追加された。
クラスを使わなくても、Reactのstateなどの機能が使えるという機能。
Vueで言うところのOptions API - Composition APIの関係性に近く、昔のクラスベースのReactと新しいHookで共存させることができる。Options APIと同様に、過去のクラスベースの構文が今後消えるわけでもないらしい。
useState
state
はコンポーネントが保持するメモリの役割を果たす[1]。
Vueで言うところの ref()
に近いかも。
以下のようにして使う。
import { FC, useState } from "react";
export const Sample: FC = () => {
const [name, setName] = useState("Taro");
return (
<>
<p>Hello, {name} !</p>
</>
);
};
例えば上記のように state
が string
だった時は、useState()
はこんな感じ。
export function useState<string>(initialState: string | (() => string)): [string, React.Dispatch<React.SetStateAction<string>>
Arrayが返ってくるので分割代入する。
第一に値、第二に値を変更するための Dispatch
が返ってくる。
変数名は慣例的に [<stateName>, set<StateName>]
とする。
const [name, setName] = useState("Taro");
コンポーネントの state
に変更があった場合、もしくは親コンポーネントが再レンダリングされた場合、Reactは コンポーネント全体を再レンダリングします。
snapshot
である
stateは state
変数は特殊な取り扱いで、スナップショットのような振る舞いになっている。
この公式ドキュメントを読めばいいけど、ここでは一応改めて要約する。
以下の説明では、このstateを例として取り上げます。
const [name, setName] = useState("Taro");
readonly
で、値を変更する時は setState()
を使う
(1) stateは stateの値である name
は immutable
(不変) であり、この値を直接変更することはできない。
// ❌これはやってはだめ。
name = "foo bar"
値を変更する時は必ず setter
を使う。
setName("foo bar")
stateで配列を取り扱うときも Array.push()
してはいけない。
// ❌直接変更してはいけない、何故ならimmutableなので
const [arr, setArr] = useState(["a", "b", "c"]);
arr.push("d");
そのため別で配列を作って、それをsetする。
const [arr, setArr] = useState(["a", "b", "c"]);
setArr([...arr, "d"])
stateのImmutability(不変性)の重要性については、公式ドキュメントを参照。
上記ドキュメントにあるように、この特性によってstateの状態を容易に把握できるようになるため、パフォーマンスの効率の向上に繋がる。stateの巻き戻しが必要になる場面はあまり思いつかないが…
set
しても、レンダリングするまでは変更されない
(2) 新しい値を 以下のようなコードがあったとします。
ボタンを押された後、name
は何になるでしょうか?
export const Sample: FC = () => {
const [name, setName] = useState("a");
const changeName = () => {
setName(name + name);
setName(name + name);
};
return (
<>
<p>Hello, {name} !</p>
<button onClick={changeName}>Change Name</button>
</>
);
};
setName(name + name)
を2回走らせていますが、出力されるのは aa
です。
直感に反しますが、これが state が snapshot
である所以です。
setName
は実際には以下の手順で処理を行います。
- setName(value) によって、
value
でname
を再レンダリングすることを予約する - レンダリングのタイミングで、
value
によってname
を更新し、再レンダリング&DOMの変更を行う
この手順で処理されるので、要はレンダリングされるまで name
は更新されません。なので今回の例では aa
しか表示されませんでした。
先程のコードをaaaa
で出力したければ、別で変数を用意しておき最後にsetします。
const changeName = () => {
let result = name;
result += result;
result += result;
setName(result);
};
Vueのような、Static Hoistingはない
Vueとの大きな違いとして、ReactにはStatic Hoisting[2] (Hosting
じゃないよ!) がないようで、以下のようなコードを書くとReactではボタンを押すたびに大遅延が発生してしまう…
Reactの再レンダリングタイミングは、
- コンポーネントの
state
が更新されたとき - 親コンポーネントが再レンダリングされたとき
が条件のようなので、レンダリングコストを考慮しながらコンポーネントを切り分けていく必要がありそう。
// ボタンを押すたび、1000msの遅延が発生する
import { FC, useState } from "react";
import { SuperHeavy } from "./SuperHeavy.tsx";
export const Sample: FC = () => {
const [name, setName] = useState("Taro");
const changeName = () => {
setName(name + name);
};
return (
<>
<p>Hello, {name} !</p>
<button onClick={changeName}>Change Name</button>
<SuperHeavy />
</>
);
};
import { FC } from "react";
export const SuperHeavy: FC = () => {
const now = performance.now();
while (performance.now() - now < 1000) {}
return (
<>
<p>wow</p>
</>
);
};
VueはStatic Hoistingで静的なv-nodeは別で保持して再利用されるので、値の変更が発生する箇所だけ処理される。そのため以下の例でも、ページを開いたときしか1000msの待ち時間は発生せず、ボタンを何度押しても即時反映される。
<script setup lang="ts">
// 1000ms待たされるのはページロード時だけ。
import {ref} from "vue";
import SuperHeavy from "./SuperHeavy.vue";
const name = ref("Taro")
const changeName = () => {
name.value += name.value
};
</script>
<template>
<p>Hello, {{name}} !</p>
<button @click="changeName">Change Name</button>
<SuperHeavy />
</template>
<script setup lang="ts">
const now = performance.now();
while (performance.now() - now < 1000) {}
</script>
<template>
<p>wow</p>
</template>
そういった理由から、Reactでは再レンダリングが不必要に行われないように注意して設計する必要がありそう。
もしくはメモ化を使う(メモ化周りは後程書く…)。
この記事がとても分かりやすかったです。
useReducer
useState
と同様に state
を更新するものだが、間に自分で定義した関数を挟むことができる。
VuexやPiniaのような状態管理ライブラリの考え方に近いかもしれない(ただし、グローバルではない)。
useReducer
の使い方は以下の通り。
const [state, dispatch] = useReducer(reducer, initialState);
先にサンプルを見た方が分かりやすいかも。
例えば、UIのテーマやレスポンシブの状態といった、ブラウザの状態を保持する state を考える。
state
本体を定義する
(1) まずState本体そのものを定義する。
今回は width
, height
, breakpoint
, theme
の4つを持っている。
export type Browser = {
width: number;
height: number;
breakpoint: "sm" | "md" | "lg";
theme: "light" | "dark";
};
(2) デフォルト値を定義
useReducer
では初期状態を渡す必要があるので、作っておく。
export const browserDefault: Browser = {
width: 0,
height: 0,
breakpoint: "sm",
theme: "light",
};
(3) アクションを定義
reducerがどんなアクションを持っているか定義する。
今回はtype
と args
を定義して、それによってアクションのタイプや与える引数を定義した。
breakpoint
は width
に従って変更されるので、引数として与える必要はない。
export type BrowserAction =
| {
type: "RESIZED";
args: {
width: number;
height: number;
};
}
| {
type: "CHANGE_THEME";
args: {
theme: "light" | "dark";
};
};
(4) reducer を定義
実際にreducerを定義する。
action.type
にタイプ、action.args
に引数が来るので、それに応じてstateを更新。
Reducerとして定義した関数は、return
で新しいstateを返す必要があります。
export const browserReducer = (state: Browser, action: BrowserAction): Browser => {
switch (action.type) {
case "RESIZED":
let newbp: "sm" | "md" | "lg";
if (action.args.width < 640) {
newbp = "sm";
} else if (action.args.width < 768) {
newbp = "md";
} else {
newbp = "lg";
}
return {
...state,
width: action.args.width,
height: action.args.height,
breakpoint: newbp,
};
case "CHANGE_THEME":
return {
...state,
theme: action.args.theme,
};
}
};
(5) 使う
後はコンポーネント側で利用します。
reducer周りはただのtype
やfunction
なので、別ファイルに記述することができるのも一つのメリット。コンポーネントの定義とごちゃごちゃにならず可読性が上がります。
import { FC, useReducer } from "react";
import { browserDefault, browserReducer } from "./BrowserReduser.tsx";
export const Sample: FC = () => {
const [browser, browserDispatch] = useReducer(browserReducer, browserDefault);
const handleResize = () => {
const width = window.innerWidth;
const height = window.innerHeight;
browserDispatch({ type: "RESIZED", args: { width, height } });
};
const handleTheme = (theme: "light" | "dark") => {
browserDispatch({ type: "CHANGE_THEME", args: { theme } });
};
return (
<>
<div>
<h1>Current State</h1>
<ul>
<li>
Size: {browser.width} x {browser.height}
</li>
<li>Breakpoint: {browser.breakpoint}</li>
<li>theme: {browser.theme}</li>
</ul>
</div>
<div>
<h1>Change Buttons</h1>
<button onClick={handleResize}>Resized</button>
<button
onClick={() => {
handleTheme("light");
}}
>
Change light theme
</button>
<button
onClick={() => {
handleTheme("dark");
}}
>
Change dark theme
</button>
</div>
</>
);
};
この記事を参考にしました。
useState
との使い分け
以下のような状態なら、useState
よりも useReducer
を使う動機がありそう。
- stateをある程度のデータのまとまり・オブジェクトとして扱いたい時に…
- データの集合体として使いたいが、一部データのみの更新パターンがある (
theme
だけ変えたいなど) - state内のフィールドの情報を基に、別フィールドの値を動的に生成したい (上記
breakpoint
の例)
- データの集合体として使いたいが、一部データのみの更新パターンがある (
- 上限・下限値など、setterの処理を自分で制御したい
- stateの更新処理が複雑で、コンポーネント外に切り出して可読性を上げたい
- 複雑なデータなどを取り扱うstateで、テストのためにコンポーネントから分離したい
useContext
ユーザのログイン情報や、カラーテーマなど、上のコンポーネントから下のコンポーネント、特に深い階層まで渡そうとしたとき、props
で全部渡すと地獄みたいなことになってしまう…。
特に中間コンポーネントでは値を使用しないのに、最深部のコンポーネントだけで使うような場合は最悪なことになってしまう。
useContext
を使う事でこれを解消することができ、Context以下のコンポーネント全体にデータを送ることができる。
今回は親コンポーネント→孫コンポーネントにカラーテーマを送ってボタンの色を変更する例を考える。
(1) Contextを作成
まずコンポーネント 外 でContextを作成する。
export type ColorTheme = "light" | "dark";
export const ColorThemeContext = createContext<ColorTheme | null>(null);
Provide
する
(2) Contextを 親コンポーネントのreturnするHTML内に、コンポーネントのようにタグを追加します。
Provider
以下全てのコンポーネントに値が伝播します。
export const Sample: FC = () => {
return (
<>
<ColorThemeContext.Provider value={"dark"}>
<SampleChild />
</ColorThemeContext.Provider>
</>
);
};
中間コンポーネントも作っておきます。
export const SampleChild: FC = () => {
return (
<>
<SampleGrandChild />
</>
);
};
useContext
で値を取得する
(3) 孫コンポーネント内で useContext
して、contextの値を受け取ります。
受け取った値は普通に使えます。
export const SampleGrandChild: FC = () => {
const theme = useContext(ColorThemeContext);
const lightStyle = {
color: "gray",
backgroundColor: "white",
};
const darkStyle = {
color: "white",
backgroundColor: "gray",
};
if (theme === "light") {
return <button style={lightStyle}>Light Theme</button>;
} else {
return <button style={darkStyle}>Dark Theme</button>;
}
};
📌コード全体
export type ColorTheme = "light" | "dark";
export const ColorThemeContext = createContext<ColorTheme | null>(null);
export const Sample: FC = () => {
return (
<>
<ColorThemeContext.Provider value={"dark"}>
<SampleChild />
</ColorThemeContext.Provider>
</>
);
};
export const SampleChild: FC = () => {
return (
<>
<SampleGrandChild />
</>
);
};
export const SampleGrandChild: FC = () => {
const theme = useContext(ColorThemeContext);
const lightStyle = {
color: "gray",
backgroundColor: "white",
};
const darkStyle = {
color: "white",
backgroundColor: "gray",
};
if (theme === "light") {
return <button style={lightStyle}>Light Theme</button>;
} else {
return <button style={darkStyle}>Dark Theme</button>;
}
};
Provider
が存在する時
複数の useContext
に対して上の階層のコンポーネントを見に行った時、複数のProvider
があった時は、呼び出すコンポーネントに最も近い Provider
の値を参照する。
下記の例では、SampleGrandChild
の theme
は light
になる。
export const Sample: FC = () => {
return (
<>
<ColorThemeContext.Provider value={"dark"}>
<SampleChild />
</ColorThemeContext.Provider>
</>
);
};
export const SampleChild: FC = () => {
return (
<>
<ColorThemeContext.Provider value={"light"}>
<SampleGrandChild />
</ColorThemeContext.Provider>
</>
);
};
export const SampleGrandChild: FC = () => {
const theme = useContext(ColorThemeContext);
// --省略--
if (theme === "light") {
return <button style={lightStyle}>Light Theme</button>;
} else {
return <button style={darkStyle}>Dark Theme</button>;
}
};
Stateとの組み合わせ
stateと組み合わせることで、値が変更されたら再レンダリングを走らせることができる。
ボタンを押したら孫コンポーネントの色が変わる例。
export const Sample: FC = () => {
const [theme, setTheme] = useState<ColorTheme>("light");
const changeTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<>
<ColorThemeContext.Provider value={theme}>
<SampleChild />
</ColorThemeContext.Provider>
<button onClick={changeTheme}>Change Theme</button>
</>
);
};
export const SampleChild: FC = () => {
return (
<>
<SampleGrandChild />
</>
);
};
export const SampleGrandChild: FC = () => {
const theme = useContext(ColorThemeContext);
const lightStyle = {
color: "gray",
backgroundColor: "white",
};
const darkStyle = {
color: "white",
backgroundColor: "gray",
};
if (theme === "light") {
return <button style={lightStyle}>Light Theme</button>;
} else {
return <button style={darkStyle}>Dark Theme</button>;
}
};
useRef
コンポーネントに情報を保存したいが、レンダリングはしたくない時に使う。
Vueの ref
と名前は似ているが…別物と言える?
const ref = useRef(0)
state
との大きな違いは以下の通り。
-
ref
を更新しても、レンダリングは走らない。 - mutableであり、
Dispatch
を介さずに直接値を変更することができる。
クリックすると alert()
でnumを表示するサンプル。
import { FC, useRef, useState } from "react";
export const Sample: FC = () => {
const num = useRef(0);
const handleClick = () => {
num.current++;
alert(`num: ${num.current}`);
};
return (
<>
<button onClick={handleClick}>Click!</button>
</>
);
};
Vueの場合。レンダリングに関係ない変数は普通に let
で定義すればよい。useRef
のような特殊なことをしなくてもいい。
<script setup lang="ts">
let num = 0
const handleClick = () => {
num++
alert(`num: ${num}`)
};
</script>
<template>
<button @click="handleClick">Click!</button>
</template>
Reactの場合、useRefを使わないとどうなる?
レンダリングに関係ないなら普通に変数定義すればいいのでは?とも考えるが、Reactの場合通常の変数はレンダリング時にコードが丸ごと走りなおすので、変数の値が初期値に戻ってしまう。
例えば以下のようなサンプルで、num
は useRef
を使い、 nn
は普通の変数定義にする。
import { FC, useRef, useState } from "react";
export const Sample: FC = () => {
const num = useRef(0);
let nn = 0;
const [testState, setTestState] = useState(0);
const handleClick = () => {
num.current++;
nn++;
alert(`num: ${num.current}\nnn: ${nn}`);
};
const handleStateClick = () => {
setTestState(testState + 1);
};
return (
<>
<button onClick={handleClick}>Click!</button>
<button onClick={handleStateClick}>Update State: {testState}</button>
</>
);
};
結果はこうなってしまう。
[handleClick] -> num: 1 nn:1
[handleClick] -> num: 2 nn:2
[handleClick] -> num: 3 nn:3
[handleStateClick] -> num:3 nn:0
[handleClick] -> num: 4 nn:1
state
の更新などがかかると、レンダリングが走って通常の変数は再度初期化されてしまうため、値が初期値に戻ってしまう。そのため内部的に値を保持するには useRef
が必須。
実際のユースケース: DOM要素の参照に使う
実際のユースケースとしては、DOM要素の参照を拾うのによく使われている様子。
例えば width: 70%
のボックスの width
を取得するサンプル…
import { FC, useRef } from "react";
export const Sample: FC = () => {
const boxRef = useRef<HTMLDivElement>(null);
const handleClick = () => {
alert(`70% width: ${boxRef.current?.clientWidth}`);
};
return (
<>
<div
style={{
margin: "0 auto",
width: "70%",
color: "white",
backgroundColor: "black",
}}
ref={boxRef}
>
test box
</div>
<button onClick={handleClick}>Calc size</button>
</>
);
};
結果はこんな感じ。