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 添加生命週期方法,我們只能在不需要更改接收到的數據的元件上使用它。