React中的高階組件
React中的高階組件
高階組件HOC
即Higher Order Component
是React
中用於復用組件邏輯的一種高級技巧,HOC
自身不是React API
的一部分,它是一種基於React
的組合特性而形成的設計模式。
描述
高階組件從名字上就透漏出高級的氣息,實際上這個概念應該是源自於JavaScript
的高階函數,高階函數就是接受函數作為輸入或者輸出的函數,可以想到柯里化就是一種高階函數,同樣在React
文檔上也給出了高階組件的定義,高階組件是接收組件並返回新組件的函數。
A higher-order component is a function that takes a component and returns a new component.
具體而言,高階組件是參數為組件,返回值為新組件的函數,組件是將props
轉換為UI
,而高階組件是將組件轉換為另一個組件。HOC
在React
的第三方庫中很常見,例如Redux
的connect
和Relay
的createFragmentContainer
。
// 高階組件定義
const higherOrderComponent = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
// ...
render() {
return <WrappedComponent {...this.props} />;
}
};
}
// 普通組件定義
class WrappedComponent extends React.Component{
render(){
//....
}
}
// 返回被高階組件包裝過的增強組件
const EnhancedComponent = higherOrderComponent(WrappedComponent);
在這裡要注意,不要試圖以任何方式在HOC
中修改組件原型,而應該使用組合的方式,通過將組件包裝在容器組件中實現功能。通常情況下,實現高階組件的方式有以下兩種:
- 屬性代理
Props Proxy
。 - 反向繼承
Inheritance Inversion
。
屬性代理
例如我們可以為傳入的組件增加一個存儲中的id
屬性值,通過高階組件我們就可以為這個組件新增一個props
,當然我們也可以對在JSX
中的WrappedComponent
組件中props
進行操作,注意不是操作傳入的WrappedComponent
類,我們不應該直接修改傳入的組件,而可以在組合的過程中對其操作。
const HOC = (WrappedComponent, store) => {
return class EnhancedComponent extends React.Component {
render() {
const newProps = {
id: store.id
}
return <WrappedComponent
{...this.props}
{...newProps}
/>;
}
}
}
我們也可以利用高階組件將新組件的狀態裝入到被包裝組件中,例如我們可以使用高階組件將非受控組件轉化為受控組件。
class WrappedComponent extends React.Component {
render() {
return <input name="name" />;
}
}
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
constructor(props) {
super(props);
this.state = { name: "" };
}
render() {
const newProps = {
value: this.state.name,
onChange: e => this.setState({name: e.target.value}),
}
return <WrappedComponent
{...this.props}
{...newProps}
/>;
}
}
}
或者我們的目的是將其使用其他組件包裹起來用以達成布局或者是樣式的目的。
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
render() {
return (
<div class="layout">
<WrappedComponent {...this.props} />
</div>
);
}
}
}
反向繼承
反向繼承是指返回的組件去繼承之前的組件,在反向繼承中我們可以做非常多的操作,修改state
、props
甚至是翻轉Element Tree
,反向繼承有一個重要的點,反向繼承不能保證完整的子組件樹被解析,也就是說解析的元素樹中包含了組件(函數類型或者Class
類型),就不能再操作組件的子組件了。
當我們使用反向繼承實現高階組件的時候可以通過渲染劫持來控制渲染,具體是指我們可以有意識地控制WrappedComponent
的渲染過程,從而控制渲染控制的結果,例如我們可以根據部分參數去決定是否渲染組件。
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends WrappedComponent {
render() {
return this.props.isRender && super.render();
}
}
}
甚至我們可以通過重寫的方式劫持原組件的生命周期。
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends WrappedComponent {
componentDidMount(){
// ...
}
render() {
return super.render();
}
}
}
由於實際上是繼承關係,我們可以去讀取組件的props
和state
,如果有必要的話,甚至可以修改增加、修改和刪除props
和state
,當然前提是修改帶來的風險需要你自己來控制。在一些情況下,我們可能需要為高階屬性傳入一些參數,那我們就可以通過柯里化的形式傳入參數,配合高階組件可以完成對組件的類似於閉包的操作。
const HOCFactoryFactory = (params) => {
// 此處操作params
return (WrappedComponent) => {
return class EnhancedComponent extends WrappedComponent {
render() {
return params.isRender && this.props.isRender && super.render();
}
}
}
}
HOC與Mixin
使用Mixin
與HOC
都可以用於解決橫切關注點相關的問題。
Mixin
是一種混入的模式,在實際使用中Mixin
的作用還是非常強大的,能夠使得我們在多個組件中共用相同的方法,但同樣也會給組件不斷增加新的方法和屬性,組件本身不僅可以感知,甚至需要做相關的處理(例如命名衝突、狀態維護等),一旦混入的模塊變多時,整個組件就變的難以維護,Mixin
可能會引入不可見的屬性,例如在渲染組件中使用Mixin
方法,給組件帶來了不可見的屬性props
和狀態state
,並且Mixin
可能會相互依賴,相互耦合,不利於代碼維護,此外不同的Mixin
中的方法可能會相互衝突。之前React
官方建議使用Mixin
用於解決橫切關注點相關的問題,但由於使用Mixin
可能會產生更多麻煩,所以官方現在推薦使用HOC
。
高階組件HOC
屬於函數式編程functional programming
思想,對於被包裹的組件時不會感知到高階組件的存在,而高階組件返回的組件會在原來的組件之上具有功能增強的效果,基於此React
官方推薦使用高階組件。
注意
不要改變原始組件
不要試圖在HOC
中修改組件原型,或以其他方式改變它。
function logProps(InputComponent) {
InputComponent.prototype.componentDidUpdate = function(prevProps) {
console.log("Current props: ", this.props);
console.log("Previous props: ", prevProps);
};
// 返回原始的 input 組件,其已經被修改。
return InputComponent;
}
// 每次調用 logProps 時,增強組件都會有 log 輸出。
const EnhancedComponent = logProps(InputComponent);
這樣做會產生一些不良後果,其一是輸入組件再也無法像HOC
增強之前那樣使用了,更嚴重的是,如果你再用另一個同樣會修改componentDidUpdate
的HOC
增強它,那麼前面的HOC
就會失效,同時這個HOC
也無法應用於沒有生命周期的函數組件。
修改傳入組件的HOC
是一種糟糕的抽象方式,調用者必須知道他們是如何實現的,以避免與其他HOC
發生衝突。HOC
不應該修改傳入組件,而應該使用組合的方式,通過將組件包裝在容器組件中實現功能。
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log("Current props: ", this.props);
console.log("Previous props: ", prevProps);
}
render() {
// 將 input 組件包裝在容器中,而不對其進行修改,Nice!
return <WrappedComponent {...this.props} />;
}
}
}
過濾props
HOC
為組件添加特性,自身不應該大幅改變約定,HOC
返回的組件與原組件應保持類似的接口。HOC
應該透傳與自身無關的props
,大多數HOC
都應該包含一個類似於下面的render
方法。
render() {
// 過濾掉額外的 props,且不要進行透傳
const { extraProp, ...passThroughProps } = this.props;
// 將 props 注入到被包裝的組件中。
// 通常為 state 的值或者實例方法。
const injectedProp = someStateOrInstanceMethod;
// 將 props 傳遞給被包裝組件
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
最大化可組合性
並不是所有的HOC
都一樣,有時候它僅接受一個參數,也就是被包裹的組件。
const NavbarWithRouter = withRouter(Navbar);
HOC
通常可以接收多個參數,比如在Relay
中HOC
額外接收了一個配置對象用於指定組件的數據依賴。
const CommentWithRelay = Relay.createContainer(Comment, config);
最常見的HOC
簽名如下,connect
是一個返回高階組件的高階函數。
// React Redux 的 `connect` 函數
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
// connect 是一個函數,它的返回值為另外一個函數。
const enhance = connect(commentListSelector, commentListActions);
// 返回值為 HOC,它會返回已經連接 Redux store 的組件
const ConnectedComment = enhance(CommentList);
這種形式可能看起來令人困惑或不必要,但它有一個有用的屬性,像connect
函數返回的單參數HOC
具有簽名Component => Component
,輸出類型與輸入類型相同的函數很容易組合在一起。同樣的屬性也允許connect
和其他HOC
承擔裝飾器的角色。此外許多第三方庫都提供了compose
工具函數,包括lodash
、Redux
和Ramda
。
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// 你可以編寫組合工具函數
// compose(f, g, h) 等同於 (...args) => f(g(h(...args)))
const enhance = compose(
// 這些都是單參數的 HOC
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
不要在render方法中使用HOC
React
的diff
算法使用組件標識來確定它是應該更新現有子樹還是將其丟棄並掛載新子樹,如果從render
返回的組件與前一個渲染中的組件相同===
,則React
通過將子樹與新子樹進行區分來遞歸更新子樹,如果它們不相等,則完全卸載前一個子樹。
通常在使用的時候不需要考慮這點,但對HOC
來說這一點很重要,因為這代表着你不應在組件的render
方法中對一個組件應用HOC
。
render() {
// 每次調用 render 函數都會創建一個新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 這將導致子樹每次渲染都會進行卸載,和重新掛載的操作!
return <EnhancedComponent />;
}
這不僅僅是性能問題,重新掛載組件會導致該組件及其所有子組件的狀態丟失,如果在組件之外創建HOC
,這樣一來組件只會創建一次。因此每次render
時都會是同一個組件,一般來說,這跟你的預期表現是一致的。在極少數情況下,你需要動態調用HOC
,你可以在組件的生命周期方法或其構造函數中進行調用。
務必複製靜態方法
有時在React
組件上定義靜態方法很有用,例如Relay
容器暴露了一個靜態方法getFragment
以方便組合GraphQL
片段。但是當你將HOC
應用於組件時,原始組件將使用容器組件進行包裝,這意味着新組件沒有原始組件的任何靜態方法。
// 定義靜態函數
WrappedComponent.staticMethod = function() {/*...*/}
// 現在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 增強組件沒有 staticMethod
typeof EnhancedComponent.staticMethod === "undefined" // true
為了解決這個問題,你可以在返回之前把這些方法拷貝到容器組件上。
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必須準確知道應該拷貝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
但要這樣做,你需要知道哪些方法應該被拷貝,你可以使用hoist-non-react-statics
依賴自動拷貝所有非React
靜態方法。
import hoistNonReactStatic from "hoist-non-react-statics";
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
除了導出組件,另一個可行的方案是再額外導出這個靜態方法。
// 使用這種方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...單獨導出該方法...
export { someFunction };
// ...並在要使用的組件中,import 它們
import MyComponent, { someFunction } from "./MyComponent.js";
Refs不會被傳遞
雖然高階組件的約定是將所有props
傳遞給被包裝組件,但這對於refs
並不適用,那是因為ref
實際上並不是一個prop
,就像key
一樣,它是由React
專門處理的。如果將ref
添加到HOC
的返回組件中,則ref
引用指向容器組件,而不是被包裝組件,這個問題可以通過React.forwardRef
這個API
明確地將refs
轉發到內部的組件。。
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 將自定義的 prop 屬性 「forwardedRef」 定義為 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回調的第二個參數 「ref」。
// 我們可以將其作為常規 prop 屬性傳遞給 LogProps,例如 「forwardedRef」
// 然後它就可以被掛載到被 LogProps 包裹的子組件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
示例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>React</title>
</head>
<body>
<div id="root"></div>
</body>
<script src="//unpkg.com/react@17/umd/react.development.js"></script>
<script src="//unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="//unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
class WrappedComponent extends React.Component {
render() {
return <input name="name" />;
}
}
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
constructor(props) {
super(props);
this.state = { name: "" };
}
render() {
const newProps = {
value: this.state.name,
onChange: e => this.setState({name: e.target.value}),
}
return <WrappedComponent
{...this.props}
{...newProps}
/>;
}
}
}
const EnhancedComponent = HOC(WrappedComponent);
const HOC2 = (WrappedComponent) => {
return class EnhancedComponent extends WrappedComponent {
render() {
return this.props.isRender && super.render();
}
}
}
const EnhancedComponent2 = HOC2(WrappedComponent);
var vm = ReactDOM.render(
<>
<EnhancedComponent />
<EnhancedComponent2 isRender={true} />
</>,
document.getElementById("root")
);
</script>
</html>
每日一題
//github.com/WindrunnerMax/EveryDay
參考
//juejin.cn/post/6844903477798256647
//juejin.cn/post/6844904050236850184
//zh-hans.reactjs.org/docs/higher-order-components.htm