前言
本篇主要对React的状态进行一个介绍。
声明式 UI 与命令式 UI 的比较
当你设计 UI 交互时,可能会去思考 UI 如何根据用户的操作而响应变化。想象一个允许用户提交一个答案的表单:
当你向表单输入数据时,“提交”按钮会随之变成可用状态
当你点击“提交”后,表单和提交按钮都会随之变成不可用状态,并且会加载动画会随之出现
如果网络请求成功,表单会随之隐藏,同时“提交成功”的信息会随之出现
如果网络请求失败,错误信息会随之出现,同时表单又变为可用状态
在 命令式编程 中,以上的过程直接告诉你如何去实现交互。你必须去根据要发生的事情写一些明确的命令去操作 UI。
而在Vuejs渐进式框架和React库里面都是采用的声明式编程。
声明式地考虑 UI
你已经从上面的例子看到如何去实现一个表单了,为了更好地理解如何在 React 中思考,一般情况下要用 React 重新实现这个 UI就要经过这几个一般步骤:
定位你的组件中不同的视图状态
确定是什么触发了这些 state 的改变
表示内存中的 state(需要使用 useState)
删除任何不必要的 state 变量
连接事件处理函数去设置 state
React的状态管理
在React中,状态管理是一个非常重要的概念,React的状态管理是通过state
来实现的。
state
state
是React组件中的一个内置对象,用于存储组件内部的状态。state
是一个普通的JavaScript对象,我们可以通过this.state
来访问它。
React 会为 UI 中的组件结构构建 渲染树。
当向一个组件添加状态时,那么可能会认为状态“存在”在组件内。但实际上,状态是由 React 保存的。React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。
这边与Vuejs的区别是,Vue的响应式系统是与组件进行隔离开来的,而React的状态是与组件进行绑定的。
我们将state嵌入进React的组件里面,作为组件vnode的一部分信息,当React的state更新时,也意味着组件的信息更新,会触发组件的重新渲染。
如果理解了这一点,那么我们就可以很轻易的理解React的响应式是怎么工作的了,React的状态是与组件绑定的,当状态更新时,组件也会更新,这也是React的响应式系统。
我们来具体给一个例子:
import { useState } from 'react';
export default function App() {
const counter = <Counter />;
return (
<div>
{counter}
{counter}
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
下面是它们的树形结构的样子:
这是两个独立的 counter,因为它们在树中被渲染在了各自的位置。 一般情况下你不用去考虑这些位置来使用 React,但知道它们是如何工作会很有用。
而他们有各自的状态,因为每个组件实例都有自己的 state。 你可以在一个组件中使用多个 state 变量。当我更新了state之后,会触发组件的重新渲染,这边我们可以看到,两个counter的状态是独立的,互不影响。
相同位置的相同组件会使得 state 被保留下来
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
使用好看的样式
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
那么相同位置的不同组件会使 state 重置,这一点应该大家也能够知道为什么了。
再举一个例子:将一个<Counter>
替换为一个 <p>
当 Counter 变为 p 时,Counter 会被移除,同时 p 被添加。
当切换回来时,p 会被删除,而 Counter 会被添加
参考的React文档在这里:状态与渲染树中的位置相关
提取状态逻辑到 reducer 中
对于那些需要更新多个状态的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以在组件外部将所有状态更新逻辑合并到一个称为 “reducer”
的函数中。这样,事件处理程序就会变得简洁,因为它们只需要指定用户的 “actions”
。在文件的底部,reducer
函数指定状态应该如何更新以响应每个 action
!
useReducer 是 React 中的一个 Hook,用于管理组件的状态逻辑。它提供了一种更复杂状态逻辑的组织方式,适用于那些包含多个子值或需要根据先前的状态来更新的状态。
useReducer 的使用方式类似于 Redux
中的 reducer
概念。
下面是 useReducer
的基本用法:
import React, { useReducer } from 'react';
// reducer函数,接受当前state和action,返回新的state
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
const initialState = { count: 0 };
const Counter = () => {
// 使用useReducer,传入reducer和初始state
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
</div>
);
};
export default Counter;
主要的步骤包括:
定义
reducer
函数: 这个函数接收当前状态state
和一个描述动作的action
,然后根据action
的类型执行相应的逻辑,并返回新的状态。初始化状态: 通过调用
useReducer(reducer, initialState)
来创建状态和dispatch
函数。在组件中使用状态和
dispatch
: 通过state
访问当前状态值,通过dispatch
触发reducer
执行。
使用 Context 进行深层数据传递
通常,你会通过 props 将信息从父组件传递给子组件。 但是,如果要在组件树中深入传递一些 prop,或者树里的许多组件需要使用相同的 prop,那么传递 prop 可能会变得很麻烦。Context 允许父组件将一些信息提供给它下层的任何组件,不管该组件多深层也无需通过 props 逐层透传。
import React, { createContext, useContext, useState } from 'react';
// 创建一个Context
const MyContext = createContext();
// 创建一个包含Provider和Consumer的组件
const MyProvider = ({ children }) => {
const [data, setData] = useState('Hello from Context');
return (
<MyContext.Provider value={{ data, setData }}>
{children}
</MyContext.Provider>
);
};
// 使用 useContext 获取Context中的值
const ChildComponent = () => {
const { data, setData } = useContext(MyContext);
return (
<div>
<p>{data}</p>
<button onClick={() => setData('Updated Context')}>Update Context</button>
</div>
);
};
// 在应用中使用Provider包裹需要共享数据的组件
const App = () => {
return (
<MyProvider>
<ChildComponent />
</MyProvider>
);
};
export default App;
使用 Reducer 和 Context 进行状态扩展
Reducer 帮助你合并组件的状态更新逻辑同时Context 帮助你将信息深入传递给其他组件。
你可以将 reducers 和 context 组合在一起使用,以管理复杂应用的状态。
基于这种想法,使用 reducer 来管理一个具有复杂状态的父组件。组件树中任何深度的其他组件都可以通过 context 读取其状态。还可以 dispatch 一些 action 来更新状态。
合理的使用 Reducer 和 Context 可以帮助你更好的管理状态,同时也可以让你的代码更加简洁。
相比于Vuejs基于Proxy的响应式系统,使用代理的方式,以及provide和inject的方式,Vuejs的状态管理是在一个单独的WeekMap里面进行管理的,当变量的状态进行改变,可以激活effect副作用函数,并进行值的更新。而React的状态管理是与组件绑定的,当状态更新时,组件也会更新,但是当我们要进行一个全局状态的管理时,就得进行在父组件里面定义。
能基于具体的情况进行React状态的定义,才能更好的进行状态的管理。