11React 組件化
資源:
組件化
快速開始
https://www.html.cn/create-react-app/docs/getting-started/
npx create-react-app reactDemo
cd reactDemo
yarn start
組件化優(yōu)點
增強代碼重用性嫉入,提高開發(fā)效率豁遭;
簡化調試步驟糠涛,提升整個項目的可維護性;
便于協(xié)同開發(fā)驻债;
注意點:降低耦合性乳规。
組件跨層級通信 - Context
在一個典型的 React 應用中,數據是通過 props 屬性自上而下(由父及子)進行傳遞的合呐,但這種做法對于某些類型的屬性而言是極其繁瑣的(例如:地區(qū)偏好暮的,UI 主題),這些屬性是應用程序中許多組件都需要的淌实。Context 提供了一種在組件之間共享此類值的方式冻辩,而不是顯式地通過組件數的逐層傳遞 props。
React 中使用 Context 實現(xiàn)祖代組件向后代組件跨層級傳值拆祈,Vue 中的 provide & inject 來源于 Context恨闪。
Context API
React.createContext
創(chuàng)建一個 Context 對象,當 React 訂閱了這個 Context 對象的組件放坏,這個組件會從組件樹中離自身最近的那個匹配的 Provide 中讀取到當前的 context 值咙咽。
Context.Provider
Provider 接收一個 value 屬性,傳遞給消費組件淤年,允許消費組件訂閱 context 的變化钧敞。一個 Provider 可以和多個消費組件有對應關系。多個 Provider 也可以嵌套使用麸粮,里層的會覆蓋外層的數據溉苛。
當 Provider 的 value 的值發(fā)生變化時,它內部的所有消費組件都會重新渲染豹休。Provider 及其內部 consumer 組件都不受制于 shouldComponentUpdate 函數炊昆,因此當 consumer 組件在其祖先組件退出更新的情況下也能更新。
Class.contextType
掛載在 class 上的 contextType 屬性會被重賦值為一個由 React.createContext() 創(chuàng)建的 Context 對象威根。這能讓你使用 this.context 來消費最近 Context 上的那個值凤巨。你可以在任何生命周期中訪問到它,包括 render 函數中洛搀。
你只通過該 API 訂閱單一 context敢茁。
Context.Consumer
這里,React 組件也可以訂閱到 context 變更留美,這能讓你在函數式組件中完成訂閱 context彰檬。
這個函數接收當前的 context 值,返回一個 React 節(jié)點谎砾。傳遞給函數的 value 值等同于往上組件樹離這個 context 最近的 Provider 提供的 value 值逢倍。如果沒有對應的 Provider,value 參數等同于傳遞給 createContext() 的 defaultValue景图。
useContext
接收一個 context 對象(React.createContext 的返回值)并返回該 context 的當前值较雕。當前 context 值由上層組件中距離當前組件最近的 <MyContext.Provider> 的 value prop 決定。只能用在 function 組件中。
使用 Context
創(chuàng)建 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 值經常被一起使用亮蒋,那你可能要考慮一下另外創(chuàng)建你自己的渲染組件扣典,以提供這些值。
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 組件中觸發(fā)意外的渲染趁怔。舉個例子湿硝,當每一次 Provider 重渲染時,以下的代碼會重渲染所有下面的 consumers 組件痕钢,因為 value 屬性總是被賦值為新的對象:
class App extends React.Component {
render () {
return (
<Provider value={{ something: 'something' }}>
<Toolbar />
</Provider>
)
}
}
為了防止這種情況图柏,將 value 狀態(tài)提升到父節(jié)點的 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 中出現(xiàn)的裝飾器語法來更優(yōu)雅的書寫代碼。
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 上矗积,執(zhí)行順序從下往上。
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 算法(稱為協(xié)調)使用組件標識來確定它是應該更新現(xiàn)有子樹還是將其丟棄并掛載新子樹茵烈。如果從 render 返回的組件與前一個渲染中的組件相同(===),則 React 通過將子樹與新子樹進行區(qū)分來遞歸更新子樹砌些。如果他們不相等瞧毙,則完全卸載前一個子樹。
render() {
// 每次調?用 render 函數都會創(chuàng)建?一個新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 這將導致子樹每次渲染都會進行卸載寄症,和重新掛載的操作宙彪!
return <EnhancedComponent />;
}
這不僅僅是性能問題,重新掛載組件會導致該組件及其所有子組件的狀態(tài)丟失有巧。
表單組件設計與實現(xiàn)
antd 表單使用
實現(xiàn)用戶名密碼登錄释漆,并實現(xiàn)校驗
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 實現(xiàn):
注意 useForm 是 React Hooks 的實現(xiàn)男图,只能用于函數組件。
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 表單組件設計思路
表單組件要求實現(xiàn)數據收集甜橱、校驗逊笆、提交等特性,
可通過高階組件擴展高階組件給表單組件傳遞一個 input 組件包裝函數接管其輸入事件并統(tǒng)一管理表單數據高階組件給表單組件傳遞一個校驗函數使其具備數據校驗功能
但是 antd3 的設計有個問題岂傲,就是局部變化會引起整體變化难裆,antd4 改進了這個問題。
antd4 表單組件實現(xiàn)
antd4 的表單實現(xiàn)基于 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
};
// 表單校驗失敗執(zhí)?行行
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 實現(xiàn):
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
};
// 表單校驗失敗執(zhí)?行行
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>
);
}
}
實現(xiàn) my-rc-field-form
實現(xiàn) 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;
實現(xiàn) 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>
);
}
實現(xiàn) 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;
實現(xiàn) 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();
// 在這?里里校驗 成功的話 執(zhí)?行行onFinish ,失敗執(zhí)?行行onFinishFailed
const { onFinish, onFinishFailed } = this.callbacks;
if (err.length === 0) {
// 成功的話 執(zhí)?行行onFinish
onFinish(this.getFiledsValue());
} else if (err.length > 0) {
// 归薛,失敗執(zhí)?行行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];
}
實現(xiàn) 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;
}
}
實現(xiàn) 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;
實現(xiàn):
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()} />;
}
};
}