React组件的状态管理是一个很重要的内容。从字面来理解,按钮是否可单击、图片是否显示等,这些都是状态。广义来讲,React组件的状态还1包括传入React的数据,例如某个组件要展示列表,列表的数据也是该组件的状态。总之,状态就是React UI中的数据。没有数据的项目是没有实际意义的,React的状态管理就是解决组件内部、组件之间的通信。React状态管理的重要性由此可见一斑。
5.3.1 state
在React class组件时代,状态就是this.state,使用this.setState进行更新。在Hooks中使用useState来获取和更新组件state。
React不会直接从代码中修改UI。例如,不会编写诸如"禁用按钮""启用按钮"和"显示成功消息"等命令。相反,React将描述组件的不同视觉状态的UI("初始状态""输入状态"和"成功状态"),通过用户输入触发状态更改。
software-labs-client工程中的src/features/bar/components/Search.js组件是一个使用React构建的搜索按钮。Search.js的部分代码如下(注意它是如何使用状态变量来确定是启用还是禁用搜索按钮的):
01 import React, { useState, useEffect } from 'react';
02 import { useSelector } from 'react-redux';
03 import { FormattedMessage } from 'react-intl';
04 import _ from 'lodash';
05 import { selectPreferences } from '../../wrappers/selector';
06 import { Button } from 'antd';
07
08 const Search = () => {
09 const preferences = useSelector(selectPreferences);
10 const countryCode = preferences.countryCode;
11 const [disabled, setDisabled] = useState(false);
12
13 useEffect(() => {
14 if(!countryCode){
15 setDisabled(true);
16 return;
17 }
18 setDisabled(false);
19 }, [countryCode])
20
21 const handleSearch = async () => {
22 if(disabled){
23 return;
24 }
25 //发送ajax请求
26 ......
27 }
28
29 return (
30 <>
31 <Button
32 id="search-button"
33 type="primary"
34 onClick={handleSearch}
35 disabled={disabled}
36 >
37 <FormattedMessage id="SEARCH"/></Button>
38 </>
39 );
40 }
41
42 export default Search;
代码解析:
l 第01行引入React、useState、useEffect。
l 第02行引入useSelector。
l 第03行引入FormattedMessage,用于对文案进行国际化处理。
l 第04行引入lodash工具库。
l 第05行引入selectPreferences函数,用于从Redux中查找preferences对象,该对象保存了用户当前的区域、国家和语言。
l 第06行从antd中引入Button组件。
l 第08~40行是Search组件的内容。
l 第09行从Redux中获取preferences。
l 第10行preferences中获取countryCode值。
l 第11行通过useState设置state状态,disabled的值为false,设置disabled值的函数是setDisabled。
l 第13~19行是useEffect的调用。如果countryCode为空,则设置disabled的值为true,即按钮不可单击。
5.3.2 props
有了状态与组件,自然就有了状态在组件间的传递,一般称为 "通信"。
父子通信通过props传递,比较简单。例如在software-labs-client项目中,打开src/features/ feedback/index.js文件,其中定义了FeedBack组件,代码如下:
01 import React, { useState } from 'react';
02 import _ from 'lodash';
03 import './index.scss';
04 import feedback_icon from '../../assets/images/feedback-icon.png'
05 import FeedBackModal from './components/FeedBackModal';
06
07 const FeedBack = () => {
08 const [visible, setVisible] = useState(false);
09
10 const handleCancel = () => {
11 setVisible(false);
12 }
13
14 const handleOk = () => {
15 setVisible(false);
16 }
17
18 const openModal = () => {
19 setVisible(true);
20 }
21
22 return (
23 <div id="feedBackMod">
24 <button type="button" className="btn btn-feedback" onClick={openModal}>
25 <img src={feedback_icon} className="mr-2"/>
26 <span>FEEDBACK</span>
27 </button>
28 <FeedBackModal visible={visible} handleCancel={handleCancel} handleOk={handleOk}/>
29 </div>
30 );
31 }
32 export default FeedBack;
代码解析:
l 第24行,单击FEEDBACK按钮,执行click事件,调用openModal函数。
l 第28行父组件向子组件FeedBackModal/传递3个值,其中visible是布尔值,handleCancel和handleOk是函数。
FeedBack组件如图5.7所示,初始时只展示FEEDBACK按钮。
在FeedBackModal子组件中,通过props接收父组件FeedBack传递的值,FeedBackModal子组件的位置是src/features/feedback/ components/FeedBackModal.js,代码如下:
01 import React, { useState, useEffect } from 'react';
02 import _ from 'lodash';
03 import { Modal } from 'antd';
04 import './FeedBackModal.scss';
05
06 const FeedBackModal = (props) => {
07 const { visible, handleCancel, handleOk } = props;
08
09 useEffect(() => {
10 //......
11 }, [visible])
12
13 const ModalHeader = () => {
14 //......
15 }
16 const submit = async (v) => {
17 //......
18 handleOk();
19 }
20 const modalCancel = async () => {
21 //......
22 handleCancel()
23 }
24 return (
25 <Modal
26 title={ModalHeader()}
27 open={visible}
28 onCancel={modalCancel}
29 wrapClassName="feedBackModal"
30 footer={null}
31 >
32 //......
33 <button type="submit" onClick={submit}>SEND FEEDBACK</button>
34 </Modal>
35 )
36 }
37
38 export default FeedBackModal;
代码解析:
l 第03行引入antd的Modal组件。
l 第06~36行是子组件FeedBackModal的内容。其中第07行通过props获取父组件传递来的变量,分别是visible、handleCancel、handleOk;在第09~11行的useEffect函数中监测visible的变化并执行相关逻辑。
l 第33行,单击SEND FEEDBACK按钮后调用submit函数提交用户的反馈信息。第16~19行定义了submit函数,在这个函数中调用父组件传递的handleOk函数。
l 第28行,onCancel是antd的Modal组件自定义的属性,Modal是一个弹窗,单击弹窗中的取消按钮时调用modalCancel函数。第20~23行定义了modalCancel函数,在该函数中调用了父组件传递的handleCancel函数。
FeedBackModal组件如图5.8所示,单击FEEDBACK按钮时,visible为true,重新渲染组件后展示FeedBackModal;单击右上角的关闭按钮时,调用handleCancel函数关闭弹窗。
对于深层级、远距离组件之间的通信,可以采用"状态提升"+ props逐层传递的方式。也就是说将state从子组件中清除,并将其移动到最接近的公共父组件中,然后通过props逐层向下传递给子组件。这种方式被称为提升状态,是编写React代码时最常见的事情之一。
5.3.3 context
当组件之间共享的数据很多时,利用props逐层传递就显得既复杂又难以维护。于是React引入了context,一个用于解决组件"跨级"通信的官方方案。
context一般在顶层组件创建,方便数据的全局注入和全局共享。例如在顶层组件App.js中创建context,代码如下:
01 import React,{Component,createContext} from 'react';
02 import B from './B.js';
03 export const GlobalContext = createContext({name:'scw'});
04 class App extends Component {
05 constructor(props) {
06 super(props);
07 }
08 handleClick = (e) => {
09 console.log('组件B单击了');
10 }
11 render() {
12 return (
13 <GlobalContext.Provider value={{name:'scw1',onClick:this.handleClick.bind(this)}}>
14 <B />
15 </GlobalContext.Provider>
16 )
17 }
18 }
19 export default App;
代码解析:
l 第1行引入createContext。
l 第2行引入B组件。
l 第3行用createContext创建context的初始默认值。参数可以是对象、字符串等任意类型的值。
l 第13行使用GlobalContext.Provider进行context全局注入,这里的value表示对context重新赋值。使用Provider在顶层组件注入context数据后,里层的所有子组件及其后代组件均可访问到对应的context数据。当Provider的value值发生变化时,它内部的所有消费组件都会重新渲染。
在B组件中消费context,代码如下:
01 import { GlobalContext } from './App';
02 class B extends Component {
03 render() {
04 return <GlobalContext.Consumer>
05 {
06 (globalContext) =>
07 <span onClick={globalContext.onClick}> {globalContext.name} </span>
08 }
09 </GlobalContext.Consumer>
10 }
11 }
12 export default B;
代码解析:
l 在第7行消费context的数据、onClick和name。
虽然context可以在全局注入变量使所有组件共享状态,但是并不建议大量使用context。因为尽管它可以减少逐层传递,但当组件结构复杂时,我们并不知道context是从哪里传过来的。context就像一个全局变量,而全局变量正是导致应用走向混乱的原因之一,给组件带来了外部依赖的副作用。因此,真正意义上的全局信息且信息不会更改,例如界面主题、用户信息等,才应该使用context。
本文节选自《React.js+Node.js+MongoDB企业级全栈开发实践》,获出版社和作者授权共享。