リストと key
リストと key 。まずはリストの変換方法のおさらいからスタートです。
map
関数を使ったコード。
const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map((number) => number * 2); console.log(doubled); // [2, 4, 6, 8, 10]
配列を要素のリストに変換することが、上記とほぼ同様である。
複数コンポーネントをレンダーする
要素の週語を {}
で囲み JSX の中に書くことができる。
配列 numbers
の各要素を map
で <li>
でくくり、変数 listItem
に返す。
listItem
配列を <ul>
要素ではさみレンダーする。
const numbers = [1, 2, 3, 4, 5]; const listItems = numbers.map((number) => <li>{number}</li>); ReactDOM.render(<ul>{listItems}</ul>, document.getElementById('root'));
1から5までの数字の箇条書きリストが表示される。
## 基本的なリストコンポーネント
先程の例を numbers
配列を受け取り要素のリストを出力するコンポーネントに書き換える。
function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => <li>{number}</li> ); return ( <ul>{listItems}</ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') );
コードを実行するとブラウザに期待した結果が表示されます。
Warning: Each child in a list should have a unique "key" prop.
それと同時に上記の警告も出力されます。ユニークなキーを追加する。
... const listItem = numbers.map((number) => <li key={number.toString()}>{number}</li>);
key
key
は。どの要素が要素が変更、追加もしくは削除されたのかを識別するのに役立ちます。配列内の項目に識別するために、それぞれの項目にユニークな key
を与えます。
多くの場合、データ内にある ID
を key
として使うことになる。
const todoItems = todos.map((todo) => <li key={todo.id}>{todo.text}</li>);
データ設計時はユニークなキーフィールドに id
名を割り当ておくように留意しとこう!
レンダーされる要素に安定したID
がない場合は、項目のインデックスを代用する。
const todoItems = todo.map((todo, index) => <li key={index}> {todo.text} </li> )
todoItems
のようなリストは、並び順が変更されたり、追加登録されたり削除される可能性がある。 このようなリスト要素に変更が加わった場合、変更前の index
とj変更後の index
は変化シます。
依存するのがどれほど危険かわかりますね。
key でリンクされている Index as a key is an anti-pattern ではユニークID 生成ライブラリとして nanoid が紹介されている。 UUID しか知らなかった。勉強になります!
A tiny, secure, URL-friendly, unique string ID generator for JavaScript.
key のあるコンポーネントの抽出
ListItem
コンポーネントを抽出する際には、 key
は、配列内の <ListItem />
要素に残しておくべき。
正しい key
の使用法
function ListItem(props) { // こで key を指定しない return <li>{props.value}</li>; } function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => ( // key は配列の中で指定せよ <ListItem key={number.toString()} value={number} /> )); return <ul>{listItems}</ul>; } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render(<NumberList numbers={numbers} />, document.getElementById('root'));
key
は、それを取り囲んでいる配列側で用いる。
key
の基本ルールは、map()
呼び出しの中に現れる要素に必要となる。
兄弟要素の中で一意であればよい
key
は兄弟要素の中でユニークでなければならない。グローバルではユニークでなくてもよい。
次は sidebar
と content
を兄弟要素として持つ Blog
コンポーネントです。
function Blog(props) { const sidebar = ( <ul> {props.posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); const content = props.posts.map((post) => ( <div key={post.id}> <h3>{post.title}</h3> <p>{post.content}</p> </div> )); return ( // sidebar と content は兄弟要素 <div> {sidebar} <hr /> {content} </div> ); } const posts = [ { id: 1, title: 'Hello World', content: 'Welcome to learning React!' }, { id: 2, title: 'Installation', content: 'You can install React from npm.' }, ]; ReactDOM.render(<Blog posts={posts} />, document.getElementById('root'));
key
は、コンポーネントには渡されない。 React へのヒントとして使われる。同じ値をコンポーネントでも必要となる場合は、別の名前の props
として明示的に渡す。
const content = posts.map((post) => <Post key={post.id} id={post.id} title={post.title} /> );
Post
コンポーネントは props.id
は読み取れるが、 props.key
は読み取れない。
mapを JSX に埋め込む
map()
の結果をインラインとして記述できる。
インライン化部分。
{numbers.map((number) => ( <ListItem key={number.toString()} value={number} /> ))}
全体のコード。
function ListItem(props) { return <li>{props.value}</li>; } function NumberList(props) { const numbers = props.numbers; return ( <ul> {numbers.map((number) => ( <ListItem key={number.toString()} value={number} /> ))} </ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render(<NumberList numbers={numbers} />, document.getElementById('root'));
map()
がネストするならコンポーネントに抽出することを考慮する。
リストと key
の項目はパターンが多い。頭に入り切らない。公式サイトを何度も参照することになること間違いなしです。
フォーム
9.フォーム 。
フォーム要素は他のDOM要素と異なる動作をする。
HTMLフォームの標準の動作は、フォームを送信した際に新しいページに移動することです。しかし、フォームの送信に応じて入力したデータにアクセスする関数があった方が便利です。
制御されたコンポーネントと呼ばれるテクニックを使うことで、実現できる。
制御されたコンポーネント
<input>
、 <textarea>
、 <select>
のようなフォーム要素は、それ自身で状態を保持している。
React では、変更されうる状態は state
プロパティに保持され、 setState()
関数で更新を管理する。
フォーム送信時に名前をログに残す場合を例にコンポーネントを記述する。
class NameForm extends React.Component { constructor(props) { super(props); this.state = { value: '' }; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { // 制御された値 this.state.value を setState 関数に渡す // フォームに入力された値はキーストローク毎にキャプチャされる console.log(`[handleChange event]: ${event.target.value}`); this.setState({ value: event.target.value }); } handleSubmit(event) { // 制御された値 this.state.value を setState 関数に渡す // submit毎にキャプチャされる console.log(`[handleSubmit event]: ${this.state.value}`); alert('A name was submitted: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Name: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="Submit" /> </form> ); } } ReactDOM.render(<NameForm />, document.getElementById('root'));
<input>
要素から value={this.state.value}
と属性値を渡している。これにより React の state
が信頼できる情報源となる。
これでフォームの状態と setState()
の状態を結合できる。別の言い方をすれば、フォームの状態を React が管理可能になったと理解できそうです。
handleChange
はキーストローク毎に実行され、 state
を更新する。
React の管理下になった値は、他の UI に渡したり、他のイベントハンドラからリセットできるようになる。
例では、テキストボックスに入力した値を送信ボタンをクリックする毎に alert
に使いまわし通知している。
Hooks で書き換える
今記述した NameForm
はクラスコンポーネントです。 Hooks で書き換える。
function NameForm(props) { // 文字列は配列です const [name, setName] = React.useState([]); const handleSubmit = (e) => { alert('A name was submitted: ' + name); e.preventDefault(); }; return ( <form onSubmit={handleSubmit}> <label> Name: <input type="text" value={name} onChange={(e) => setName(e.target.value)} /> </label> <input type="submit" value="Submit" /> </form> ); } ReactDOM.render(<NameForm />, document.getElementById('root'));
<input>
には文字列が入力される。文字列は配列の一種なので、初期値は空の配列にする。
preventDefault();
を alert
より先に書きたい誘惑に駆られます。だが、公式サイトの順番を守っておく。
const handleSubmit = (e) => { // e.preventDefault(); alert('A name was submitted: ' + name); e.preventDefault(); };
textareaタグ
<textarea value={}>
を使い HTML の <textarea>
の代わりとする。単一行の入力フォームと似た書き方ができる。
class EssayForm extends React.Component { constructor(props) { super(props); this.state = { value: 'Please write an essay about your favorite DOM element.', }; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({ value: event.target.value }); } handleSubmit(event) { alert('An essay was submitted: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Essay: <textarea value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="submit" /> </form> ); } } ReactDOM.render(<EssayForm />, document.getElementById('root'));
constructor
内で this.state.value
に文字列を定義シているので、表示されるページには最初からテキストが入っている。
constructor(props) { super(props); this.state = { value: 'Please write an essay about your favorite DOM element.', };
Hooks で書き換える
「Hooks への書き換えも随分と慣れてきた」などと思っていたら違った。 EssayForm
コンポーネントに props
引数がなくても動作する。いつから勘違いしていたんだ!?
// const EssayForm = (props) => { const EssayForm = () => { const [text, setText] = React.useState([ 'Please write an essay about your favorite DOM element.', ]); const handleSubmit = (e) => { alert('An essay was submitted: ' + text); event.preventDefault(); }; return ( <form onSubmit={handleSubmit}> <label> Essay: <textarea value={text} onChange={(e) => setText(handleChange)} /> </label> <input type="submit" value="submit" /> </form> ); }; ReactDOM.render(<EssayForm />, document.getElementById('root'));
selectタグ
HTML の selected
属性の代わりに、 親の select
タグで value
属性を使う。
constructor(props) { super(props); this.state = { value: 'coconut' }; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({ value: event.target.value }); } handleSubmit(event) { alert('Your favorite flavor is: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Pick your favorite flavor: <select value={this.state.value} onChange={this.handleChange}> <option value="grapefruit">Grapefruit</option> <option value="lime">Lime</option> <option value="coconut">Coconut</option> <option value="mango">Mango</option> </select> </label> <input type="submit" value="Submit" /> </form> ); } } ReactDOM.render(<FlavorForm />, document.getElementById('root'));
<input type="text">
、 <textarea>
、 <select>
の3つは、 value
属性を受け取る。
複数の入力の処理
複数の input
要素を処理する場合、各入力要素に name
属性を追加し、ハンドラに event.target.name
に基づいた処理を選択できる。
class Reservation extends React.Component { constructor(props) { super(props); this.state = { isGoing: true, numberOfGuests: 2, }; this.handleInputChange = this.handleInputChange.bind(this); } handleInputChange(event) { const target = event.target; const value = target.type === 'checkbox' ? target.checked : target.value; const name = target.name; this.setState({ [name]: value, }); } render() { return ( <form> <label> is Going: <input name="isGoing" type="checkbox" checked={this.state.isGoing} onChange={this.handleInputChange} /> </label> <br /> <label> Number of guests: <input name="numberOfGuests" type="number" value={this.state.numberOfGuests} onChange={this.handleInputChange} /> </label> </form> ); } } ReactDOM.render(<Reservation />, document.getElementById('root'));
次の []
を用いたプロパティ名は Computed property names と呼ぶ。
// Computed property name 構文(計算されたプロパティ名) this.setState({ [name]: value, });
ES5 における次のコードと同じ。
var partialState = {}; partialState[name] = value; this.setState(partialState);
JavaScript Primer では、 プロパティの追加 で説明されている。
式の評価結果をプロパティ名に使う
MDN では 計算されたプロパティ名 で説明されている。
括弧
[]
の中に式を記述でき、それが計算されてプロパティ名として使用されます。
この構文のことをすっかり忘れていた。
私のモダンJavaScript戦闘力は低い!モダンJavaScriptの速習が改めて必要です。
本格的なソリューション の中で Formik が紹介されている。 フォームはユーザ入力を受け付ける窓口だ。やはりきちんと作り込まれたライブラリがある。
日本語の情報だと CodeZine の React向けライブラリを解説 が 2020/12/07 で比較的新しい記事を読むことができる。