reactjs / reactjs/react.dev

[DOCS] Add a clear explanation for the difference between children and [children]

オープン
#2,024 コメント 0 件 リアクション 1 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

主要言語
JavaScript
スター
11.8k
フォーク
7.9k
平均マージ
1日 11時間
マージ済み PR(30日)
11

説明

related issues (from the react repo): 1493 8731 15623
Most of these cases may summed up to this simple snippet:

class App extends React.Component {
  state = { condition: false };
  toggle = () => this.setState(({ condition }) => ({ condition: !condition }));
  renderToggleBtn = () => <button onClick={this.toggle}>toggle</button>;
  renderChildren = () => [1, 2].map(o => <Child key={o} id={o} />);

  render() {
    const { condition } = this.state;
    if (condition) {
      return (
        <div>
          <Parent>{this.renderChildren()}</Parent>
          {this.renderToggleBtn()}
        </div>
      );
    } else {
      return (
        <div>
          <Parent>
            {this.renderChildren()}
            <div>dummy</div>
          </Parent>
          {this.renderToggleBtn()}
        </div>
      );
    }
  }
}

On each change of the condition Boolean the Child components will get unmounted and mounted again, this obviously may lead to bugs or unexpected behavior (lost of state).

This happens because when react "sees" a single child as children it will ref to the child element itself (like mentioned on some of the issues i listed above) but when children is a multiple element list it will treat it as an array, since arrays can't have keys (they absolutely can't right?) react can't keep a reference to it, thus it will hit a different type in the reconciliation process hence we get a re-creation of the tree (I remember i read somewhere in the source code that Fragment without a key is treated the same way but can't find it now).

If we focus on the <Parent/> part of the tree,
on the 1st condition case we get an element that roughly looks like this:

{
  type: Parent,
  children: [
      {type: Child}, // <-- keep an eye for this item's type
      {type: Child},
  ]
};

And on the 2nd condition case we get an element that roughly looks like this:

{
  type: Parent,
  children: [
    [{type: Child},{type: Child}], // <-- keep an eye for this item's type
    {type: 'div', children: 'dummy'}
  ]
}

So we end up comparing the types of the first member of `children:

type Child -> type Array // different type, re-create the node tree

We need somehow to keep the same type or provide a consistent key to prevent the mismatch.
Some possible solutions could be:

  1. Wrapping the array of Child's with an element or a transparent keyed Fragment
renderChildren = () => (
  <React.Fragment key="dont-remount-pls">
    {[1, 2].map(o => (
      <Child key={o} id={o} />
    ))}
  </React.Fragment>
);

This way we kind of hacking and bypassing the limitation of passing a key to an array.

  1. Maybe a better and more readable solution is to use the && operator instead of if else which will create a "hole" in the tree for the <div>dummy</div> element and this will force children to always be a multiple elements list.
  render() {
    const { condition } = this.state;
    return (
      <div>
        <Parent>{this.renderChildren()}</Parent>
        {this.renderToggleBtn()}
        {condition && <div>dummy</div>}
      </div>
    );
  }

So we end up toggling between these 2 results:
When the condition is true

{
  type: Parent,
  children: [
    [{type: Child},{type: Child}], // <-- keep an eye for this item's type
    {type: 'div', children: 'dummy'}
  ]
}

When the condition is false

{
  type: Parent,
  children: [
    [{type: Child},{type: Child}], // <-- keep an eye for this item's type
    null
  ]
}

And in diffing:

type Array -> type Array // same type, do not re-create the node tree

while the question if this is considered a bug / abstraction leak or a design decision may be arguable, I think that we should at least document it clearly. I couldn't find any info regarding this behavior (except the issues posted above :point_up: and some digging in the source code).

Maybe its also good to call the returning JSX via if else as none best practice? (it may throw some hit at react but its better than a frustrated developer ), maybe even a lint rule?

If you think this should land on the official DOCS, I don't mind doing the PR but i may need some help with wording and i may also have some technical mistakes regarding on how i think it works as i described in this issue. 🙂

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

まず再現スニペットと関連する React の issue 1493、8731、15623 を確認し、その後、文面を作成する前に、説明されている children と [children] の挙動を検証します。公式ドキュメントが、その挙動、コンポーネントの状態への影響、および意図しない remount を避けるための推奨方法を明確に説明していれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
javascript, react
領域
documentation, frontend
issue の種類
ドキュメント
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
おおむね明確
初心者へのやさしさ
25/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。