陷阱

使用 Children 並不常見,而且可能導致程式碼脆弱。 參考常見的替代方案

Children 允許您操作和轉換您收到的 JSX 作為 children 屬性

const mappedChildren = Children.map(children, child =>
<div className="Row">
{child}
</div>
);

參考

Children.count(children)

呼叫 Children.count(children) 來計算 children 資料結構中的子元素數量。

import { Children } from 'react';

function RowList({ children }) {
return (
<>
<h1>Total rows: {Children.count(children)}</h1>
...
</>
);
}

請參考以下更多範例。

參數

回傳值

這些 children 內的節點數量。

注意事項

  • 空節點(nullundefined 和布林值)、字串、數字和 React 元素 都算作個別節點。陣列不算作個別節點,但它們的子節點會被計算。遍歷不會深入 React 元素:它們不會被渲染,它們的子節點也不會被遍歷。Fragments 不會被遍歷。

Children.forEach(children, fn, thisArg?)

呼叫 Children.forEach(children, fn, thisArg?) 來對 children 資料結構中的每個子節點執行程式碼。

import { Children } from 'react';

function SeparatorList({ children }) {
const result = [];
Children.forEach(children, (child, index) => {
result.push(child);
result.push(<hr key={index} />);
});
// ...

請參考以下更多範例。

參數

  • children:您的元件收到的 children 屬性 的值。
  • fn:您想要針對每個子節點執行的函式,類似於 陣列的 forEach 方法 的回呼函式。它會以子節點作為第一個參數,子節點的索引作為第二個參數被呼叫。索引從 0 開始,每次呼叫遞增。
  • 選用 thisArg:呼叫 fn 函式時所使用的 this。如果省略,則為 undefined

回傳值

Children.forEach 回傳 undefined

注意事項

  • 空節點(nullundefined 和布林值)、字串、數字和 React 元素 都算作個別節點。陣列不算作個別節點,但它們的子節點會被計算。遍歷不會深入 React 元素:它們不會被渲染,它們的子節點也不會被遍歷。Fragments 不會被遍歷。

Children.map(children, fn, thisArg?)

呼叫 Children.map(children, fn, thisArg?) 來映射或轉換 children 資料結構中的每個子節點。

import { Children } from 'react';

function RowList({ children }) {
return (
<div className="RowList">
{Children.map(children, child =>
<div className="Row">
{child}
</div>
)}
</div>
);
}

請參考以下更多範例。

參數

  • children:您的元件收到的 children 屬性 的值。
  • fn:映射函式,類似於 陣列的 map 方法 的回呼函式。它會以子節點作為第一個參數,子節點的索引作為第二個參數被呼叫。索引從 0 開始,每次呼叫遞增。您需要從這個函式返回一個 React 節點。這可以是一個空節點(nullundefined 或布林值)、字串、數字、React 元素或其他 React 節點的陣列。
  • 選用 thisArg:呼叫 fn 函式時所使用的 this。如果省略,則為 undefined

回傳值

如果 childrennullundefined,則會傳回相同的值。

否則,會傳回一個扁平陣列,其中包含您從 fn 函式傳回的節點。傳回的陣列將包含您傳回的所有節點,但 nullundefined 除外。

注意事項

  • 空節點(nullundefined 和布林值)、字串、數字和 React 元素 都算作個別節點。陣列不算作個別節點,但它們的子節點會被計算。遍歷不會深入 React 元素:它們不會被渲染,它們的子節點也不會被遍歷。Fragments 不會被遍歷。

  • 如果您從 fn 傳回一個元素或一個帶有鍵值的元素陣列,**則傳回元素的鍵值將會自動與 children 中對應原始項目的鍵值合併。** 當您從 fn 在陣列中傳回多個元素時,它們的鍵值只需要在彼此之間局部唯一即可。


Children.only(children)

呼叫 Children.only(children) 來斷言 children 代表單個 React 元素。

function Box({ children }) {
const element = Children.only(children);
// ...

參數

傳回值

如果 children 是一個有效的元素,則傳回該元素。

否則,拋出錯誤。

注意事項

  • **如果您傳遞一個陣列(例如 Children.map 的傳回值)作為 children,此方法一律會拋出錯誤。** 換句話說,它強制規定 children 是單個 React 元素,而不是只有一個元素的陣列。

Children.toArray(children)

呼叫 Children.toArray(children)children 資料結構建立成陣列。

import { Children } from 'react';

export default function ReversedList({ children }) {
const result = Children.toArray(children);
result.reverse();
// ...

參數

傳回值

傳回 children 中元素的扁平陣列。

注意事項

  • 空節點(nullundefined 和布林值)將會在回傳的陣列中被省略。回傳元素的鍵值將會根據原始元素的鍵值、它們的巢狀層級和位置來計算。這確保了扁平化陣列不會導致行為上的改變。

用法

轉換子元素

要轉換你的元件children 屬性接收到的子元素 JSX,請呼叫 Children.map

import { Children } from 'react';

function RowList({ children }) {
return (
<div className="RowList">
{Children.map(children, child =>
<div className="Row">
{child}
</div>
)}
</div>
);
}

在上面的例子中,RowList 將它接收到的每個子元素都包裝到一個 <div className="Row"> 容器中。例如,假設父元件傳遞三個 <p> 標籤作為 children 屬性給 RowList

<RowList>
<p>This is the first item.</p>
<p>This is the second item.</p>
<p>This is the third item.</p>
</RowList>

那麼,使用上面的 RowList 實作,最終渲染的結果將會如下所示

<div className="RowList">
<div className="Row">
<p>This is the first item.</p>
</div>
<div className="Row">
<p>This is the second item.</p>
</div>
<div className="Row">
<p>This is the third item.</p>
</div>
</div>

Children.map 類似於使用 map() 轉換陣列。不同之處在於 children 資料結構被視為*不透明的。*這表示即使它有時是一個陣列,您也不應該假設它是一個陣列或任何其他特定資料類型。這就是為什麼如果您需要轉換它,您應該使用 Children.map

import { Children } from 'react';

export default function RowList({ children }) {
  return (
    <div className="RowList">
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}

深入探討

為什麼 children 屬性不總是一個陣列?

在 React 中,children 屬性被視為一個*不透明的*資料結構。這表示您不應該依賴它的結構方式。要轉換、過濾或計算子元素,您應該使用 Children 方法。

實際上,children 資料結構在內部通常以陣列表示。但是,如果只有一個子元素,則 React 不會建立額外的陣列,因為這會導致不必要的記憶體開銷。只要您使用 Children 方法而不是直接檢視 children 屬性,即使 React 改變了資料結構的實際執行方式,您的程式碼也不會出錯。

即使 children 是一個陣列,Children.map 仍然具有 hữu ích 的特殊行為。例如,Children.map 會將回傳元素上的鍵值與您傳遞給它的 children 上的鍵值組合起來。這確保了原始的 JSX 子元素即使像上例中那樣被包裝起來也不會「遺失」鍵值。

陷阱

children 資料結構不包含您以 JSX 形式傳遞的元件**的渲染輸出**。在下面的例子中,RowList 接收到的 children 只包含兩個項目而不是三個

  1. <p>這是第一個項目。</p>
  2. <MoreRows />

這就是為什麼在此範例中只產生兩個列包裝器的原因

import RowList from './RowList.js';

export default function App() {
  return (
    <RowList>
      <p>This is the first item.</p>
      <MoreRows />
    </RowList>
  );
}

function MoreRows() {
  return (
    <>
      <p>This is the second item.</p>
      <p>This is the third item.</p>
    </>
  );
}

在操作 children 時,無法取得內部元件(例如 <MoreRows />)的渲染輸出。這就是為什麼通常最好使用其他解決方案之一


為每個子項運行一些程式碼

呼叫 Children.forEach 來迭代 `children` 資料結構中的每個子元素。它不回傳任何值,類似於 陣列的 forEach 方法。您可以使用它來執行自定義邏輯,例如構建您自己的陣列。

import { Children } from 'react';

export default function SeparatorList({ children }) {
  const result = [];
  Children.forEach(children, (child, index) => {
    result.push(child);
    result.push(<hr key={index} />);
  });
  result.pop(); // Remove the last separator
  return result;
}

陷阱

如前所述,在操作 children 時,無法取得內部元件的渲染輸出。這就是為什麼 通常最好使用替代方案之一 的原因。


計算子元素數量

呼叫 Children.count(children) 來計算子元素的數量。

import { Children } from 'react';

export default function RowList({ children }) {
  return (
    <div className="RowList">
      <h1 className="RowListHeader">
        Total rows: {Children.count(children)}
      </h1>
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}

陷阱

如前所述,在操作 children 時,無法取得內部元件的渲染輸出。這就是為什麼 通常最好使用替代方案之一 的原因。


將子元素轉換為陣列

呼叫 Children.toArray(children) 將 `children` 資料結構轉換為普通的 JavaScript 陣列。這讓您可以使用內建的陣列方法(例如 filtersortreverse)來操作陣列。

import { Children } from 'react';

export default function ReversedList({ children }) {
  const result = Children.toArray(children);
  result.reverse();
  return result;
}

陷阱

如前所述,在操作 children 時,無法取得內部元件的渲染輸出。這就是為什麼 通常最好使用替代方案之一 的原因。


替代方案

注意事項

本節說明 Children API(首字母大寫 `C`)的替代方案,該 API 的導入方式如下:

import { Children } from 'react';

不要將其與 使用 children 屬性(首字母小寫 `c`)混淆,使用 `children` 屬性是良好且鼓勵的做法。

公開多個元件

使用 `Children` 方法操作子元素通常會導致程式碼脆弱。當您在 JSX 中將子元素傳遞給元件時,通常不希望元件會操作或轉換個別的子元素。

盡可能避免使用 `Children` 方法。例如,如果您希望 RowList 的每個子元素都被 <div className="Row"> 包裹,請匯出一個 Row 元件,並手動將每一列都包裹在其中,如下所示:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList>
      <Row>
        <p>This is the first item.</p>
      </Row>
      <Row>
        <p>This is the second item.</p>
      </Row>
      <Row>
        <p>This is the third item.</p>
      </Row>
    </RowList>
  );
}

與使用 Children.map 不同,這種方法不會自動包裹每個子元素。**然而,與先前使用 Children.map 的範例相比,這種方法有一個顯著的優點,即使您繼續提取更多元件,它仍然有效。** 例如,如果您提取自己的 MoreRows 元件,它仍然有效:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList>
      <Row>
        <p>This is the first item.</p>
      </Row>
      <MoreRows />
    </RowList>
  );
}

function MoreRows() {
  return (
    <>
      <Row>
        <p>This is the second item.</p>
      </Row>
      <Row>
        <p>This is the third item.</p>
      </Row>
    </>
  );
}

這不適用於 Children.map,因為它會將 <MoreRows /> 視為單個子元素(以及單個列)。


接受物件陣列作為屬性

您也可以明確地將陣列作為屬性傳遞。例如,這個 RowList 接受一個 rows 陣列作為屬性:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList rows={[
      { id: 'first', content: <p>This is the first item.</p> },
      { id: 'second', content: <p>This is the second item.</p> },
      { id: 'third', content: <p>This is the third item.</p> }
    ]} />
  );
}

由於 rows 是一個普通的 JavaScript 陣列,RowList 元件可以使用內建的陣列方法(例如 map)來操作它。

當您想要將更多資訊作為結構化資料與子元件一起傳遞時,這個模式特別有用。在以下範例中,TabSwitcher 元件接收一個物件陣列作為 tabs 屬性。

import TabSwitcher from './TabSwitcher.js';

export default function App() {
  return (
    <TabSwitcher tabs={[
      {
        id: 'first',
        header: 'First',
        content: <p>This is the first item.</p>
      },
      {
        id: 'second',
        header: 'Second',
        content: <p>This is the second item.</p>
      },
      {
        id: 'third',
        header: 'Third',
        content: <p>This is the third item.</p>
      }
    ]} />
  );
}

與將子元件作為 JSX 傳遞不同,這種方法允許您將一些額外資料(例如 header)與每個項目關聯。因為您直接使用 tabs,而且它是一個陣列,所以您不需要 Children 方法。


呼叫渲染屬性來自訂渲染

除了為每個項目產生 JSX 之外,您也可以傳遞一個返回 JSX 的函式,並在必要時呼叫該函式。在此範例中,App 元件將一個 renderContent 函式傳遞給 TabSwitcher 元件。TabSwitcher 元件僅針對選定的索引標籤呼叫 renderContent

import TabSwitcher from './TabSwitcher.js';

export default function App() {
  return (
    <TabSwitcher
      tabIds={['first', 'second', 'third']}
      getHeader={tabId => {
        return tabId[0].toUpperCase() + tabId.slice(1);
      }}
      renderContent={tabId => {
        return <p>This is the {tabId} item.</p>;
      }}
    />
  );
}

renderContent 這樣的屬性被稱為「*渲染屬性*」,因為它是一個指定如何渲染使用者介面一部分的屬性。然而,它並沒有什麼特別之處:它只是一個恰好是函式的普通屬性。

渲染屬性是函式,因此您可以將資訊傳遞給它們。例如,這個 RowList 元件將每列的 idindex 傳遞給 renderRow 渲染屬性,該屬性使用 index 來突顯偶數列。

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList
      rowIds={['first', 'second', 'third']}
      renderRow={(id, index) => {
        return (
          <Row isHighlighted={index % 2 === 0}>
            <p>This is the {id} item.</p>
          </Row> 
        );
      }}
    />
  );
}

這是父元件和子元件如何在不操作子元件的情況下進行協作的另一個例子。


疑難排解

我傳遞了一個自訂元件,但 Children 方法沒有顯示其渲染結果

假設您將兩個子元件傳遞給 RowList,如下所示:

<RowList>
<p>First item</p>
<MoreRows />
</RowList>

如果您在 RowList 內執行 Children.count(children),您將得到 2。即使 MoreRows 渲染 10 個不同的項目,或者它返回 nullChildren.count(children) 仍然是 2。從 RowList 的角度來看,它只「看到」它接收到的 JSX。它沒有「看到」 MoreRows 元件的內部。

這個限制使得提取元件變得困難。這就是為什麼使用 替代方案 比使用 Children 更受歡迎的原因。