React 组件化

1,280 阅读3分钟

使用Context进行跨层级通信

使用组件化的网页开发方法, 组件之间的通信是个避免不了的话题。组件之间的通信方式也是多种多样,现在我们就来介绍一种使用Context进行跨层级的通信的方式。

首先, 一般我们会先在一个单独的文件中, 比如context.js。定义如下的变量:

import React from 'react';
export const ThemeContext = React.createContext();
export const ThemeProvider = ThemeContext.Provider;
export const ThemeConsumer = ThemeContext.Consumer;

然后在祖先组件中,使用Provider对象向子孙组件传值。

 <ThemeProvider value={this.state.theme}>
    <HomePage />
    <UsersPage />
</ThemeProvider>

注意如果用以下字面量的方式传递Provider的值,React每次都会重新渲染它的子组件,因为在JavaScript中两个字面值对象总是不相等的。

{themeColor: 'red'} !== {themeColor: 'red'}

在Class子组件中, 可以这样拿到Provider提供的值。 可以使用Consumer对象拿到Provider提供的值:

<ThemeConsumer>
    {theme=><h1 style={{color: theme.themeColor}}>大王叫我去巡山</h1>}
</ThemeConsumer>

首先在Class中定义静态属性contextType。

static contextType = ThemeContext;

然后可以如下列代码拿到Provider的值:

render() {
    const { themeColor } = this.context;
    return <div>
        <h1 style={{color: themeColor}}>大王叫我去巡山</h1>
    </div>
}

在函数组件中,可以使用useContext方法拿到祖先组件传下来的值。如下代码:

export function UsersPage(props) {
    const {themeColor} = useContext(ThemeContext);
    return (
        <div style={{color:themeColor}}> 我去你家啊!</div>
    );
};

高阶组件

React官方定义,高阶组件为一个函数,接收一个组件,返回另一个组件。

const foo = Cmp => props => {
    return (
        <div className="border">
            <Cmp {...props} />
        </div>
    );
};

开发中,通常使用高级组件对原有组件进行属性代理(添加新的属性),或者对原有组件进行包装,返回一个新的组件类型。

我们可以像调用普通函数那样使用高阶组件,也可以是使用ES7中的装饰器语法。

@foo
class Child extends Component {
    render() {
        return (
            <div>HOC {this.props.name}</div>
        );
    }
}

递归组件

递归组件通常用来渲染递归数据,如下列代码中的 nodes。

this.nodes = [
  {
    val: "v",
    children: [
      { val: "v2" },
      { val: "v3" },
      { val: "v4", children: [{ val: "vv2" }, { val: "vv3" }] },
    ],
  },
  {
    val: "t",
    children: [
      { val: "t2" },
      { val: "t3" },
      { val: "t4", children: [{ val: "tt2" }, { val: "tt3" }] },
    ],
  },
];

要实现递归组件,其实只要像调用递归函数那样调用自身即可, 如下代码所示!

class Node extends Component {
  render() {
    let { val, children } = this.props.node;
    return (
      <div>
        <h1>{val}</h1>
        <div>{children && children.map((node) => <Node node={node} />)}</div>
      </div>
    );
  }
}

仿 Antd4 表单组件

熟悉 Antd4 的同学,都应该对他的 Form 表单的使用比较熟悉了。如下列代码所示:

<Form
  form={form}
  onFinish={(values) => {
    console.log("onFinish......", values);
  }}
  onFinishFailed={() => {
    console.log("onFinishFailed......");
  }}
>
  <Field name="username">
    <input placeholder="太好了" />
  </Field>
  <Field name="password">
    <input placeholder="太好了" />
  </Field>
  <button type="submit">提交</button>
</Form>

要实现上述功能,通常需要使用Context对象,在Form组件中向下传递一个FormStore类的实例。

Context类型定义如下:

import { createContext } from 'react';
const FieldContext = createContext()
const FieldProvider = FieldContext.Provider
const FieldConsumer = FieldContext.Consumer

Form组件的定义如下:

import { FieldProvider } from "./FieldContext";
export default function Form({ children, form, onFinish,  onFinishFailed }) {
  form.setCallback({
      onFinish,
      onFinishFailed,
  })
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        form.submit();
      }}
    >
      <FieldProvider value={form}>{children}</FieldProvider>
    </form>
  );
}

FormStore类型定义如下:

import React from 'react';
class FormStore{
    constructor(){
        this.store = {}
        this.fieldEntities = []
        this.callbacks = {}
    }
    registerField = entity => {
        this.fieldEntities.push(entity)
        return ()=>{
            this.fieldEntities = this.fieldEntities.filter(item => item !== entity)
            delete this.store[entity.props.name]
        }
    }
    getFieldValue = name => {
        return this.store[name];
    }
    setFieldsValue = newStore => {
        this.store = {
            ...this.store,
            ...newStore,
        }       
        this.fieldEntities.forEach(entity => {
            let {name} = entity.props;            
            Object.keys(newStore).forEach(key => {
                if(name === key){
                    entity.onStoreChange();
                }
            
            })
        })
    }

    submit = () => {
        this.callbacks['onFinish'](this.store);
    }

    setCallback = (callbacks) => {
        this.callbacks = {
            ... callbacks,
            ... this.callbacks
        }
    }

    getForm = () => {
        return {
            submit: this.submit,
            registerField: this.registerField,
            getFieldValue: this.getFieldValue,
            setFieldsValue: this.setFieldsValue
        }
    }
}

export default function useForm(){
    const formRef = React.useRef();
    if(!formRef.current){
        const formstore = new FormStore();
        formRef.current = formstore;
    }
    return [formRef.current];
}

Field定义如下:

import React, { Component } from "react";
import { FieldContext } from "./FieldContext";
export default class Field extends Component {
  static contextType = FieldContext;

  componentDidMount() {
    const { registerField } = this.context;
    this.unregisterField = registerField(this);
  }

  componentWillUnmount() {
    if (this.unregisterField) {
      this.unregisterField();
    }
  }

  onStoreChange = () => {
    this.forceUpdate();
  };

  getControlled = () => {
    const { name } = this.props;
    const { getFieldValue, setFieldsValue } = this.context;
    return {
      value: getFieldValue(name) || "",
      onChange: (event) => {
        const newValue = event.target.value;
        // console.log("newValue", newValue);
        setFieldsValue({ [name]: newValue });
      },
    };
  };

  render() {
    const { children } = this.props;
    const returnChildNode = React.cloneElement(children, this.getControlled());
    return returnChildNode;
  }
}