React-Hooks 复合组件#
复合组件是你有兩個或更多的組件一起工作來完成一項有用的任務。通常,其中一個組件是父級,另一個是子級。目標是提供更豐富表達性和靈活性的 API。
把它想象成<select>
和<option>
:
<select>
<option value="value1">key1</option>
<option value="value2">key2</option>
<option value="value3">key3</option>
</select>
如果你試圖只用其中一個而不使用另一個,它就不會工作(或者沒有意義)。此外,這實際上是一個非常出色的 API。讓我們看看如果我們沒有組合組件 API 來使用時會是什麼樣子(請記住,這是 HTML 而不是 JSX)
<select options="key1:value1;key2:value2;key3:value3"></select>
我相信你可以想出其他表達這個意思的方式,但是太惡心了。那麼用這種 API 如何表達 disabled 屬性呢?有點瘋狂。
所以,複合組件 API 為您提供了一種表達組件之間關係的好方法。
另一個重要方面是 “隱式狀態” 的概念。<select>
元素會隱式存儲有關所選選項的狀態,並將該狀態與它的子元素分享,以便它們根據該狀態來呈現自身。但這種共享是隱式的,因為我們的 HTML 代碼中甚至無法訪問此狀態(而且也不需要)。
好的,讓我們看一下一個合法的 React 組件,它暴露了複合組件來進一步理解這些原則。這裡是 Reach UI 中<Menu />
組件的例子,它暴露了複合組件 API:
function App() {
return (
<Menu>
<MenuButton>
Actions <span aria-hidden>▾</span>
</MenuButton>
<MenuList>
<MenuItem onSelect={() => alert('Download')}>Download</MenuItem>
<MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem>
<MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem>
</MenuList>
</Menu>
)
}
在這個例子中,<Menu>
建立了一些共享的隱式狀態。 <MenuButton>
、<MenuList>
和<MenuItem>
組件都可以訪問或操作該狀態,而且都是隱式實現的。這樣就可以提供你想要的表達 API。
那麼這是如何做到的呢?好吧,如果你觀看我的課程,我會向你展示兩種方法來實現。一個使用 React.cloneElement 在子元素上,另一個使用 React context。 (我的課程需要略微更新以顯示如何使用 hooks 來實現此目的)。在本博客文章中,我將向您展示如何使用 context 創建一套簡單的複合部件。
在教授一個新的概念時,我喜歡先使用簡單的例子。所以我們將使用我最喜歡的<Toggle>
組件例子來進行講解。
這是我們如何使用<Toggle>
複合組件的方式:
function App() {
return (
<Toggle onToggle={(on) => console.log(on)}>
<ToggleOn>The button is on</ToggleOn>
<ToggleOff>The button is off</ToggleOff>
<ToggleButton />
</Toggle>
)
}
好的,你們都在等待的時刻到了,實際上使用上下文和掛鉤實現複合組件的全部代碼。
import * as React from 'react'
// this switch implements a checkbox input and is not relevant for this example
import { Switch } from '../switch'
const ToggleContext = React.createContext()
function useEffectAfterMount(cb, dependencies) {
const justMounted = React.useRef(true)
React.useEffect(() => {
if (!justMounted.current) {
return cb()
}
justMounted.current = false
}, dependencies)
}
function Toggle(props) {
const [on, setOn] = React.useState(false)
const toggle = React.useCallback(() => setOn((oldOn) => !oldOn), [])
useEffectAfterMount(() => {
props.onToggle(on)
}, [on])
const value = React.useMemo(() => ({ on, toggle }), [on])
return <ToggleContext.Provider value={value}>{props.children}</ToggleContext.Provider>
}
function useToggleContext() {
const context = React.useContext(ToggleContext)
if (!context) {
throw new Error(`Toggle compound components cannot be rendered outside the Toggle component`)
}
return context
}
function ToggleOn({ children }) {
const { on } = useToggleContext()
return on ? children : null
}
function ToggleOff({ children }) {
const { on } = useToggleContext()
return on ? null : children
}
function ToggleButton(props) {
const { on, toggle } = useToggleContext()
return <Switch on={on} onClick={toggle} {...props} />
}
所以這個工作原理是我們使用 React 創建一個上下文,在其中存儲狀態和更新狀態的機制。然後<Toggle>
組件負責將該上下文值提供給 React 樹的其餘部分。