Function as Child Component

「Function as Child Component」是一種在 React 中常見的設計模式,也常被稱為 Render Props 模式的一種變體。

它的核心概念是:把一個 function 當作子元素(children)傳給元件,這個 function 會在元件內部被呼叫,並且可以取得元件內部的狀態或資料,然後回傳你想要渲染的內容。

這個模式的語法長這樣:

<SomeComponent>{(data) => <div>{data.value}</div>}</SomeComponent>
  • 這裡的 children 不是一般的 JSX,而是一個 function。
  • SomeComponent 會在內部呼叫這個 function,並把自己的狀態或資料(如 data)當參數傳進去。

這種模式的好處

  • 高度彈性:父元件可以完全決定要怎麼渲染內容,子元件只負責提供資料或狀態。
  • 邏輯與 UI 分離:元件可以專注在「邏輯」或「資料提供」,而 UI 交給外部決定。
  • 可重用性高:同一個元件可以被不同的 children function 以不同方式渲染。

使用情境舉例

提到共用,有時候我們遇到的情境是,不同元件需要反應相同的狀態

為了讓輸入元件能夠和其他元件共享其狀態,必須把狀態向上移動到需要他的那個元件最靠近的共同祖先,這就是「提升狀態 Lifting state up」。

在小型應用程序中,只有幾個元件的情況下,我們可以避免使用像 Redux 或 React Context 這樣的狀態管理庫,而改用這種模式,將狀態提升到最接近的共同祖先元件中。

以溫度單位轉換為例
像是下圖這種溫度單位轉換的應用程式,就是一個例子:

https://www.digikey.tw/zh/resources/conversion-calculators/conversion-calculator-temperature

// 輸入攝氏溫度,立即反應相對應的華氏度(Fahrenheit)和克耳文(Kelvin, K)溫度
 
function App() {
  const [value, setValue] = useState("");
 
  return (
    <div className="App">
      <h1>Temperature Converter</h1>
      <Input value={value} handleChange={setValue} />
      <Kelvin value={value} />
      <Fahrenheit value={value} />
    </div>
  );
}

儘管這是一種有效的解決方案,但在具有處理多個子元件的大型應用程序中,「提升狀態」可能會變得複雜。每次狀態變化都可能導致重新渲染所有子元件,甚至那些不處理數據的子元件,這可能對應用程序的性能產生負面影響。

以 Render Props 來解決

function App() {
  return (
    <div className="App">
      <h1>Temperature Converter</h1>
      <Input
        render={(value) => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      />
    </div>
  );
}

我們可以使用 Render Props 模式來解決這個問題。我們將改變 Input 元件,使其能夠接收 render props 能夠幫助我們有效的縮小、限制 state 狀態改變之後所影響的範圍

以 Function as Child Component 來解決

function App() {
  return (
    <div className="App">
      <h1>Temperature Converter</h1>
      <Input>
        {(value) => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      </Input>
    </div>
  );
}

我們還可以將函數作為子元件傳遞給 React 元件。這個函數通過 children 屬性傳遞給我們,從技術上來說,它也是一個 render props。

這樣,Kelvin 和 Fahrenheit 就能夠直接存取 value,而不用擔心 render props 的命名。

生活中常見的 Function as Child Component

Formik

Formik 是一個表單管理函式庫,可以管理表單中的數據,進行輸入驗證、錯誤提示、表單提交...等等。它的 <Formik> 元件就支援 Function as Child Component。你可以直接在 children 傳一個 function,Formik 會把表單狀態與操作方法傳進來。

import { Formik, Form, Field } from "formik";
 
<Formik
  initialValues={{ email: "" }}
  onSubmit={(values) => {
    console.log(values);
  }}
>
  {({ handleSubmit, values }) => (
    <Form onSubmit={handleSubmit}>
      <Field name="email" />
      <button type="submit">Submit</button>
      <div>目前輸入:{values.email}</div>
    </Form>
  )}
</Formik>;

這裡 <Formik> 的 children 是一個 function,Formik 會把表單的狀態(如 values)和操作方法(如 handleSubmit)傳進來,讓你可以完全自訂 UI 和行為。

react-transition-group

<Transition> 元件可以用 function as child 來取得動畫狀態,根據不同狀態渲染不同內容。

import { Transition } from "react-transition-group";
 
<Transition in={true} timeout={300}>
  {(state) => <div>現在動畫狀態:{state}</div>}
</Transition>;

這裡 <Transition> 的 children 是一個 function,會收到目前的動畫狀態(如 entering、entered、exiting、exited),你可以根據 state 動態渲染內容或加上不同 class。

react-apollo

Apollo Client 的 <Query> 元件(舊版)也支援 function as child,會把查詢結果傳進來。

import { Query } from "@apollo/client/react/components";
 
<Query query={GET_USER}>
  {({ loading, error, data }) => {
    if (loading) return <p>Loading...</p>;
    if (error) return <p>Error!</p>;
    return <div>使用者名稱:{data.user.name}</div>;
  }}
</Query>;

這裡 <Query> 的 children 是一個 function,Apollo 會把 loading、error、data 等查詢狀態傳進來,讓你可以根據不同狀態渲染不同內容。

這三個例子都展現了 Function as Child Component 的彈性與威力: 元件本身只負責邏輯和資料,UI 交給外部 function 決定,讓元件更通用、更容易擴充!

優點

  • 使用 render props 模式可以輕鬆地在多個元件之間共享邏輯和數據。通過使用 render props 或子元件屬性,可以使元件非常可重用。
  • 跟 HOC 比起來,render props 能夠避免 HOC 會遇到的 naming collisions 問題(名稱衝突,合併屬性)。

缺點

  • 我們試圖透過 render props 解決的問題,在很大程度上已被 React Hooks 取代。隨著 Hooks 改變了我們向元件添加可重用性和數據共享的方式,它們在許多情況下可以替代 render props 模式。
  • 由於我們無法向 render props 添加生命週期方法,我們只能在不需要更改接收到的數據的元件上使用它。