¶背景
我们在使用Antd
组件时经常会有以下写法:
<Form>
<Form.Item name="username">
<Input />
</Form.Item>
<Form.Item name="password">
<Input.Password />
</Form.Item>
</Form>
那么像Form.Item
、Input.Password
这种组件是如何实现的呢?
¶以一个Toggle组件为例
假设我们现在要实现一个Toggle
组件,其下包含Toggle.On
、Toggle.Off
用来指示当前状态,以及一个Toggle.Button
用来更新状态。用法大概如下所示:
function App() {
const onToggle = (...args) => {
console.log('onToggling: ', ...args);
};
return (
<Toggle onToggle={onToggle}>
<Toggle.On>开关打开了</Toggle.On>
<Toggle.Off>开关关闭了</Toggle.Off>
<Toggle.Button />
</Toggle>
);
}
我们可以利用类的static
去实现这个结构的组件:
import React from 'react';
import { Switch } from 'antd';
class Toggle extends React.Component {
static On = ({ on, children }) => (on ? children : null);
static Off = ({ on, children }) => (on ? null : children );
static Button = ({ on, toggle }) => {
return <Switch checked={on} onChange={toggle} />
};
state = {
on: false,
};
toggle = () => {
this.setState(({ on }) => ({
on: !on,
}), () => {
this.props.onToggle(this.state.on);
});
};
render() {
const { children } = this.props;
return React.Children.map(children, (child) => React.cloneElement(child, {
on: this.state.on,
toggle: this.toggle,
}));
}
}
¶完善
上述实现中我们未对children
进行校验,下面我们进行简单的校验以支持普通的jsx
标签:
+ function componentHasChild(child) {
+ for (const property in Toggle) {
+ if (Toggle.hasOwnProperty(property)) {
+ if (child.type === Toggle[property]) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
class Toggle extends React.Component {
// ...
render() {
const { children } = this.props;
+ return React.Children.map(children, child => {
+ if (componentHasChild(child)) {
+ return React.cloneElement(child, {
+ on: this.state.on,
+ toggle: this.toggle,
+ });
+ }
+ return child;
+ });
}
}
使用示例:
+ const Hi = () => <h1>hello world</h1>
function App() {
const onToggle = () => console.log('toggle...');
return (
<Toggle onToggle={onToggle}>
<Toggle.On>开关打开了</Toggle.On>
<Toggle.Off>开关关闭了</Toggle.Off>
<Toggle.Button />
+ <span>Hello</span>
+ <Hi />
</Toggle>
);
}
¶升级为Context版本
我们可以使用Context
改造一下上面的Toggle
组件:
import React from 'react';
import { Switch } from 'antd';
const ToggleContext = React.createContext();
function ToggleConsumer(props) {
return (
<ToggleContext.Consumer {...props}>
{context => {
if (!context) {
// 必须被Toggle包裹
throw new Error(
`Toggle compound components cannot be rendered outside the Toggle component`,
);
}
return props.children(context);
}}
</ToggleContext.Consumer>
);
}
class Toggle extends React.Component {
static On = ({children}) => (
<ToggleConsumer>
{({ on }) => (on ? children : null)}
</ToggleConsumer>
);
static Off = ({children}) => (
<ToggleConsumer>
{({ on }) => (on ? null : children)}
</ToggleConsumer>
);
static Button = props => (
<ToggleConsumer>
{({on, toggle }) => (
<Switch on={on} onClick={toggle} {...props} />
)}
</ToggleConsumer>
);
// 此处toggle放在state之上保证state初始化时this.toggle不为空
toggle = () => {
this.setState(
({ on }) => ({ on: !on }),
() => this.props.onToggle(this.state.on),
);
};
state = { on: false, toggle: this.toggle };
render() {
return (
<ToggleContext.Provider value={this.state}>
{this.props.children}
</ToggleContext.Provider>
);
}
}
使用示例:
function App() {
const onToggle = (...args) => console.log('onToggle', ...args);
return (
<Toggle onToggle={onToggle}>
<Toggle.On>开关打开了</Toggle.On>
<Toggle.Off>开关关闭了</Toggle.Off>
<div>
<Toggle.Button />
</div>
</Toggle>
);
}