11React 组件化
资源:
组件化
快速开始
https://www.html.cn/create-react-app/docs/getting-started/
npx create-react-app reactDemo
cd reactDemo
yarn start
组件化优点
增强代码重用性,提高开发效率;
简化调试步骤,提升整个项目的可维护性;
便于协同开发;
注意点:降低耦合性。
组件跨层级通信 - Context
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不是显式地通过组件数的逐层传递 props。
React 中使用 Context 实现祖代组件向后代组件跨层级传值,Vue 中的 provide & inject 来源于 Context。
Context API
React.createContext
创建一个 Context 对象,当 React 订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provide 中读取到当前的 context 值。
Context.Provider
Provider 接收一个 value 属性,传递给消费组件,允许消费组件订阅 context 的变化。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value 的值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
Class.contextType
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
你只通过该 API 订阅单一 context。
Context.Consumer
这里,React 组件也可以订阅到 context 变更,这能让你在函数式组件中完成订阅 context。
这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。
useContext
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。只能用在 function 组件中。
使用 Context
创建 Context => 获取 Provider 和 Consumer => Provider 提供值 => Consumer 消费值
范例:共享主题色
import React, { Component } from 'react'
import ContextTypePage from './ContextTypePage'
import { ThemeContext, UserContext } from '../Context'
import UserContextPage from './UseContextPage'
import ConsumerPage from './ConsumerPage'
export default class ContextPage extends Component {
constructor(props) {
super(props)
this.state = {
theme: {
themeColor: 'red'
},
user: {
name: '林慕'
}
}
}
changeColor = () => {
const { themeColor } = this.state.name
this.setState({
theme: {
themeColor: themeColor === 'red' ? 'green' : 'red'
}
})
}
render () {
const { theme, user } = this.state
return (
<div>
<h3>ContextPage</h3>
<button onClick={this.changeColor}>change color</button>
<ThemeContext.Provider value={theme}>
<ContextTypePage></ContextTypePage>
<UserContext.Provider value={user}>
<UserContextPage></UserContextPage>
<ConsumerPage></ConsumerPage>
</UserContext.Provider>
</ThemeContext.Provider>
</div>
)
}
}
Context.js
import React from 'react'
export const ThemeContext = React.createContext({ themeColor: 'pink' })
export const UserContext = React.createContext()
pages/ContextTypePage.js
import React, { Component } from 'react'
import { ThemeContext } from '../Context'
export default class ContextTypePage extends Component {
static contextType = ThemeContext
render () {
const { themeColor } = this.context
return (
<div className='border'>
<h3 className={themeColor}>ContextTypePage</h3>
</div>
)
}
}
pages/ConsumerPage.js
import React, { Component } from 'react'
import { ThemeContext, UserContext } from '../Context'
export default class ConsumerPage extends Compoennt {
render () {
return (
<div className='border'>
<ThemeContext.Consumer>
{
themeContext => {
<dvi>
<h3 className={themeContext.themeColor}>ConsumerPage</h3>
<UserContext.Consumer>
{userContext => <HandleUserContext {...userContext}></HandleUserContext>}
</UserContext.Consumer>
</dvi>
}
}
</ThemeContext.Consumer>
</div>
)
}
}
function HandleUserContext (userCtx) {
return <div>{userCtx.name}</div>
}
消费多个 Context
<ThemeProvider value={theme}>
<ContextTypePage />
<ConsumerPage />
{/*多个Context */}
<UserProvider value={user}>
<MultipleContextsPage />
</UserProvider>
</ThemeProvider>
如果两个或者更多的 context 值经常被一起使用,那你可能要考虑一下另外创建你自己的渲染组件,以提供这些值。
pages/UseContextPage
import React, { useState, useEffect, useContext } from "react";
import { ThemeContext, UserContext } from "../Context";
export default function UseContextPage (props) {
const themeContext = useContext(ThemeContext);
const { themeColor } = themeContext;
const userContext = useContext(UserContext);
return (
<div className="border">
<h3 className={themeColor}>UseContextPage</h3>
<p>{userContext.name}</p>
</div>
);
}
注意事项:
因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:
class App extends React.Component {
render () {
return (
<Provider value={{ something: 'something' }}>
<Toolbar />
</Provider>
)
}
}
为了防止这种情况,将 value 状态提升到父节点的 state 里:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: { something: 'something' },
};
}
render () {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
总结
在 React 的官方文档中,Context 被归类为高级部分(Advanced),属于 React 的高级 API,建议不要滥用。
高阶组件 - HOC
为了提高组件复用率,可测试性,就要保证组件功能单一性;但是若要满足复杂需求就要扩展功能单一的组件,在 React 里就有了 HOC(Higher-Order Component)的概念。
定义:高阶组件是参数为组件,返回值为新组件的函数。
基本使用
HocPage.js
import React, { Component } from 'react'
// 这里大写开头的cmp是指function或者class组件
const foo = Cmp => props => {
return (
<div className='border'>
<Cmp {...props}></Cmp>
</div>
)
}
function Child (props) {
return <div>child{props.name}</div>
}
const Foo = foo(Child)
export default class HocPage extends Component {
render () {
return (
<div>
<h3>HocPage</h3>
<Foo name='msg'></Foo>
</div>
)
}
}
链式调用
import React, { Component } from 'react'
// 这里大写开头的cmp是指function或者class组件
const foo = Cmp => props => {
return (
<div className='border'>
<Cmp {...props}></Cmp>
</div>
)
}
const foo2 = Cmp => props => {
return (
<div className='greenBorder'>
<Cmp {...props}></Cmp>
</div>
)
}
function Child (props) {
return <div>child{props.name}</div>
}
const Foo = foo2(foo(foo(Child)))
export default class HocPage extends Component {
render () {
return (
<div>
<h3>HocPage</h3>
<Foo name='msg'></Foo>
</div>
)
}
}
装饰器写法
高阶组件本身是对装饰器模式的应用,自然可以利用 ES7 中出现的装饰器语法来更优雅的书写代码。
yarn add @babel/plugin-proposal-decorators
更新 config-overrides.js
//配置完成后记得重启下
const { addDecoratorsLegacy } = require("customize-cra");
module.exports = override(
...,
addDecoratorsLegacy()//配置装饰器器
);
如果 vsCode 对装饰器有 warning,vsCode 的设置加上:
javascript.implicitProjectConfig.experimentalDecorators": true
装饰器只能用在 class 上,执行顺序从下往上。
HocPage.js
@foo2
@foo
@foo
class Child extends Component {
render () {
return (
<div>
Child{this.props.name}
</div>
)
}
}
export default class HocPage extends Component {
render () {
rerurn(
<div>
<h3>HocPage</h3>
<Child></Child>
</div>
)
}
}
组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。
HOC 在 React 的第三方库中很常见,例如 React-Redux 的 connect。
使用 HOC 的注意事项
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
不要在 render 方法中使用 HOC
React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。如果他们不相等,则完全卸载前一个子树。
render() {
// 每次调⽤用 render 函数都会创建⼀一个新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
return <EnhancedComponent />;
}
这不仅仅是性能问题,重新挂载组件会导致该组件及其所有子组件的状态丢失。
表单组件设计与实现
antd 表单使用
实现用户名密码登录,并实现校验
FormPage.js
import React, { Component, useEffect } from "react";
import { Form, Input, Button } from "antd";
const FormItem = Form.Item;
const nameRules = { required: true, message: "请输⼊入姓名!" };
const passworRules = { required: true, message: "请输⼊入密码!" };
export default class AntdFormPage extends Component {
formRef = React.createRef();
componentDidMount () {
this.formRef.current.setFieldsValue({ name: "default" });
}
onReset = () => {
this.formRef.current.resetFields();
};
onFinish = val => {
console.log("onFinish", val); //sy-log
};
onFinishFailed = val => {
console.log("onFinishFailed", val); //sy-log
};
render () {
console.log("AntdFormPage render", this.formRef.current); //sy-log
return (
<div>
<h3>AntdFormPage</h3>
<Form
ref={this.formRef}
onFinish={this.onFinish}
onFinishFailed={this.onFinishFailed}
onReset={this.onReset}>
<FormItem label="姓名" name="name" rules={[nameRules]}>
<Input placeholder="name input placeholder" />
</FormItem>
<FormItem label="密码" name="password" rules={[passworRules]}>
<Input placeholder="password input placeholder" />
</FormItem>
<FormItem>
<Button type="primary" size="large" htmlType="submit">
Submit
</Button>
</FormItem>
<FormItem>
<Button type="default" size="large" htmlType="reset">
Reset
</Button>
</FormItem>
</Form>
</div>
);
}
}
function 实现:
注意 useForm 是 React Hooks 的实现,只能用于函数组件。
export default function AntdFormPage (props) {
const [form] = Form.useForm();
const onFinish = val => {
console.log("onFinish", val); //sy-log
};
const onFinishFailed = val => {
console.log("onFinishFailed", val); //sy-log
};
const onReset = () => {
form.resetFields();
};
useEffect(() => {
form.setFieldsValue({ name: "default" });
}, []);
return (
<Form
form={form}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
onReset={onReset}>
<FormItem label="姓名" name="name" rules={[nameRules]}>
<Input placeholder="name input placeholder" />
</FormItem>
<FormItem label="密码" name="password" rules={[passworRules]}>
<Input placeholder="password input placeholder" />
</FormItem>
<FormItem>
<Button type="primary" size="large" htmlType="submit">
Submit
</Button>
</FormItem>
<FormItem>
<Button type="default" size="large" htmlType="reset">
Reset
</Button>
</FormItem>
</Form>
);
}
antd3 表单组件设计思路
表单组件要求实现数据收集、校验、提交等特性,
可通过高阶组件扩展高阶组件给表单组件传递一个 input 组件包装函数接管其输入事件并统一管理表单数据高阶组件给表单组件传递一个校验函数使其具备数据校验功能
但是 antd3 的设计有个问题,就是局部变化会引起整体变化,antd4 改进了这个问题。
antd4 表单组件实现
antd4 的表单实现基于 rc-field-form,git 源码地址:https://github.com/react-component/field-form
安装 rc-field-form:
yarn add rc-field-form
使用 useForm,仅限 function:
import React, { Component, useEffect } from "react";
// import Form, {Field} from "rc-field-form";
import Form, { Field } from "../components/my-rc-field-form/";
import Input from "../components/Input";
const nameRules = { required: true, message: "请输⼊入姓名!" };
const passworRules = { required: true, message: "请输⼊入密码!" };
export default function MyRCFieldForm (props) {
const [form] = Form.useForm();
const onFinish = val => {
console.log("onFinish", val); //sy-log
};
// 表单校验失败执⾏行行
const onFinishFailed = val => {
console.log("onFinishFailed", val); //sy-log
};
useEffect(() => {
console.log("form", form); //sy-log
form.setFieldsValue({ username: "default" });
}, []);
return (
<div>
<h3>MyRCFieldForm</h3>
<Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
<Field name="username" rules={[nameRules]}>
<Input placeholder="input UR Username" />
</Field>
<Field name="password" rules={[passworRules]}>
<Input placeholder="input UR Password" />
</Field>
<button>Submit</button>
</Form>
</div>
);
}
class 实现:
export default class MyRCFieldForm extends Component {
formRef = React.createRef();
componentDidMount () {
console.log("form", this.formRef.current); //sy-log
this.formRef.current.setFieldsValue({ username: "default" });
}
onFinish = val => {
console.log("onFinish", val); //sy-log
};
// 表单校验失败执⾏行行
onFinishFailed = val => {
console.log("onFinishFailed", val); //sy-log
};
render () {
return (
<div>
<h3>MyRCFieldForm</h3>
<Form
ref={this.formRef}
onFinish={this.onFinish}
onFinishFailed={this.onFinishFailed}>
<Field name="username" rules={[nameRules]}>
<Input placeholder="Username" />
</Field>
<Field name="password" rules={[passworRules]}>
<Input placeholder="Password" />
</Field>
<button>Submit</button>
</Form>
</div>
);
}
}
实现 my-rc-field-form
实现 Form/index
import React from "react";
import _Form from "./Form";
import Field from "./Field";
import useForm from "./useForm";
const Form = React.forwardRef(_Form);
Form.Field = Field;
Form.useForm = useForm;
export { Field, useForm };
export default Form;
实现 Form
import React from "react";
import useForm from "./useForm";
import FieldContext from "./FieldContext";
export default function Form ({ children, onFinish, onFinishFailed, form }, ref) {
const [formInstance] = useForm(form);
React.useImperativeHandle(ref, () => formInstance);
formInstance.setCallback({
onFinish,
onFinishFailed
});
return (
<form
onSubmit={event => {
event.preventDefault();
event.stopPropagation();
formInstance.submit();
}}>
<FieldContext.Provider value={formInstance}>
{children}
</FieldContext.Provider>
</form>
);
}
实现 FieldContext
import React from "react";
const warnFunc = () => {
console.log("----------err--------"); //sy-log
};
const FieldContext = React.createContext({
registerField: warnFunc,
setFieldsValue: warnFunc,
getFieldValue: warnFunc,
getFieldsValue: warnFunc,
submit: warnFunc
});
export default FieldContext;
实现 useForm
import React from "react";
class FormStore {
constructor() {
this.store = {}; //存储数据,⽐比如说username password
this.fieldEntities = [];
// callback onFinish onFinishFailed
this.callbacks = {};
}
// 注册
registerField = entity => {
this.fieldEntities.push(entity);
return () => {
this.fieldEntities = this.fieldEntities.filter(item => item !== entity);
delete this.store[entity.props.name];
};
};
setCallback = callback => {
this.callbacks = {
...this.callbacks,
...callback
};
};
// 取数据
getFiledValue = name => {
return this.store[name];
};
getFiledsValue = () => {
return this.store;
};
// 设置数据
setFieldsValue = newStore => {
this.store = {
...this.store,
...newStore
};
this.fieldEntities.forEach(entity => {
const { name } = entity.props;
Object.keys(newStore).forEach(key => {
if (key === name) {
entity.onStoreChange();
}
});
});
};
validate = () => {
let err = [];
// todo
this.fieldEntities.forEach(entity => {
const { name, rules } = entity.props;
let value = this.getFiledValue(name);
let rule = rules && rules[0];
if (rule && rule.required && (value === undefined || value === "")) {
// 出错
err.push({
[name]: rules.message,
value
});
}
});
return err;
};
submit = () => {
console.log("this.", this.fieldEntities); //sy-log
let err = this.validate();
// 在这⾥里里校验 成功的话 执⾏行行onFinish ,失败执⾏行行onFinishFailed
const { onFinish, onFinishFailed } = this.callbacks;
if (err.length === 0) {
// 成功的话 执⾏行行onFinish
onFinish(this.getFiledsValue());
} else if (err.length > 0) {
// ,失败执⾏行行onFinishFailed
onFinishFailed(err);
}
};
getForm = () => {
return {
registerField: this.registerField,
setCallback: this.setCallback,
submit: this.submit,
getFiledValue: this.getFiledValue,
getFiledsValue: this.getFiledsValue,
setFieldsValue: this.setFieldsValue
};
};
}
// ⾃自定义hook
export default function useForm (form) {
const formRef = React.useRef();
if (!formRef.current) {
if (form) {
formRef.current = form;
} else {
// new ⼀一个
const formStore = new FormStore();
formRef.current = formStore.getForm();
}
}
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.cancelRegisterField = registerField(this);
}
componentWillUnmount () {
if (this.cancelRegisterField) {
this.cancelRegisterField();
}
}
onStoreChange = () => {
this.forceUpdate();
};
getControlled = () => {
const { name } = this.props;
const { getFiledValue, setFieldsValue } = this.context;
return {
value: getFiledValue(name), //取数据
onChange: event => {
// 存数据
const newValue = event.target.value;
setFieldsValue({ [name]: newValue });
}
};
};
render () {
console.log("field render"); //sy-log
const { children } = this.props;
const returnChildNode = React.cloneElement(children,
this.getControlled());
return returnChildNode;
}
}
实现 my-rc-form
import React, { Component } from "react";
// import {createForm} from "rc-form";
import createForm from "../components/my-rc-form/";
import Input from "../components/Input";
const nameRules = { required: true, message: "请输⼊入姓名!" };
const passworRules = { required: true, message: "请输⼊入密码!" };
@createForm
class MyRCForm extends Component {
constructor(props) {
super(props);
// this.state = {
// username: "",
// password: ""
// };
}
componentDidMount () {
this.props.form.setFieldsValue({ username: "default" });
}
submit = () => {
const { getFieldsValue, validateFields } = this.props.form;
// console.log("submit", getFieldsValue()); //sy-log
validateFields((err, val) => {
if (err) {
console.log("err", err); //sy-log
} else {
console.log("校验成功", val); //sy-log
}
});
};
render () {
console.log("props", this.props); //sy-log
// const {username, password} = this.state;
const { getFieldDecorator } = this.props.form;
return (
<div>
<h3>MyRCForm</h3>
{getFieldDecorator("username", { rules: [nameRules] })(
<Input placeholder="Username" />
)}
{getFieldDecorator("password", { rules: [passworRules] })(
<Input placeholder="Password" />
)}
<button onClick={this.submit}>submit</button>
</div>
);
}
}
export default MyRCForm;
实现:
import React, { Component } from "react";
export default function createForm (Cmp) {
return class extends Component {
constructor(props) {
super(props);
this.state = {};
this.options = {};
}
handleChange = e => {
const { name, value } = e.target;
this.setState({ [name]: value });
};
getFieldDecorator = (field, option) => InputCmp => {
this.options[field] = option;
return React.cloneElement(InputCmp, {
name: field,
value: this.state[field] || "",
onChange: this.handleChange
});
};
setFieldsValue = newStore => {
this.setState(newStore);
};
getFieldsValue = () => {
return this.state;
};
validateFields = callback => {
// 自己想象吧~
};
getForm = () => {
return {
form: {
getFieldDecorator: this.getFieldDecorator,
setFieldsValue: this.setFieldsValue,
getFieldsValue: this.getFieldsValue,
validateFields: this.validateFields
}
};
};
render () {
return <Cmp {...this.props} {...this.getForm()} />;
}
};
}