React 进阶 - props

2023-05-17 20:55:14 浏览数 (1)

# props

组件之间就像发动机的各个零件,想要让 React 这台机器运作起来,就要处理好各个零件,也就是各个组件之间的联系,而 props 担任的角色就是将每个组件联系起来。

props 是 React 组件通信最重要的手段,它在 React 的世界中充当的角色是十分重要的。

# props 是什么

对于在 React 应用中写的子组件,无论是函数组件 FunComponent ,还是类组件 ClassComponent ,父组件绑定在它们标签里的属性/方法,最终会变成 props 传递给它们。对于一些特殊的属性,比如说 ref 或者 key ,React 会在底层做一些额外的处理。

代码语言:javascript复制
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 对象的属性上,对于组件的插槽会被绑定在 propschildren 属性中。

# React 如何定义 props

props 能做的事情:

  • 在 React 组件层级 props 充当的角色
    • 父组件 props 可以把数据层传递给子组件去渲染消费
    • 子组件可以通过 props 中的 callback ,来向父组件传递信息
    • 可以将视图容器作为 props 进行渲染
  • 从 React 更新机制中 props 充当的角色
    • props 在组件更新中充当了重要的角色,在 fiber 调和阶段中,diff 可以说是 React 更新的驱动器
    • 在 React 中,无法直接检测出数据更新波及到的范围,props 可以作为组件是否更新的重要准则,变化即更新,于是有了 PureComponentmemo 等性能优化方案
  • 从 React 插槽层面 props 充当的角色
    • React 可以把组件的闭合标签里的插槽,转化成 children 属性

# 监听 props 变化

类组件

  • componentWillReceiveProps 可以作为监听 props 的生命周期,但是 React 已经不推荐使用 componentWillReceiveProps,因为这个生命周期超越了 React 的可控制的范围内,可能引起多次执行等情况发生
  • 生命周期的替代方案 getDerivedStateFromProps

函数组件

  • 可以用 useEffect 来作为 props 改变后的监听函数(有一点值得注意, useEffect 初始化会默认执行一次)
代码语言:javascript复制
function Index(props) {
  useEffect(() => {
    console.log('props.number change', props.number)
  }, [props.number])
  return <div>Index</div>
}

# props children 模式

props children 模式 在 React 中非常常用,尤其对一些优秀开源组件库。比如 react-router 中的 SwitchRoute , antd 中的 FormFormItem

props children 的基本应用:

props 插槽组件

代码语言:javascript复制
<Container>
  <Children></Children>
</Container>

Container 组件中,通过 props.children 属性访问到 Children 组件,为 React element 对象,作用:

  • 可以根据需要控制 Children 组件的渲染
  • Container 可以用 React.cloneElement 强化 props 或者修改 Children 的子元素

render props 模式

代码语言:javascript复制
<Container>
  {(ContainerProps) => <Children {...ContainerProps} />}
</Container>

Container 组件中,通过 props.children 属性访问到 Children 为一个函数,作用:

  • 可以根据需要控制 Children 组件的渲染
  • 可以将需要传给 Childrenprops 直接通过函数参数的方式传递给执行函数 children

混合模式

  • ContainerChildren 既有函数也有组件
代码语言:javascript复制
<Container>
  {(ContainerProps) => <Children {...ContainerProps} />}
  <Children />
</Container>

  • 这种情况需要先遍历 children ,判断 children 元素类型:
    • 针对 element 节点,通过 cloneElement 混入 props
    • 针对函数,直接传递参数,执行函数

# props 使用小技巧

抽象 props

  • 抽象 props 一般用于跨层级传递 props ,一般不需要具体指出 props 中某个属性,而是将 props 直接传入或者是抽离到子组件中

混入 props

代码语言:javascript复制
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

代码语言:javascript复制
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

代码语言:javascript复制
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

代码语言:javascript复制
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 元素
  • FormItemname 属性作为表单提交时候的 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>;
}

0 人点赞