React props complete analysis

Props is the most important means of communication between React components, and its role in the React world is very important. Learning props well can make the communication between components more flexible. At the same time, this article will introduce some operation skills of props and learn how to write nested components.

1. What is Props

Let's look at a demo first:

function Chidren(){
  return <div> 我是子组件 </div>
}
/* props 接受处理 */
function Father(props) {
      const {  children , mes , renderName , say ,Component } = props
      const renderFunction = children[0]
      const renderComponent = children[1]
      /* 对于子组件,不同的props是怎么被处理 */
      return (
        <div>
            { renderFunction() }
            { mes }
            { renderName() }
            { renderComponent }
            <Component />
            <button onClick={ () => say() } > 触发更改 </button>
        </div> )
}
/* props 定义绑定 */
class App extends React.Component{
  state={  
      mes: "hello,React"
  }
  node = null
  say= () =>  this.setState({ mes:'let us learn React!' })
  render(){
      return <div>
          <Father  
             mes={this.state.mes}  // ① props 作为一个渲染数据源
             say={ this.say  }     // ② props 作为一个回调函数 callback
             Component={ Chidren } // ③ props 作为一个组件
             renderName={ ()=><div> my name is YinJie </div> } // ④ props 作为渲染函数
          >
              { ()=> <div>hello,world</div>  } { /* ⑤render props */ }
              <Chidren />             { /* ⑥render component */ }
          </Father>
      </div>
  }
}

Let's look at the output:

When the click triggers a change, the callback can be called to change the data source:

So props can be:

  • ① props renders the data source as a subcomponent.
  • ② props is used as a callback function to notify the parent component.
  • ③ props is passed as a pure component.
  • ④ props as a rendering function.
  • ⑤ The difference between render props and ④ is that it is placed on the children attribute.
  • ⑥ render component slot component.

Two, props children mode

Let's take a look at some basic situations of prop + children:

① props slot component

<Container>
    <Children>
</Container>

The above can be accessed in the Container component through the props.children attribute to the Children component, which is a React element object.

effect:

  • You can control whether Children are rendered according to your needs.

  • As mentioned in the previous section, Container can use React.cloneElement to enhance props (mix in new props), or modify Children's child elements.

Give an example of using React.cloneElement to strengthen props. It is mostly used to mix new props into subcomponents when writing components. Next, we will make a navigation component. We hope that its structure is as follows:

<Menu>
    <MenuItem >
        active
    </MenuItem>
    <MenuItem>
        disabled
    </MenuItem>
    <MenuItem >
        xyz
    </MenuItem>
</Menu>

We want to add an index attribute to each MenuItem subcomponent. This matter should not be added manually by the user. It is best to automatically add it to each MenuItem subcomponent in the Menu component, and the Menu component should also determine the type of the subcomponent , if the type of the subcomponent is not a MenuItem component, an error will be reported.

Menu.tsx:

const Menu: React.FC<MenuProps> = (props) => {
    // ... 一些操作
    const renderChildren = () => { // 让子级的children都是 menuItem,有不是的就报错
        return React.Children.map(children, (child, index) => {
            const childElement = child as React.FunctionComponentElement<MenuItemProps>
            const { displayName } = childElement.type
            if(displayName === 'MenuItem' || displayName === "SubMenu") {
                return React.cloneElement(childElement, { index: index.toString() })
            } else {
                console.error('warning: Menu has a child whitch is not a MenuItem')
            }
        })
    }
    return (
        <ul className={classes} style={style} data-testid="test-menu">
            <MenuContext.Provider value={passedContext}>
                {renderChildren()}
            </MenuContext.Provider>
        </ul>
    )
}

In the Menu component, we use React.children.map to loop through the child components, and we can get the displayName static property of each child component through child.type, which is defined in the child component:

Use the displayName of the subcomponent to determine whether it is the MenuItem we need, and if so, call React.cloneElement to add the index attribute to the subcomponent.

② render props mode

<Container>
   { (ContainerProps)=> <Children {...ContainerProps}  /> }
</Container>

In this case, in the Container, the props.children attribute access is a function, not a React element object. We should call this function:

function  Container(props) {
    const  ContainerProps = {
        name: 'alien',
        mes:'let us learn react'
    }
     return  props.children(ContainerProps)
}

The effect of this method is:

  • 1 Control whether Children render or not as needed.
  • 2 The props that need to be passed to Children can be directly passed to the execution function children through function parameters.

3. render props mode

If the Children of the Container have both functions and components, how should this situation be handled?

<Container>
    <Children />
    { (ContainerProps)=> <Children {...ContainerProps} name={'haha'}  />  }
</Container>
const Children = (props)=> (<div>
    <div>hello, my name is {  props.name } </div>
    <div> { props.mes } </div>
</div>)

function  Container(props) {
    const ContainerProps = {
        name: 'alien',
        mes:'let us learn react'
    }
     return props.children.map(item=>{
        if(React.isValidElement(item)){ // 判断是 react elment  混入 props
            return React.cloneElement(item,{ ...ContainerProps },item.props.children)
        }else if(typeof item === 'function'){
            return item(ContainerProps)
        }else return null
     })
}

const Index = ()=>{
    return <Container>
        <Children />
        { (ContainerProps)=> <Children {...ContainerProps} name={'haha'}  />  }
    </Container>
}

In this case, you need to traverse children first to determine the type of children element:

  • For the element node, mix in props through cloneElement;
  • For functions, pass parameters directly and execute functions.

3. Advanced practice - implement a simple  <Form> <FormItem>nested component 

Next comes the practical part. Need to write a practice demo for form state management <Form> and  <FormItem> components

  • <Form>Used to manage form state;
  • <FormItem>Used to manage <Input>input box components. ,

The functions that the written components can realize are:

  • Form Components can be obtained by ref. Then you can call the instance method  submitForm to get the form content, which is used to submit the form, and resetForm the method is used to reset the form.
  • ②The Formcomponent automatically filters out FormItemother React elements except
  • ③The FormItem name attribute in the form is used as the key when the form is submitted, as well as the displayed label.
  • ④The  value of the form FormItem can be collected automatically  <Input/> .

App.js:

import React, { useState, useRef } from "react";
import Form from './Form'
import FormItem from './FormItem'
import Input from './Input'

function App () {
  const form =  useRef(null)
  const submit =()=>{
      /* 表单提交 */
      form.current.submitForm((formValue)=>{ // 调用 form 中的submitForm方法
          console.log(formValue)
      })
  }
  const reset = ()=>{
      /* 表单重置 */
      form.current.resetForm() //调用 form 中的 resetForm 方法
  }
  return <div className='box' >
      <Form ref={ form } >
          <FormItem name="name" label="我是"  >
              <Input   />
          </FormItem>
          <FormItem name="mes" label="我想对大家说"  >
              <Input   />
          </FormItem>
          <FormItem name="lees" label="ttt"  >
              <Input   />
          </FormItem>
      </Form>
      <div className="btns" >
          <button className="searchbtn"  onClick={ submit } >提交</button>
          <button className="concellbtn" onClick={ reset } >重置</button>
      </div>
  </div>
}

export default App

Form.js:

class Form extends React.Component{
    state={
        formData:{}
    }
    /* 用于提交表单数据 */
    submitForm=(cb)=>{
        cb({ ...this.state.formData })
    } 
    /* 获取重置表单数据 */
    resetForm=()=>{
       const { formData } = this.state
       Object.keys(formData).forEach(item=>{
           formData[item] = ''
       })
       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
                /* 克隆`FormItem`节点,混入改变表单单元项的方法 */
                const Children = React.cloneElement(child,{ 
                    key:name ,                             /* 加入key 提升渲染效果 */
                    handleChange:this.setValue ,           /* 用于改变 value */
                    value:this.state.formData[name] ||  '' /* value 值 */
                },child.props.children)
                renderChildren.push(Children)
            }
        })
        return renderChildren
    }
}
/* 增加组件类型type  */
Form.displayName = 'form'

Design thinking:

  • First of all   , it is best to use class components <Form> without using  them, because only class components can obtain instances.forwardRef
  • Create a formData attribute under state to collect form state.
  • To encapsulate  methods for resetting forms , submitting forms , and changing form cell items .
  • To filter out  FormItem elements other than elements, how do you know if it is? FormItemHere is a way to teach you a way to bind static properties to function components or class components to prove its identity, and then traverse props.children At this time, you can verify the identity on the type attribute of the React element (class or function component itself). In this demo project, the displayName attribute bound to the function proves the identity of the component.
  • To clone  FormItem a node, mix in the method handleChange that changes the form cell item and the value of the form into props.

FormItem.js:

function FormItem(props){
    const { children , name  , handleChange , value , label  } = props
    const onChange = (value) => {
        /* 通知上一次value 已经改变 */
        handleChange(name,value)
    }
   return <div className='form' >
       <span className="label" >{ label }:</span>
       {
            React.isValidElement(children) && children.type.displayName === 'input' 
            ? React.cloneElement(children,{ onChange , value })
            : null
       }
   </div>    
}
FormItem.displayName = 'formItem'

Design thinking:

  • FormItemBe sure to bind the displayName attribute for  <Form> identification<FormItem />
  • The declaration  onChange method, provided through props <Input>, acts as a callback function for changing the value.
  • FormItemFilter out  input elements other than .

Input.js:

/* Input 组件, 负责回传value值 */
function Input({ onChange , value }){
    return  <input className="input"  onChange={ (e)=>( onChange && onChange(e.target.value) ) } value={value}  />
}
/* 给Component 增加标签 */
Input.displayName = 'input'

Design thinking:

  • Bind the displayName flag input.
  • input DOM element, bound onChange method, used to pass value.

Let's rewrite it again through the function component:

App.js, FormItem.js and Input.js are still the same. Form.js uses hooks to manage the state, and through forwardRef, useImperativeHandle, let the App component access the methods in the Form:

import React, { useState, forwardRef, useImperativeHandle } from "react"
const Form = (props, ref) =>{
    const { children } = props
    const [ formData, setFormData ] = useState({})
    useImperativeHandle(ref, () => ({
        submitForm: submitForm,
        resetForm: resetForm
    }))
    /* 用于提交表单数据 */
    const submitForm=(cb)=>{
        cb(formData)
    } 
    /* 获取重置表单数据 */
    const resetForm=()=>{
        const newData = formData
       Object.keys(newData).forEach(item=>{
          newData[item] = ''
       })
       setFormData(newData)
    }
    /* 设置表单数据层 */
    const setValue=(name,value)=>{
        setFormData({
            ...formData,
            [name]:value
        })
    }

    const renderChildren = () => {
        return React.Children.map(children,(child)=>{
            if(child.type.displayName === 'formItem'){
                const { name } = child.props
                /* 克隆`FormItem`节点,混入改变表单单元项的方法 */
                const Children = React.cloneElement(child,{ 
                    key:name ,                             /* 加入key 提升渲染效果 */
                    handleChange: setValue ,           /* 用于改变 value */
                    value: formData[name] ||  '' /* value 值 */
                },child.props.children)
                return Children
            }
        })
    }
    return ( 
          renderChildren()
        )
}
/* 增加组件类型type  */
Form.displayName = 'form'

export default forwardRef(Form) 

Start the project and see the effect:

Click submit, and the content we enter in the input box can be displayed on the console.

In order to reflect the high reusability of our nested components, we can add subitems to the root component at will:

 

Guess you like

Origin blog.csdn.net/qq_49900295/article/details/127211050