React-状态管理

useState

状态,即state。在React中,每当State改变时,都会触发函数组件重新渲染,以此来更新视图。
React通过useState来创建一个State,该函数接收一个参数作为State的初始值,并返回一个包含State和修改State的函数的数组,即:
const [state, setState] = useState(0) console.log(state) // 0 setState(1) console.log(state) // 1
如果在同一时刻需要基于原来的值修改State,则需要传递一个函数作为参数。
// ✖ function changeState(){ setState(state + 1) setState(state + 1) setState(state + 1) } // 1 // ✔ function changeState(){ setState((value)=> value + 1) setState((value)=> value + 1) setState((value)=> value + 1) } // 3
上面例子中,多次调用setState(state + 1) 无法按我们所想地修改state的值,这是因为当调用setState函数时,并不会理解更改state的值,官方推荐的方法是传递一个函数,即如第二个函数那样。
可以查看下面的示例:

选择State结构

当你组织State结构时应该尽量遵循以下原则:
  1. 组合相关状态。如果始终同时更新两个或多个状态变量,请考虑将它们合并到单个状态变量中。
  1. 避免状态矛盾。当状态的结构方式是,几个状态部分可能相互矛盾和“不同意”时,你就会留下错误的余地。尽量避免这种情况。
  1. 避免冗余状态。如果可以在渲染期间从组件的属性或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。
  1. 避免状态重复。当相同的数据在多个状态变量之间或在嵌套对象内重复时,很难使它们保持同步。尽可能减少重复。
  1. 避免深度嵌套状态。深层分层状态更新不是很方便。如果可能,最好以扁平的方式构建状态。

在组件之间共享状态

提取状态

假如在App组件下有A组件和B组件,两个组件中都有状态index,当改变其中一个组件的index状态时,另一个子组件(A或者B组件)也应该跟着改变。
要实现这个功能,我们首先应该进行状态提升,即将index状态提升到最近的公共组件中,这里很明显它们的公共组件是App组件,然后通过Props传递index状态到两个子组件。
还有一个问题是如何改变状态,就是说当用户点击子组件时,如何通知父组件调用setIndex更改状态的值?解决的方式是将事件处理程序作为Prop传递到子组件中,在这个事件处理程序的内部会调用setIndex函数来改变index状态的值,只要子组件调用这个事件处理程序,就会改变父组件的index状态,从而使两个子组件的index值都改变。
// App.tsx import "./styles.css"; import { A } from "./A"; import { B } from "./B"; import { useState } from "react"; export default function App() { const [index, setIndex] = useState(0); return ( <div className="App"> <A index={index} onChangeIndex={() => setIndex((val) => val + 1)} /> <B index={index} onChangeIndex={() => setIndex((val) => val + 1)} /> </div> 0 ); } // A.tsx export function A({ index, onChangeIndex }: any) { return ( <div> <p>{index}</p> <button onClick={onChangeIndex}>改变A组件的Index</button> </div> ); } // B.tsx export function B({ index, onChangeIndex }: any) { return ( <div> <p>{index}</p> <button onClick={onChangeIndex}>改变B组件的Index</button> </div> ); }
附完整示例:
 

受控组件和非受控组件

通常认为具有本地状态的组件是非受控组件,相反如果有某些重要信息由Props而不是组件自身的本地状态驱动的,则称这些组件为受控组件。
例如上面的A组件和B组件就是受控组件,它们的重要状态受控于父组件。
不受控制的组件更易于在其父级组件中使用,因为它们需要的配置更少。但是,当你想要将它们协调在一起时,它们的灵活性较低。受控组件具有更大的灵活性,但它们需要在父组件使用 props 来配置它们。

单一事实来源

对于每个唯一的状态,你将选择“拥有”它的组件。这一原则也被称为“单一事实来源”。这并不意味着所有状态都存在于一个地方,而是对于每个状态,都有一个特定的组件来保存该信息。您将组件提升到其公共共享父级,并将其传递给需要它的子级,而不是在组件之间复制共享状态。

保留和重置组件状态

组件在UI树中的位置

假设我们有一个子组件Child。
// Child.tsx import { useState } from "react"; export function Child() { const [index, setIndex] = useState(0); return ( <div> <span>{index}</span> <button onClick={() => setIndex((val) => val + 1)}>+1</button> </div> ); }
那么在父组件挂载多个Child组件时,不同的Child组件有着自己的本地状态,互不干扰。
// App.tsx import "./styles.css"; import { Child } from "./Child"; export default function App() { return ( <div className="App"> <Child /> <Child /> </div> ); }
notion image
当点击第一个+1按钮时,并不会影响第二个Child的index状态。你可能会认为index状态“存在于”组件内部。但状态实际上是在 React 内部保持的。React 会根据该组件在 UI 树中的位置,将它所持有的每一段状态与正确的组件相关联。
对于在UI树相同位置的相同组件,React会保留组件状态,而其他情况(相同位置不同的组件或者不同位置相同的组件),React都不会保留组件状态。注意这里说的是UI树的位置,而不是JSX的位置。
当我们通过&&动态地删除和挂载组件时,组件原先的状态会被重置,换句话说就是组件会被删除,然后重新创建挂载,重新创建挂载的时候又会自动初始化状态。
// App.tsx import "./styles.css"; import { Child } from "./Child"; import { useState } from "react"; export default function App() { const [isShow, setIsShow] = useState(true); return ( <div className="App"> <div> <button onClick={() => setIsShow((val) => !val)}>切换isShow</button> </div> {isShow && <Child />} </div> ); }
点击+1按钮改变Child的index状态,当点击一个切换isShow 按钮后删除组件,在点击一次会重新创建组件,此时原本的index状态已经重置,在视图的表现是index重置为0。
在React还可以用三目运算符来根据条件选择组件:
// App.tsx import "./styles.css"; import { Child } from "./Child"; import { useState } from "react"; export default function App() { const [isShow, setIsShow] = useState(true); return ( <div className="App"> <div> <h3>&&</h3> <div> <button onClick={() => setIsShow((val) => !val)}>切换isShow</button> </div> {isShow ? <Child /> : <Child />} {isShow ? <Child /> : <div><Child /></div>} </div> </div> ); }
这里特意用了两组对照,一个是两个条件都是<Child />组件,另一组是<Child />div包裹的<Child /> 组件,结果是第一组切换isShow时index状态会保持原来的状态,而第二组切换isShow会重置状态。

相同位置相同组件重置状态

某些时候,你可能想相同位置相同组件的情况下重置状态,这个时候有两种方法:
1. 在不同位置呈现组件
{isShow ? <Child /> : <Child />} // 改写成 {isShow && <Child /> } {!isShow && <Child />}
2.使用key
{isShow ? <Child /> : <Child />} // 改写成 {isShow ? <Child key="a" /> : <Child key="b" />}

重置状态

有些情况下我们需要重置组件状态,例如表单提交后重置表达状态。这个时候可以使用key来重置状态。将State作为组件的key,只要调用setState函数,就会改变key值,从而重置组件状态。
export default function App() { const [init, setInit] = useState(true); return ( <div className="App"> <div> <div> <button onClick={() => setInit((val) => !val)}>重置组件状态</button> </div> <Child key={init} /> </div> </div> ); }

保留组件状态

除了重置状态,很多时候我们也需要保留组件的状态,这个时候有三种常见的方法:
  1. 通过css控制组件显示和隐藏,而不是直接操作DOM节点。
  1. 将状态提升到父组件。
  1. 将重要状态信息存储到LocalStorage中。

使用useReducer整合状态逻辑

在复杂的组件中,可能存在大量的状态,根据前面所说的“组合相关状态”原则,我们应该将相同逻辑的状态组合为单一状态。但是即使将状态组合起来了,组件中仍然会有大量更改状态的事件处理程序,这个时候我们可以使用useReducer来组合这些的状态更新逻辑。
假设我们需要组合id、time、title三个状态,并且还需要有状态更改的事件处理程序,我们的代码可能是这样的:
import { useReducer, useState } from "react"; import "./styles.css"; export default function App() { const [data, setData] = useReducer({ id: 0, title: "Hello", time: new Date() }); function updateTitile() { // 更新逻辑代码 } function updaeDate() { // 更新逻辑代码 } function addId() { // 更新逻辑代码 } return ( <div className="App"> <h1> {data.id} {data.title} </h1> <p>time: {data.time.getTime()}</p> <div> <button onClick={addId}>ID + 1</button> <button onClick={updaeDate}>update Date</button> <button onClick={updateTitile}>update Title</button> </div> </div> ); }
 
notion image
这样能满足我们的要求,但是随着此组件的增长,散布在其中的状态逻辑的数量也会增加。为了降低这种复杂性并将所有逻辑保存在一个易于访问的位置,您可以将该状态逻辑移动到组件外部的单个函数中,称为“reducer”。
使用Reducer管理状态与直接设置状态略有不同。不是通过设置状态来告诉 React“要做什么”,而是通过从事件处理程序中调度“操作”来指定“用户刚刚执行的操作”。
import { useReducer, useState } from "react"; import "./styles.css"; export default function App() { function dataReducer(state: any, action: any) { switch (action.type) { case "add_id": { return { ...state, id: state.id + 1 }; } case "update_time": { return { ...state, time: action.time }; } case "update_title": { return { ...state, title: action.title }; } default: { throw Error('Unknown action: ' + action.type); } } } const [data, dispatch] = useReducer(dataReducer, { id: 0, title: "Hello", time: new Date() }); function updateTitile() { dispatch({ type: "update_title", title: data.title === "Hello" ? "World" : "Hello" }); } function updaeDate() { dispatch({ type: "update_time", time: new Date() }); } function addId() { dispatch({ type: "add_id" }); } return ( <div className="App"> <h1> {data.id} {data.title} </h1> <p>time: {data.time.getTime()}</p> <div> <button onClick={addId}>ID + 1</button> <button onClick={updaeDate}>update Date</button> <button onClick={updateTitile}>update Title</button> </div> </div> ); }
在上面的代码中使用useReducer来替代useState,并实现了dataReducer 这个Reducer函数,dataReducer函数里定义了状态更改的“操作”,然后在事件处理程序中通过“dispatch”来触发这些“操作”。
useReducer函数使用时需要传递一个Reducer函数和初始值,函数返回值类似useState,不过两者还是有所区别。
const [data, dispatch] = useReducer(dataReducer, { id: 0, title: "Hello", time: new Date() }); const [data, setData] = useState({ id: 0, title: "Hello", time: new Date() });
useReducer函数返回的数组的第二个元素是dispatch函数,与setData函数不同,它并不会直接更改data状态,而是告诉Reducer应该执行哪些“操作”。
dispatch 调用时需要传递一个对象作为参数,这个对象包含:
  1. type: 描述要执行哪种操作。
  1. 要更新的数据(可选)
dispatch({ type: "update_time", time: new Date() });
而所谓的“操作”,则是提前定义在Reducer函数里。
function dataReducer(state: any, action: any) { switch (action.type) { case "add_id": { return { ...state, id: state.id + 1 }; } case "update_time": { return { ...state, time: action.time }; } case "update_title": { return { ...state, title: action.title }; } default: { throw Error('Unknown action: ' + action.type); } } }
它有两个参数:stateactionstate是状态,action则是dispatch传递的参数。Reducer函数的返回值就是状态更新后的值,这一点和setData函数相同。
Reducer有两个原则:
  1. Reducer函数必须是“纯”的,即相同的输入总是产生相同的输出,不应该产生副作用。
  1. 每个action都描述单个用户交互,即使这会导致数据中的多个更改。
附完整示例:

通过context深入传递数据

有时候我们需要将状态传递到子孙组件(子组件的子组件)中,这个时候如果仍然通过Props来传递就会比较麻烦,我们可以使用React的Context来传递状态。
使用context只需要三步:
  1. 创建context(createContext)
  1. 使用context(useContext)
  1. 提供context(Context.Provider)