2.1K Views
July 18, 25
スライド概要
Vue.jsからフロントエンド開発に入った人に向けて、Reactを書くにあたってのメンタルモデルの違いを理解できるようにします。
Vue.jsからはじめるReact ashphy @[email protected]
今日の目標 • Vue.jsとの比較からReactのメンタルモデルの違いを理解する
おことわり • Vue 3.5を知ってることを前提とする • Vapor Modeは触れない • VueとReactどちらが優れているかという議論は行わない • Reactを知っている人には新しい情報はない
どの範囲がレンダリングされるか? ボタンを押したときにどの範囲が再レンダリングされるでしょうか?
どの範囲がレンダリングされるか? • ReactとVueのコード
Vue 親コンポーネントのみが再レンダリングされる
React 親子の両方が再レンダリングされる
Reactは子要素も再レンダリングされる レンダーとコミット ‒ React
Reactは全体を再レンダリング • 差分が発生した要素からすべて再レンダリングしていく • 何が変わったのかではなく、今の状態に対するUIを計算する
Vueは変化した要素だけ再レンダリング • Vueは状態(state)が使われたコンポーネントを追跡している = Signals
Vueはなぜ変化したコンポーネントだけ 再レンダリングできるのか?
ref と useState Vue const count = ref(0); Refオブジェクト const increment = () => { count.value++; }; React const [count, setCount] = useState(0); 値 更新関数 const increment = () => { setCount((prev) => prev + 1); };
refの実装 (擬似コード) const myRef = { _value: 0, get value() { 状態の仕様を追跡 track() return this._value }, set value(newValue) { this._value = newValue trigger() } 状態の変更を通知 } プロパティアクセスで状態の使用状況を追跡
reactiveの実装 (擬似コード) const reactive(target) { const handler = { 状態の仕様を追跡 get(obj, key, receiver) { track(obj, key); return Reflect.get(obj, key, receiver); }, set(obj, key, value, receiver) { const oldValue = obj[key]; const result = Reflect.set(obj, key, value, receiver); if (oldValue !== value) { trigger(obj, key); } 状態の変更を通知 return result; } }; return new Proxy(target, handler); プロキシを返す }
分割代入でリアクティブでなくなる
<script setup lang="ts">
const count = ref(0);
const { value } = count;
const increment = () => {
count.value++;
};
</script>
リアクティブでなくなる
<template>
<div>Count: {{ value }}</div>
<button @click="increment">Increment</button>
</template>
computed
Vue
const firstName = ref('John’)
const lastName = ref('Doe’)
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
React
const [firstName, setFirstName] = useState('John’);
const [lastName, setLastName] = useState('Doe’);
const fullName = `${firstName} ${lastName}`;
コンポーネント本体で宣言された変数はすべてリアクティブである.
※ computedはメモ化するので正確にはuseMemoが必要
Reactは純粋性(purity)を重視する • 純粋なコンポーネントやフックとは • べき等である • 同じ入力 (props, state, context) で常に同じ結果が得られること • レンダー時に副作用がない • 副作用はレンダー以外のイベントハンドラなどで実行される必要がある • ローカルな値以外を更新しない
宣言型UI UI = 𝑓(𝑥)
宣言型UI props, state, context UI = 𝑓(𝑥) コンポーネント 同じ入力 (props, state, context) で常に同じ結果が得られる
ref と useState Vue const count = ref(0); Refオブジェクト const increment = () => { count.value++; }; stateはmutable React const [count, setCount] = useState(0); 値 更新関数 const increment = () => { setCount((prev) => prev + 1); }; stateはimmutable
純粋であるとなにがいいのか? • 安全にキャッシュできる • どの順番で計算しても良い • レンダーを中断してもいい • サーバでも実行できる
Reactのレンダリング • レンダー (Rendering) • Reactがコンポーネントを呼び出すこと • コミット (Commit) パッチ(Patch) • ReactがDOMノードを更新すること • ペイント (Paint) • ブラウザがDOMを画面に描画すること
useStateの値はすぐには更新されない • state はスナップショットである
stateはスナップショットである • state をセットしても、既にある state 変数は変更されず、 代わりに再レンダーがトリガされる。
watch, watchEffect • watch • 監視するソースを自分で指定できる • once: true • 一度だけ実行できる • immediate: true • 強制的に即時実行できる コールバックをいつ実行するのか 正確にコントロールできる
useEffect • コンポーネントを外部システムと同期させるためのフック • useEffectには同期を開始する処理と終了する処理のみを記載 する
ライフサイクルフック • onMounted, onUpdated, onUnmounted => useEffect • コンポーネントが現在マウント、更新、アンマウントのどれを 行っているかを考慮すべきではない
useEffectの例
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState("https://localhost:1234");
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
}
useEffectの例
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState("https://localhost:1234");
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
セットアップコード
connection.disconnect();
};
}, [serverUrl, roomId]);
}
useEffectの例
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState("https://localhost:1234");
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
クリーンアップコード
}, [serverUrl, roomId]);
}
useEffectの例
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState("https://localhost:1234");
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
}
依存配列 リアクティブな値を指定する
useEffect • useEffectは開発中に2回実行される • 開発中のみコンポーネント、useState、set関数、useMemo、 useReducerも2回呼ばれる • コンポーネントが純粋なら「何回呼び出されても結果は同じ」 はずなので、2回呼んでみる
state 再利用可能性の保証 • 将来的にReactがstateを保ったままで一部分の追加、削除で きるような機能を導入する • → オフスクリーン • コンポーネントは複数回のmount/unmountに耐える必要が ある
Composables • hooks • ルール • ループ、条件分岐、ネストされた関数、try/catch/finallyブロック の内側で呼び出してはいけない • 早期 return を行う前に呼び出す • Reactの関数からのみ呼ぶことができる
useStateは呼び出しの順番に依存する • useStateには識別子を渡さない代わりに順番で管理される • <setup script>は一度だけ呼び出されるので管理する必要が ない
stale closure
Vue
<script setup>
const count = ref(0)
function handleClick() {
setTimeout(() => {
alert(count.value)
}, 1000)
}
</script>
<template>
<button @click="handleClick">
Click me
</button>
</template>
React
const Component() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
alert(count);
}, 1000);
};
return <button onClick={handleClick}>
Click me
</button>;
}
stale closure
Vue
<script setup>
const count = ref(0)
function handleClick() {
setTimeout(() => {
alert(count.value)
}, 1000)
}
</script>
<template>
<button @click="handleClick">
Click me
</button>
</template>
React
クロージャ (閉包環境)
const Component() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
alert(count);
}, 1000);
};
return <button onClick={handleClick}>
Click me
</button>;
}
stale closure
Vue
<script setup>
const count = ref(0)
function handleClick() {
setTimeout(() => {
alert(count.value)
}, 1000)
}
</script>
<template>
<button @click="handleClick">
Click me
</button>
</template>
React
クロージャ (閉包環境)
const Component() {
const [count, setCount] = useState(0);
const handleClick = () => {
レキシカル環境
setTimeout(() => {
alert(count);
}, 1000);
};
return <button onClick={handleClick}>
Click me
</button>;
}
stale closure
Vue
<script setup>
const count = ref(0)
function handleClick() {
setTimeout(() => {
alert(count.value)
}, 1000)
}
</script>
<template>
<button @click="handleClick">
Click me
</button>
</template>
React
クロージャ (閉包環境)
const Component() {
const [count, setCount] = useState(0);
const handleClick = () => {
レキシカル環境
setTimeout(() => {
束縛 (capture)
alert(count);
}, 1000);
};
return <button onClick={handleClick}>
Click me
</button>;
}
stale closure
Vue
<script setup>
const count = ref(0)
function handleClick() {
setTimeout(() => {
alert(count.value)
}, 1000)
}
</script>
<template>
<button @click="handleClick">
Click me
</button>
</template>
React
クロージャ (閉包環境)
const Component() {
const [count, setCount] = useState(0);
const handleClick = () => {
レキシカル環境
setTimeout(() => {
束縛 (capture)
alert(count);
}, 1000);
};
return <button onClick={handleClick}>
Click me
</button>;
}
レンダリング時の古い(stale)値に束縛される
stale closure
Vue
<script setup>
const count = ref(0)
function handleClick() {
setTimeout(() => {
alert(count.value)
}, 1000)
}
</script>
実行時の値に束縛されるが、
<template>
Refオブジェクトを通じて最新の値にアクセスできる
<button @click="handleClick">
Click me
</button>
</template>
React
クロージャ (閉包環境)
const Component() {
const [count, setCount] = useState(0);
const handleClick = () => {
レキシカル環境
setTimeout(() => {
束縛 (capture)
alert(count);
}, 1000);
};
return <button onClick={handleClick}>
Click me
</button>;
}
レンダリング時の古い(stale)値に束縛される
props Vue <script setup> const props = defineProps<{ count: number }>() const { count } = props </script> React type Props = { count: number; }; export function Counter({ count }: Props) { return <p>Count: {count}</p>; } <template> <p>Count: {{ count }}</p> </template> Reactでは引数として表現される
propsの分割代入
Vue
<script setup>
const { foo } = defineProps(['foo'])
watchEffect(() => {
// 3.5 以前は 1 回だけ実⾏される
console.log(foo)
})
</script>
const props = defineProps(['foo'])
watchEffect(() => {
console.log(props.foo)
})
React
type Props = {
count: number;
};
export function Counter(props: Props) {
const { count } = props;
return <p>Count: {count}</p>;
}
Reactはただの値なのでどこで分割代入しても良い
emit
Vue
<script setup>
const emit = defineEmits(["customClick"]);
function handleClick() {
emit("customClick", "Hello!");
}
</script>
<template>
<button @click="handleClick">
Click me
</button>
</template>
React
type Props = {
onCustomClick: (message: string) => void;
};
function Component({ onCustomClick }: Props) => {
const handleClick = () => {
onCustomClick("Hello!");
};
return <button onClick={handleClick}>
Click me
</button>;
};
ReactではイベントはPropsで渡す
フォールスルー属性
• Reactにはない 明示的に宣言される
import type { ButtonHTMLAttributes } from "react";
type MyButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
function MyButton({ className, ...rest }: MyButtonProps) {
return (
<button className={`base-class ${className ?? ""}`} {...rest} />
);
}
slot
Vue
<template>
<button class="btn">
<slot></slot>
</button>
</template>
React
type Props = {
children: React.ReactNode;
};
function Button({ children }: Props) {
return <button className="btn">
{children}
</button>;
}
名前付きslot
Vue
<template>
<button class="btn">
<span class="icon">
<slot name="icon"></slot>
</span>
<span class="label">
<slot></slot>
</span>
</button>
</template>
React
type Props = {
icon?: React.ReactNode;
children: React.ReactNode;
};
export function Button({ icon, children }: Props) {
return (
<button className="btn">
{icon && <span className="icon">{icon}</span>}
<span className="label">{children}</span>
</button>
);
}
属性のひとつとして渡すことができる
defineModel
• ない 明示的に宣言される
Vue
<script setup>
const modelValue = defineModel<string>()
</script>
<template>
<input v-model="modelValue" />
</template>
React
type Props = {
value: string;
onChange: (value: string) => void;
};
const Input = ({ value, onChange }: Props) => (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
どうやってパフォーマンスを 改善すれば良いか?
メモ化 Memorization • 関数の呼び出し結果を保存しておいて、後の呼び出しで利用し 再計算を防ぐ const [firstName, setFirstName] = useState("John"); const [lastName, setLastName] = useState("Doe"); const fullName = useMemo( () => `${firstName} ${lastName}`, [firstName, lastName] ); 依存配列に指定された値が変わると再計算される
コンポーネントもメモ化 • propsが変わらなければレンダリングをスキップする const MemoizedComponent = memo((props) => { // ... });
コンポーネントのメモ化が効かない例
interface ChildProps { onClick: () => void; }
const Child = memo(({ onClick }: ChildProps) => {
return <button onClick={onClick}>Increment</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
return (<>
<p>Count: {count}</p>
<Child onClick={() => setCount((c) => c + 1)} />
</>);
};
コンポーネントのメモ化が効かない例
interface ChildProps { onClick: () => void; }
const Child = memo(({ onClick }: ChildProps) => {
return <button onClick={onClick}>Increment</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
return (<>
<p>Count: {count}</p>
<Child onClick={() => setCount((c) => c + 1)} />
</>);
};
レンダリングのたびに関数が生成される
コンポーネントのメモ化が効かない例
interface ChildProps { onClick: () => void; }
const Child = memo(({ onClick }: ChildProps) => {
return <button onClick={onClick}>Increment</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
return (<>
<p>Count: {count}</p>
<Child onClick={() => setCount((c) => c + 1)} />
</>);
};
useCallback
• 再レンダー間で関数定義をキャッシュできるようにするフック
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
return ( <>
<p>Count: {count}</p>
<Child onClick={handleClick} />
</> );
};
子要素をchildrenとして受け取る • Render-as-children Sample
でもめんどくさくない?
React Compiler • Reactアプリを自動的に最適化する • 自動でuseMemo, useCallbackを挿入する • React Compiler Playground で試せる • 10月のReact Conf 2025でリリースされるかも…?
コンパイル結果
export default function MyApp() {
return <div>Hello World</div>;
}
import { c as _c } from "react/compiler-runtime";
export default function MyApp() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div>Hello World</div>;
$[0] = t0;
初回は要素を生成する
} else {
t0 = $[0];
}
return t0;
}
コンパイル結果
export default function MyApp() {
return <div>Hello World</div>;
}
import { c as _c } from "react/compiler-runtime";
export default function MyApp() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div>Hello World</div>;
$[0] = t0;
キャッシュする
} else {
t0 = $[0];
}
return t0;
}
コンパイル結果
export default function MyApp() {
return <div>Hello World</div>;
}
import { c as _c } from "react/compiler-runtime";
export default function MyApp() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div>Hello World</div>;
$[0] = t0;
} else {
t0 = $[0];
次回以降キャッシュを返す
}
return t0;
}
組み合わせるライブラリ
状態管理ライブラリ - Pinia • React • Jotai • Zustand • Valtio • おそらく一番Piniaに近い • Redux • 忘れていい
データ取得ライブラリ • useFetch (Nuxt) • Vite • TanStack Routerのloader • TanStack Query • Next.js • fetch (Next.jsが提供するfetchは魔改造されてる) • SWR • Vue Query • TanStack Query
ルーティング ‒ Vue Router • React Router • TanStack Router
フォーム管理ライブラリ • React Hook Form • シェアが一番多い • Conform • ドキュメントに日本語がある • TanStack Form • Vue版もあるので慣れてるならこれでいい • (あまり好きではない)
Composition Utilities - VueUse • useHooks • usehooks-ts • @react-hookz/web
まとめ • React • 全体を高速に再レンダリングする • すべてのコンポーネント、フックは純粋であると仮定
参考文献 • LEARN REACT • リアクティビティーの探求 • React Labs: 私達のこれまでの取り組み - 2022年6月版 • JavaScript Signals standard proposal
参考文献 • "React Core Panel" by Joe Savona, Ricky Hanlon, Dan Abramov, & Michael Jackson at #RemixConf 2023 💿