# props
组件之间就像发动机的各个零件,想要让 React 这台机器运作起来,就要处理好各个零件,也就是各个组件之间的联系,而 props
担任的角色就是将每个组件联系起来。
props
是 React 组件通信最重要的手段,它在 React 的世界中充当的角色是十分重要的。
# props
是什么
对于在 React 应用中写的子组件,无论是函数组件 FunComponent
,还是类组件 ClassComponent
,父组件绑定在它们标签里的属性/方法,最终会变成 props
传递给它们。对于一些特殊的属性,比如说 ref
或者 key
,React 会在底层做一些额外的处理。
function ChildrenComponent() {
return <div>ChildrenComponent</div>;
}
class PropsComponent extends React.Component {
componentDidMount() {
console.log('_this', this);
}
render () {
const { children, mes, renderName, say, Component } = this.props;
const renderFunction = children[0];
const renderComponent = children[1];
return <div>
{ renderFunction() }
{ mes }
{ renderName() }
{ renderComponent }
<Component />
<button onClick={() => say()}>change content</button>
</div>
}
}
class Index extends React.Component {
state = {
mes: 'hello world',
}
node = null
say = () => {
this.setState({
mes: 'hello react',
})
}
render() {
return <div>
<PropsComponent
mes={this.state.mes}
say={this.say}
Component={ChildrenComponent}
renderName={() => {
return <div>renderName</div>
}}
>
{() => {
return <div>renderFunction</div>
}}
<ChildrenComponent />
</PropsComponent>
</div>
}
}
props
可以作为:
- 作为一个子组件渲染数据源
mes
- 作为一个通知父组件的回调函数
say
- 作为一个单纯的组件传递
ChildrenComponent
- 作为渲染函数
renderName
- render props
renderFunction
- render component 插槽组件
renderComponent
PropsComponent
如果是一个类组件,那么可以直接通过 this.props
访问到它:
在标签内部的属性和方法会直接绑定在 props
对象的属性上,对于组件的插槽会被绑定在 props
的 children
属性中。
# React 如何定义 props
props
能做的事情:
- 在 React 组件层级
props
充当的角色- 父组件
props
可以把数据层传递给子组件去渲染消费 - 子组件可以通过
props
中的callback
,来向父组件传递信息 - 可以将视图容器作为
props
进行渲染
- 父组件
- 从 React 更新机制中
props
充当的角色props
在组件更新中充当了重要的角色,在fiber
调和阶段中,diff
可以说是 React 更新的驱动器- 在 React 中,无法直接检测出数据更新波及到的范围,
props
可以作为组件是否更新的重要准则,变化即更新,于是有了PureComponent
,memo
等性能优化方案
- 从 React 插槽层面
props
充当的角色- React 可以把组件的闭合标签里的插槽,转化成
children
属性
- React 可以把组件的闭合标签里的插槽,转化成
# 监听 props
变化
类组件
componentWillReceiveProps
可以作为监听props
的生命周期,但是 React 已经不推荐使用componentWillReceiveProps
,因为这个生命周期超越了 React 的可控制的范围内,可能引起多次执行等情况发生- 生命周期的替代方案
getDerivedStateFromProps
函数组件
- 可以用
useEffect
来作为props
改变后的监听函数(有一点值得注意,useEffect
初始化会默认执行一次)
function Index(props) {
useEffect(() => {
console.log('props.number change', props.number)
}, [props.number])
return <div>Index</div>
}
# props children
模式
props
children
模式 在 React 中非常常用,尤其对一些优秀开源组件库。比如 react-router 中的 Switch
和 Route
, antd 中的 Form
和 FormItem
。
props
children
的基本应用:
props
插槽组件
<Container>
<Children></Children>
</Container>
在 Container
组件中,通过 props.children
属性访问到 Children
组件,为 React element 对象,作用:
- 可以根据需要控制
Children
组件的渲染 Container
可以用React.cloneElement
强化props
或者修改Children
的子元素
render props
模式
<Container>
{(ContainerProps) => <Children {...ContainerProps} />}
</Container>
在 Container
组件中,通过 props.children
属性访问到 Children
为一个函数,作用:
- 可以根据需要控制
Children
组件的渲染 - 可以将需要传给
Children
的props
直接通过函数参数的方式传递给执行函数children
混合模式
Container
的Children
既有函数也有组件
<Container>
{(ContainerProps) => <Children {...ContainerProps} />}
<Children />
</Container>
- 这种情况需要先遍历
children
,判断children
元素类型:- 针对
element
节点,通过cloneElement
混入props
- 针对函数,直接传递参数,执行函数
- 针对
# props
使用小技巧
抽象 props
- 抽象
props
一般用于跨层级传递props
,一般不需要具体指出props
中某个属性,而是将props
直接传入或者是抽离到子组件中
混入 props
function Child(props) {
console.log(props)
return <div>hello, world</div>
}
function Parent(props) {
const parentProps = {
msg: 'Let us learn React'
}
return <Child {...props} {...parentProps} />
}
function Index() {
const indexProps = {
name: 'cell',
age: 18
}
return <Parent {...indexProps} />
}
抽离 props
function Child(props) {
console.log(props)
return <div>hello, world</div>
}
function Parent(props) {
const { msg, ...childProps } = props
return <Child {...childProps} />
}
function Index() {
const indexProps = {
name: 'cell',
age: 18,
msg: 'Let us learn React'
}
return <Parent {...indexProps} />
}
注入 props
显式注入 props
:能够直观看见标签中绑定的 props
function Child(props) {
console.log(props)
return <div>hello, world</div>
}
function Parent(props) {
return props.children
}
function Index() {
return <Parent>
<Child name="cell" age={18} />
</Parent>
}
隐式注入 props
:一般通过 React.cloneElement 对 props.children
克隆再混入新的 props
function Child(props) {
console.log(props)
return <div>hello, world</div>
}
function Parent(props) {
return React.cloneElement(props.children, {
msg: 'Let us learn React'
})
}
function Index() {
return <Parent>
<Child name="cell" age={18} />
</Parent>
}
# 实践练习
实现一个 Demo ,用于表单状态管理的 <Form>
和 <FormItem>
组件:
<Form>
用于管理表单状态<FormItem>
用于管理<Input>
输入框组件
组件需要实现的功能:
Form
组件可以被ref
获取实例- 可以调用实例方法
submitForm
获取表单内容,用于提交表单 resetForm
方法用于重置表单
- 可以调用实例方法
Form
组件自动过滤掉除了FormItem
之外的其他 React 元素FormItem
中name
属性作为表单提交时候的key
,还有展示的label
FormItem
可以自动收集<Input/>
表单的值
# <Form>
代码语言:javascript复制class Form extends React.Component {
state = {
formData: {}
}
submitForm = (cb) => {
cb({...this.state.formData})
}
resetForm = () => {
const { formData } = this.state;
Object.keys(formData).forEach(key => {
formData[key] = '';
});
this.setState({
formData
});
}
setValue = (name, value) => {
this.setState({
formData: {
...this.state.formData,
[name]: value
}
});
}
render() {
const { children } = this.props;
const renderChildren = [];
React.Children.forEach(children, (child) => {
if (child.type.displayName === 'formItem') {
const { name } = child.props;
const Children = React.cloneElement(child, {
key: name,
handleChange: this.setValue,
value: this.state.formData[name] || ''
}, child.props.children);
renderChildren.push(Children);
}
});
return renderChildren;
}
}
Form.displayName = 'form';
设计思路:
- 考虑到
<Form>
在不使用forwardRef
前提下,最好是类组件,因为只有类组件才能获取实例 - 创建一个
state
下的formData
属性,用于收集表单状态 - 要封装 重置表单,提交表单,改变表单单元项的方法
- 过滤掉除了
FormItem
元素之外的其他元素- 可以给函数组件或者类组件绑定静态属性来证明它的身份,然后在遍历
props.children
的时候就可以在 React element 的type
属性(类或函数组件本身)上,验证这个身份
- 可以给函数组件或者类组件绑定静态属性来证明它的身份,然后在遍历
- 要克隆
FormItem
节点,将改变表单单元项的方法handleChange
和表单的值value
混入props
中
# <FormItem>
代码语言:javascript复制function FormItem(props) {
const { children, name, handleChange, value, label } = props;
const onChange = (value) => {
handleChange(name, value);
};
return <div className='form'>
<span className='label'>{label}:</span>
{
React.isValidElement(children) && children.type.displayName === 'input'
? React.cloneElement(children, {
value,
onChange
})
: null
}
</div>;
}
FormItem.displayName = 'formItem';
设计思路:
FormItem
一定要绑定displayName
属性,用于让<Form>
识别<FormItem />
- 声明
onChange
方法,通过props
提供给<Input>
,作为改变value
的回调函数 FormItem
过滤掉除了input
以外的其他元素
# <Input>
代码语言:javascript复制function Input({ onChange, value }) {
return <input
className='input'
onChange={(e) => {
onChange && onChange(e.target.value);
}}
value={value}
/>;
}
Input.displayName = 'input';
设计思路:
- 绑定
displayName
标识input
input
DOM 元素,绑定onChange
方法,用于传递value
# 使用示例
代码语言:javascript复制export default () => {
const form = React.useRef(null);
const submit = () => {
form.current.submitForm((formValue) => {
console.log(formValue);
});
};
const reset = () => {
form.current.resetForm();
};
return <div>
<Form ref={form}>
<FormItem name='name' label='姓名'>
<Input />
</FormItem>
<FormItem name='age' label='年龄'>
<Input />
</FormItem>
</Form>
<button onClick={submit}>提交</button>
<button onClick={reset}>重置</button>
</div>;
}