今日はReactのkey
属性について話しましょう。 多くの場合、Reactを使用し始めたばかりの開発者は、 key
属性をあまり重視しません。 しかし、無駄に...
あなたが鍵を使用していないことがわかったときにアヒルが言うこと
キーを完全に異なるケースで提示するには、計画を検討してください。
多くの資料があるので、各パートの終わりに結論があります。 記事の最後に、一般的な結論も示されており、これらについて簡単に説明します。 コードは、codesandboxの例とネタバレの両方で見ることができます。
和解
反応におけるキーの主なタスクは、調整メカニズムを支援することです。 名前のリストをレンダリングする小さなコンポーネントを作成しましょう:
import React from "react"; import { render } from "react-dom"; class App extends React.Component { state = { names: ["", "", ""] }; render() { return <Names names={this.state.names} />; } } class Names extends React.PureComponent { render() { return (<ul>{this.props.names.map(name => <Name>{name}</Name>)}</ul>); } } class Name extends React.PureComponent { render() { return (<li>{this.props.children}</li>); } } render(<App />, document.getElementById("root"));
キーを指定しませんでした。 コンソールに次のメッセージが表示されます。
警告:配列またはイテレータの各子には、一意の「キー」プロップが必要です。
ここで、タスクを複雑にし、ボタンを使用して入力を作成し、新しい名前を最初と最後に追加します。 さらに、 componentDidUpdate
の名前の変更ログをcomponentDidUpdate
およびDidMount
追加し、子を示します。
import React, { Component, PureComponent, Fragment } from "react"; import { render } from "react-dom"; class App extends Component { state = { names: ["", "", ""] }; addTop = name => { this.setState(state => ({ names: [name, ...state.names] })); }; addBottom = name => { this.setState(state => ({ names: [...state.names, name] })); }; render() { return ( <Fragment> <Names names={this.state.names} /> <AddName addTop={this.addTop} addBottom={this.addBottom} /> </Fragment> ); } } class AddName extends PureComponent { getInput = el => { this.input = el; }; addToTop = () => { if (!this.input.value.trim()) { return; } this.props.addTop(this.input.value); this.input.value = ""; }; addToBottom = () => { if (!this.input.value.trim()) { return; } this.props.addBottom(this.input.value); this.input.value = ""; }; render() { return ( <Fragment> <input ref={this.getInput} /> <button onClick={this.addToTop}>Add to TOP</button> <button onClick={this.addToBottom}>Add to BOTTOM</button> </Fragment> ); } } class Names extends PureComponent { render() { return <ul>{this.props.names.map(name => <Name>{name}</Name>)}</ul>; } } class Name extends PureComponent { componentDidMount() { console.log(`Mounted with ${this.props.children}`); } componentDidUpdate(prevProps) { console.log(`Updated from ${prevProps.children} to ${this.props.children}`); } render() { return <li>{this.props.children}</li>; } } render(<App />, document.getElementById("root"));
リストの最後に「バジル」を追加してから、先頭に「ポール」を追加してください。 コンソールに注意してください。 Codesandboxでは、ボタンをクリックして表示を(上から中央に)変更することにより、ソースコードを開くこともできます。
同様のリストの作業のデモンストレーション:
上から要素を追加すると、Nameコンポーネントが再描画され、新しいコンポーネントがchildren ===
作成される状況になります:
MishaからPavelに更新されました
ダニエルからミーシャに更新
マリーナからダニエルに更新
VasilyからMarinaに更新されました
Vasilyでマウント
なぜこれが起こっているのですか? 調整メカニズムを見てみましょう。
1つのツリーから別のツリーへの完全な調整と縮小は、アルゴリズムの複雑さO(n³)を伴う高価なタスクです。 これは、大量の元素では反応が遅いことを意味します。
このため、VDOM調整メカニズムは、次の単純化(ルール)を使用して機能します。
1)異なるタイプの2つの要素は異なるサブツリーを生成します。つまり、要素タイプを<div>
から<section>
または別のタグに変更すると、reactは<div>
と<section>
内のサブツリーを異なると見なします。 この反応により、 div
内にあった要素が削除され、セクション内のすべての要素がマウントされます。 タグ自体が変更された場合でも。 ツリーの削除と初期化の同様の状況は、1つの反応コンポーネントが別の反応コンポーネントに変更されたときに発生しますが、コンテンツ自体は同じように見えますが(これは誤解にすぎません)。
oldTree: <div> <MyComponent /> </div> // MyComponent newTree: <section> <MyComponent /> </section>
Reactコンポーネントでも同様に機能します。
// : // did mount // will unmount // did mount // , MyComponent , // MyComponent. class MyComponent extends PureComponent { componentDidMount() { console.log("did mount"); } componentDidUpdate() { console.log("did update"); } componentWillUnmount() { console.log("will unmount"); } render() { return <div>123</div>; } } class A extends Component { render() { return <MyComponent />; } } class B extends Component { render() { return <MyComponent />; } } class App extends Component { state = { test: A }; componentDidMount() { this.setState({test: B}); } render() { var Component = this.state.test; return ( <Component /> ); } } render(<App />, document.getElementById("root"));
2)要素の配列は要素ごとに比較されます。つまり、反応は2つの配列に対して同時に繰り返され、要素をペアで比較します。 したがって、上記の名前を持つ例のリストのすべての要素を再描画しました。 例を見てみましょう:
// oldTree <ul> <li></li> <li></li> </ul> // newTree <ul> <li></li> <li></li> <li></li> </ul>
反応は、最初に<li></li>
を比較し、次に<li></li>
を比較し、最終的に<li></li>
古いツリーにないことを検出します。 そして、この要素を作成します。
要素を追加する場合:
// oldTree <ul> <li></li> <li></li> </ul> // newTree <ul> <li></li> <li></li> <li></li> </ul>
Reactは<li></li>
と<li></li>
を比較し、更新します。 次に、 <li></li>
と<li></li>
比較し、更新して、最後に<li></li>
ます。 要素が先頭に挿入されると、reactは配列内のすべての要素を更新します。
問題を解決するために、重要な属性が反応に使用されます。 キーが追加されると、reactは要素を次々と比較しませんが、キーの値で検索します。 レンダリング名を使用した例は、より生産的になります。
// oldTree <ul> <li key='1'></li> <li key='2'></li> </ul> // newTree <ul> <li key='3'></li> <li key='1'></li> <li key='2'></li> </ul>
reactはkey='1'
、 key='2'
を見つけ、それらに変更が発生していないと判断し、新しい<li key='3'></li>
要素<li key='3'></li>
を見つけてそれだけを追加します。 したがって、キーを使用すると、reactは1つのコンポーネントのみを更新します。
キーを追加して、例を書き換えます。 リストの一番上に要素を追加すると、1つのコンポーネントのみが作成されることに注意してください。
配列内の要素のインデックスをキーとして使用せずに、名前にidを追加し、キーを直接管理していることに注意してください。 これは、リストの先頭に名前を追加すると、インデックスが移動するためです。
最初の部分を要約するには:
キーは配列の要素を使用して作業を最適化し、要素の不必要な削除と作成の数を減らします。
キーの再利用と正規化
タスクを複雑にしましょう。 次に、抽象的な人ではなく、開発チームのメンバーのリストを作成します。 会社には2つのチームがあります。 チームメンバーは、マウスをクリックして選択できます。 「額」の問題を解決してみましょう。 人を強調し、チームを切り替えるようにしてください:
import React, { Component, PureComponent, Fragment } from "react"; import { render } from "react-dom"; import "./style.css"; class App extends Component { state = { active: 1, teams: [ { id: 1, name: "Amazing Team", developers: [ { id: 1, name: "" }, { id: 2, name: "" }, { id: 3, name: "" } ] }, { id: 2, name: "Another Team", developers: [ { id: 1, name: "" }, { id: 2, name: "" }, { id: 3, name: "" } ] } ] }; addTop = name => { this.setState(state => ({ teams: state.teams.map( team => team.id === state.active ? { ...team, developers: [ { id: team.developers.length + 1, name }, ...team.developers ] } : team ) })); }; addBottom = name => { this.setState(state => ({ teams: state.teams.map( team => team.id === state.active ? { ...team, developers: [ ...team.developers, { id: team.developers.length + 1, name } ] } : team ) })); }; toggle = id => { this.setState(state => ({ teams: state.teams.map( team => team.id === state.active ? { ...team, developers: team.developers.map( developer => developer.id === id ? { ...developer, highlighted: !developer.highlighted } : developer ) } : team ) })); }; switchTeam = id => { this.setState({ active: id }); }; render() { return ( <Fragment> <TeamsSwitcher onSwitch={this.switchTeam} teams={this.state.teams} /> <Users onClick={this.toggle} names={ this.state.teams.find(team => team.id === this.state.active) .developers } /> <AddName addTop={this.addTop} addBottom={this.addBottom} /> </Fragment> ); } } class TeamsSwitcher extends PureComponent { render() { return ( <ul> {this.props.teams.map(team => ( <li onClick={() => { this.props.onSwitch(team.id); }} key={team.id} > {team.name} </li> ))} </ul> ); } } class AddName extends PureComponent { getInput = el => { this.input = el; }; addToTop = () => { if (!this.input.value.trim()) { return; } this.props.addTop(this.input.value); this.input.value = ""; }; addToBottom = () => { if (!this.input.value.trim()) { return; } this.props.addBottom(this.input.value); this.input.value = ""; }; render() { return ( <Fragment> <input ref={this.getInput} /> <button onClick={this.addToTop}>Add to TOP</button> <button onClick={this.addToBottom}>Add to BOTTOM</button> </Fragment> ); } } class Users extends PureComponent { render() { return ( <ul> {this.props.names.map(user => ( <Name id={user.id} onClick={this.props.onClick} highlighted={user.highlighted} key={user.id} > {user.name} </Name> ))} </ul> ); } } class Name extends PureComponent { render() { return ( <li className={this.props.highlighted ? "highlight" : ""} onClick={() => this.props.onClick(this.props.id)} > {this.props.children} </li> ); } } render(<App />, document.getElementById("root"));
不快な機能に注意してください。人を選択してからチームを切り替えると、選択がアニメーション化されますが、他のチームの人が強調表示されることはありませんでした。 ビデオの印象的な例は次のとおりです。
不要なキーを再利用すると、副作用が発生する可能性があります。新しいコンポーネントを削除して作成するのではなく、reactが更新されるためです。
これは、異なるユーザーに同じキーを使用したために発生します。 したがって、この例では必須ではありませんが、試薬は要素を使用します。 さらに、新しい人を追加すると複雑なコードが生成されます。
上記のコードにはいくつかの問題があります。
- データは正規化されておらず、それらの処理は複雑です。
- 開発者エンティティとのキーの重複があります。そのため、リアクションはコンポーネントを再作成せず、更新します。 これは副作用につながります。
問題を解決するには2つの方法があります。 簡単な解決策は、開発者用の複合キーを${id }.${id }
の形式で作成することです。これにより、キーを横切って副作用をなくすことができます。
ただし、データを正規化し、エンティティを結合することにより、問題を包括的に解決できます。 したがって、状態コンポーネントには、 teams
、 developers
2つのフィールドがあります。 developers
にはid + name
マップが含まれ、 teams
にはteams
する開発者のリストが含まれます。 このソリューションを実装します。
class App extends Component { state = { active: 1, nextId: 3, developers: { "1": { name: "" }, "2": { name: "" }, }, teams: [ { id: 1, name: "Amazing Team", developers: [1] }, { id: 2, name: "Another Team", developers: [2] } ] }; addTop = name => {...}; addBottom = name => {...} toggle = id => { this.setState(state => ({ developers: { ...state.developers, [id]: { ...state.developers[id], highlighted: !state.developers[id].highlighted } } })); }; switchTeam = id => {...}; render() { // computed value state return ( <Fragment> <TeamsSwitcher onSwitch={this.switchTeam} teams={this.state.teams} /> <Users onClick={this.toggle} users={this.state.teams .find(team => team.id === this.state.active) .developers.map(id => ({ id, ...this.state.developers[id] }))} /> <AddName addTop={this.addTop} addBottom={this.addBottom} /> </Fragment> ); } }
import React, { Component, PureComponent, Fragment } from "react"; import { render } from "react-dom"; import "./style.css"; class App extends Component { state = { active: 1, nextId: 7, developers: { "1": { name: "" }, "2": { name: "" }, "3": { name: "" }, "4": { name: "" }, "5": { name: "" }, "6": { name: "" } }, teams: [ { id: 1, name: "Amazing Team", developers: [1, 2, 3] }, { id: 2, name: "Another Team", developers: [4, 5, 6] } ] }; addTop = name => { this.setState(state => ({ developers: { ...state.developers, [state.nextId]: { name } }, nextId: state.nextId + 1, teams: state.teams.map( team => team.id === state.active ? { ...team, developers: [state.nextId, ...team.developers] } : team ) })); }; addBottom = name => { this.setState(state => ({ // developers nextId , developers: { ...state.developers, [state.nextId]: { name } }, nextId: state.nextId + 1, teams: state.teams.map( team => team.id === state.active ? { ...team, developers: [...team.developers, state.nextId] } : team ) })); }; toggle = id => { this.setState(state => ({ developers: { ...state.developers, [id]: { ...state.developers[id], highlighted: !state.developers[id].highlighted } } })); }; switchTeam = id => { this.setState({ active: id }); }; render() { // computed value state return ( <Fragment> <TeamsSwitcher onSwitch={this.switchTeam} teams={this.state.teams} /> <Users onClick={this.toggle} users={this.state.teams .find(team => team.id === this.state.active) .developers.map(id => ({ id, ...this.state.developers[id] }))} /> <AddName addTop={this.addTop} addBottom={this.addBottom} /> </Fragment> ); } } class TeamsSwitcher extends PureComponent { render() { return ( <ul> {this.props.teams.map(team => ( <li onClick={() => { this.props.onSwitch(team.id); }} key={team.id} > {team.name} </li> ))} </ul> ); } } class AddName extends PureComponent { getInput = el => { this.input = el; }; addToTop = () => { if (!this.input.value.trim()) { return; } this.props.addTop(this.input.value); this.input.value = ""; }; addToBottom = () => { if (!this.input.value.trim()) { return; } this.props.addBottom(this.input.value); this.input.value = ""; }; render() { return ( <Fragment> <input ref={this.getInput} /> <button onClick={this.addToTop}>Add to TOP</button> <button onClick={this.addToBottom}>Add to BOTTOM</button> </Fragment> ); } } class Users extends PureComponent { render() { return ( <ul> {this.props.users.map(user => ( <Name id={user.id} onClick={this.props.onClick} highlighted={user.highlighted} key={user.id} > {user.name} </Name> ))} </ul> ); } } class Name extends PureComponent { render() { return ( <li className={this.props.highlighted ? "highlight" : ""} onClick={() => this.props.onClick(this.props.id)} > {this.props.children} </li> ); } } render(<App />, document.getElementById("root"));
これで、要素が正しく処理されます。
データの正規化により、アプリケーションデータレイヤーとの対話が簡素化され、構造が単純化され、複雑さが軽減されます。 たとえば、トグル関数を正規化されたデータと正規化されていないデータと比較します。
ヒント :バックエンドまたはAPIが正規化されていない形式でデータを提供する場合、 https://github.com/paularmstrong/normalizrで正規化できます
2番目の部分を要約するには:
キーを使用する場合、データを変更するときはキーを変更する必要があることを理解することが重要です。 レビュー中に発生したエラーの鮮明な例は、配列内の要素のインデックスをkey
として使用することです。 これにより、人々のリストを強調表示して表示する例として見たような副作用が生じます。
データおよび/または複合key
正規化により、目的の効果を実現できます。
- エンティティが変更されると、データが更新されます(たとえば、強調表示されたり、変更されたりします)。
- 指定された
key
持つ要素がもう存在しない場合、古いインスタンスを削除します。 - 必要なときに新しい要素を作成します。
単一のアイテムをレンダリングするときにキーを使用する
説明したように、 key
がない場合の反応では、古いツリーと新しいツリーの要素をペアで比較します。 キーがある場合は、指定されたキーを持つ目的の要素をchildren
リストで検索します。 children
が1つの要素のみで構成される場合は、ルールの例外ではありません。
別の例を見てみましょう-通知。 特定の期間に通知が1つだけ存在し、数秒間表示されて消えるとします。 このような通知を実装するのは簡単です。カウンタの最後にあるcomponentDidMount
に従ってカウンタを設定するcomponentDidMount
は、通知の非表示をアニメーション化します。次に例を示します。
class Notification1 extends PureComponent { componentDidMount() { setTimeout(() => { this.element && this.element.classList.add("notification_hide"); }, 3000); } render() { return ( <div ref={el => (this.element = el)} className="notification"> {this.props.children} </div> ); } }
はい、このコンポーネントにはフィードバックがありませんonClose
は発生しませんが、このタスクには重要ではありません。
シンプルなコンポーネントができました。
状況を想像してください-ボタンをクリックすると、同様の通知が表示されます。 ユーザーは停止せずにボタンをクリックしますが、3秒の通知の後、notification_hideクラスが追加され、ユーザーには見えなくなります( key
使用しなかった場合)。
key
を使用せずにコンポーネントの動作を修正するには、lifeCycleメソッドを使用して正しく更新されるNotification2
クラスを作成します。
class Notification2 extends PureComponent { componentDidMount() { this.subscribeTimeout(); } componentWillReceiveProps(nextProps) { if (nextProps.children !== this.props.children) { clearTimeout(this.timeout); } } componentDidUpdate(prevProps) { if (prevProps.children !== this.props.children) { this.element.classList.remove("notification_hide"); this.subscribeTimeout(); } } subscribeTimeout() { this.timeout = setTimeout(() => { this.element.classList.add("notification_hide"); }, 3000); } render() { return ( <div ref={el => (this.element = el)} className="notification"> {this.props.children} </div> ); } }
ここでは、通知コンテンツが変更された場合にタイムアウトを再開し、データを更新するときにnotification_hide
クラスを削除するコードをさらに取得しました。
ただし、最初のコンポーネントNotification1
と属性key
を使用して、問題を解決できkey
。 各通知には独自の一意のid
、これをkey
として使用しkey
。 通知が変更されたときにkey
が変更された場合、 Notification1
が再作成されます。 コンポーネントは、必要なビジネスロジックに対応します。
このように
まれに、単一のコンポーネントをレンダリングするときにkey
を使用することが正当化されます。 key
は、調整メカニズムがコンポーネントを比較する必要があるか、新しいコンポーネントを(すぐに)作成する価値があるかを理解するための非常に強力な方法です。
子に渡すときにキーを操作する
key
の興味深い機能は、コンポーネント自体では使用できないことです。 これは、 key
がspecial prop
ためです。 Reactには、 key
とref
2つの特別なprops
があります。
class TestKey extends Component { render() { // null console.log(this.props.key); // div return <div>{this.props.key}</div>; } } const App = () => ( <div> <TestKey key="123" /> </div> );
さらに、コンソールには警告が表示されます。
警告:TestKey:key
は支柱ではありません。 アクセスしようとすると、undefined
が返されます。 子コンポーネント内で同じ値にアクセスする必要がある場合は、別のプロパティとして渡す必要があります。 ( https://fb.me/react-special-props )
ただし、 key
持つ子がコンポーネントに渡された場合、それらと対話できますが、 key
フィールドはprops
オブジェクト内ではなく、コンポーネントレベルにあります。
class TestKey extends Component { render() { console.log(this.props.key); return <div>{this.props.key}</div>; } } class TestChildrenKeys extends Component { render() { React.Children.forEach(this.props.children, child => { // key child, . // , // key // prop console.log(child.key); // props, key ref props console.log(child.props.a); }); return this.props.children; } } const App = () => ( <div> <TestChildrenKeys> <TestKey a="prop1" key="1" /> <TestKey a="prop2" key="2" /> <TestKey a="prop3" key="3" /> <TestKey a="prop10" key="10" /> </TestChildrenKeys> </div> );
コンソールは以下を出力します:
1
prop1
2
prop2
3
prop3
10
小道具10
要約すると:
key
とref
は、reactのspecial props
です。 それらはpropsオブジェクトには含まれず、コンポーネント自体の内部では利用できません。
children
渡された親コンポーネントからchild.key
またはchild.ref
アクセスできますが、これを行う必要はありません。 これが必要な状況はほとんどありません。 いつでも問題をより簡単に、より良く解決できます。 コンポーネントでの処理にkey
必要な場合key
、たとえばprop id
key
複製します。
キーの範囲、コンポーネントに渡される方法、キーの有無に応じて調整メカニズムがどのように変化するかを調べました。 また、唯一の子である要素に対するキーの使用も検討しました。 最後に、主なポイントをグループ化します。
key
ないkey
、reconciliation
メカニズムは、現在のVDOMと新しいVDOMの間でコンポーネントをペアでチェックします。 このため、インターフェイスの不必要な再描画が多数発生し、アプリケーションの速度が低下します。
key
追加することでkey
ペアのkey
比較せず、同じkey
(タグ/コンポーネント名が考慮される)でコンポーネントを検索することでreconciliation
メカニズムを支援します-これにより、インターフェースの再描画の回数が減ります。 前のツリーで変更された/発生しなかった要素のみが更新/追加されます。
重複
key
が表示されないようにしてください。表示を切り替えるときに、新しいデータがキーと一致しません。 これにより、アニメーションなどの不要な副作用や、不適切な要素の動作ロジックが発生する可能性があります。
まれに、
key
が1つの要素にも使用されます。 これにより、コードのサイズと理解が減少します。 しかし、このアプローチの範囲は限られています。
-
key
とref
は特別な小道具です。 それらはコンポーネントでは利用できず、child.props
利用できません。child.key
を介して親にアクセスできますが、実際にはこれに対する実際のアプリケーションはありません。 子コンポーネントにkey
が必要key
場合、正しい解決策は、たとえばprop id
で複製することです。