この記事の目次
入力欄では展示名を直したのに、隣のプレビューは古いまま。React 状態管理でこのずれを直すときは、二つの値を同期する処理を書く前に、同じ情報を二か所で持っていないか調べます。
架空の展示会の見出し編集画面を作ります。JavaScriptの関数、JSX、propsの基本が分かる人向けです。入力とプレビューを別コンポーネントにしたまま、編集内容を共有し、リセットも一度で反映させます。サーバー保存や、複数の利用者による共同編集は含みません。
二つの画面が、同じ値の持ち主を参照する
stateは、Reactが次の表示にも保持する値です。入力コンポーネントとプレビューがそれぞれ展示名のstateを持つと、一方を更新しても、もう一方は自動では変わりません。変数名が両方titleでも、同じ記憶場所にはならないためです。
二つを包む共通の親がtitleを持ち、入力へは現在値と更新を頼む関数、プレビューへは現在値を渡します。React公式の状態共有の説明で示される、共通の親へstateを移す方法です。関係する最も近い親に置けば、無関係な画面まで状態を持たせずに済みます。
今回の更新経路は「入力操作→親の更新関数→親の新しいtitle→二つの子の表示」です。子から親のDOMを書き換えたり、別の子の関数を直接呼んだりしません。どこで値を決めたかを一か所へ集めるのが目的です。
小さな編集画面を、二つのファイルで作る
練習用の空フォルダを作り、ターミナルで次を実行します。確認用の組み合わせはNode.js 24系、React / React DOM 19.2.0、Vite 7.1.7です。最新版を自動で選ぶ例ではなく、同じコードを試すために版を固定しています。
npm init -y
npm install --save-exact react@19.2.0 react-dom@19.2.0
npm install --save-dev --save-exact vite@7.1.7
index.htmlへ次を書きます。Viteが読み込む入口です。ここではReactの状態共有だけを扱うため、ルーティング用のライブラリやNext.jsは使いません。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>展示名の編集|練習</title>
<style>
body { margin: 1rem; font-family: sans-serif; line-height: 1.8; }
main { max-width: 42rem; margin: auto; }
input { box-sizing: border-box; width: 100%; font: inherit; }
section { margin-block: 1.5rem; padding: 1rem; border: 1px solid #777; }
h2 { overflow-wrap: anywhere; }
button { font: inherit; }
:focus-visible { outline: 3px solid #245ba7; outline-offset: 3px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>
同じフォルダにmain.jsxを作ります。JSXを変換するこの最小構成ではReactも明示的にimportします。
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
function TitleEditor({ title, onTitleChange }) {
return <section>
<label htmlFor="title">展示名</label>
<input id="title" value={title}
onChange={event => onTitleChange(event.target.value)} />
</section>;
}
function Preview({ title }) {
const heading = title.trim() || '展示名を入力してください';
return <section aria-label="プレビュー">
<h2>{heading}</h2>
<p>この画面は練習用で、入力内容を保存・送信しません。</p>
</section>;
}
function App() {
const [title, setTitle] = useState('紙と光の展示');
return <main>
<h1>展示名を編集する</h1>
<TitleEditor title={title} onTitleChange={setTitle} />
<Preview title={title} />
<button type="button" onClick={() => setTitle('紙と光の展示')}>
最初の展示名に戻す
</button>
</main>;
}
createRoot(document.getElementById('root')).render(<App />);
npx vite --host 127.0.0.1で開発用サーバーを起動し、表示されたローカルURLを開きます。Viteの入口と実行方法は公式ガイドで確認できます。ファイルを直接ダブルクリックする方法では、この構成をそのまま実行できません。
入力は元の文字、見出しは表示用の文字
AppだけにuseStateがあります。TitleEditorはvalueに渡されたtitleを表示し、入力が変わったらonTitleChangeへ文字列を渡します。この関数名はこの例で決めたprops名で、Reactに最初からある命令ではありません。
Previewのheadingはstateではなく、毎回の表示でtitleから計算する値です。前後の空白を除き、空なら案内を出します。状態の構造を選ぶ公式資料でも、既存の値から計算できる情報を重ねて保持しない考え方が示されています。
入力のたびにtrimした文字をstateへ戻すと、利用者が書きかけた末尾の空白も消えます。この例では入力した文字は保持し、プレビューだけを整形します。HTMLとして表示するための文字列を作る必要もありません。通常のJSXの文字列表示で、タグに似た文字も文字として扱います。基本の出力先の考え方はtextContentの記事と比較できます。
子にコピーを残すと、リセットでずれる
一つ目の失敗は、親へstateを移した後も、子にuseState(title)を残すことです。この初期値は後から変わるpropsを追い続ける指定ではありません。親のリセット後に、入力欄だけが以前の値になる場合があります。
修正では子のコピーを削除し、完成例のようにpropsのtitleをvalueへ直接渡します。別の名前を入力してリセットを押し、入力欄とプレビューの両方が「紙と光の展示」に戻れば、この条件を確認できています。二つの更新関数をリセットボタンに並べる修正では、ずれる原因が残ります。
二つ目は、inputにvalueだけを渡してonChangeを付け忘れる失敗です。キーを押しても値が変わらず、開発時には読み取り専用の扱いに関する警告も出ます。修正では操作を親へ返す関数をつなぎ、文字の追加と削除の両方を試します。値の流れと操作の流れを一組として見てください。
三つ目は、プレビュー用のheadingまでstateにし、Effectで同期することです。この画面には外部システムとの同期がありません。計算できる表示文を保存する代わりに、描画時の計算へ戻します。空白だけの入力でも見出しが案内文になり、入力欄には元の空白が残ることを再確認します。
すべてのstateを一番上へ上げる必要はない
今回共有したいのは展示名です。入力欄の補足を開くかどうかなど、ほかの部品が使わない状態なら、その部品に置く選択ができます。「共通の親へ移す」は、アプリの全stateを最上位へ集める規則ではありません。
また、確定ボタンを押すまではプレビューを変えない仕様なら、編集中の文字と確定済みの文字は別の情報です。その場合は二つを保持する理由があります。名前が同じかどうかではなく、片方だけ変わってよい場面があるかで判断します。今回のような即時反映とは完成条件を分けてください。
公開用ビルドでも、入力から復帰まで試す
開発サーバーを止めてnpx vite buildを実行し、続けてnpx vite preview --host 127.0.0.1で生成結果を開きます。開発中だけ動く状態を見落とさないため、同じ操作を公開用ビルドでも試します。
15〜20分の確認として、通常の名前を入力すると両方が一致する、全消去するとプレビューだけ案内文になる、リセットすると両方が初期名へ戻る、Tabで入力欄とボタンへ進める、の四条件を記録します。<展示>という文字列も、要素に変わらずそのまま読めることを確かめます。
幅320pxで長い展示名の末尾まで読み、再読み込みすると初期名に戻ることも確認します。保存機能を付けていないので、最後の動作は不具合ではありません。カリキュラムで学習項目を確認するときも、この練習の成果は「状態の持ち主と更新経路を説明できること」として整理できます。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。