在使用react和redux的过程中,一直有一个问题,哪些状态需要放在redux中,状态需要保存在组件内的local state中,此外不合理的使用redux可能会带来状态管理混乱的问题,此外对于local state局部状态而言,react hooks提供了一个比class中的setState更好的一个替代方案。本文主要从状态管理出发,讲讲如何优雅的使用hooks来进行状态管理。

  • 如何使用redux

  • react hooks管理local state

  • react hooks如何解决组件间的通信

原文在我的博客中:github.com/fortheallli… 欢迎订阅


一、如何使用redux

首先要明确为什么要使用redux,这一点很重要,如果不知道为什么使用redux,那么在开发的过程中肯定不能合理的使用redux.首先来看redux的本质:

redux做为一款状态管理工具,主要是为了解决组件间通信的问题。

既然是组件间的通信问题,那么显然将所有页面的状态都放入redux中,是不合理的,复杂度也很高。

(1)全量使用redux

笔者在早期也犯了这个问题,在应用中,不管什么状态,按页面级路由拆分,全部放在redux中,页面任何状态的更改,通过react-redux的mapState和mapDispatch来实现。

如何优雅的使用react hooks来进行状态管理_第1张图片


redux中的状态从状态更新到反馈到视图,是一个过程链太长,从dispatch一个action出发,然后走reducer等逻辑,一个完整的链路包含:

创建action,创建redux中间件,创建相应type的reducer函数,创建mapState和mapDispatch等。

如果将所有状态都保存在redux中,那么每一个状态必须走这几步流程,及其繁琐,毫无疑问增加了代码量

(2)减少局部状态和redux状态的不合理混用

全量使用redux的复杂度很高,我们当然考虑将一部分状态放在redux中,一部分状态放在local state中,但是这种情况下,很容易产生一个问题,就是如果local State跟redux中的state存在状态依赖。

举例来说,在redux中的状态中有10个学生

 //redux
 
 students = [{name:"小白",score:70},{name:"小红",score:50}....]
复制代码

在local state中我们保存了分数在60分以上的学生

 // local state
 
 state = [{name:"小白",score:70}]
复制代码

如果redux中的学生改变了,我们需要从redux中动态的获取students信息,然后改变局部的state.结合react-redux,我们需要在容器组件中使用componentWillReceivedProps或者getDerivedStateFromProps这个声明周期,来根据props改变局部的local state.

componentWillReceivedProps这里不讨论,为了更高的安全性,在react中用静态的getDerivedStateFromProps代替了componentWillReceivedProps这里不讨论,而getDerivedStateFromProps这个声明周期函数在props和state变化的时候都会去执行,因此如果我们需要仅仅在props的改变而改变局部的local state,在这个声明周期中会存在着很复杂的判断逻辑。

redux中的状态和local state中的状态相关联的越多,getDerivedStateFromProps这个声明周期函数就越复杂

给我们的启示就是尽可能的减少getDerivedStateFromProps的使用,如果实在是redux和local state有关联性,用id会比直接用对象或者数组好,比如上述的例子,我们可以将学生分组,并给一个组号,每次在redux中的学生信息发生改变的时候会改变相应的组号。 这样在getDerivedStateFromProps只需要判断组号是否改变即可:

 class Container extends React.Component{
 state = {
 group_id:number
 }
 
 static getDerivedStateFromProps(props,state){ if(props.group_id!==state.group_id){
 
 ... 更新及格的学生
 }else{ return null
 }
 }
 }
复制代码

这里推荐https://github.com/paularmstrong/normalizr,如果实在redux和local state关联性强,可以先将数据范式化,范式化后的数据类似于给一个复杂结构一个id,这样子会简化getDerivedStateFromProps的逻辑.

(3)本节小结

如何使用redux,必须从redux的本质出发,redux的本质是为了解决组件间的通信问题,因此组件内部独有的状态不应该放在redux中,此外如果redux结合class类组件使用,可以将数据范式化,简化复杂的判断逻辑。

二、react hooks管理local state

前面将了应该如何使用redux,那么如何维护local state呢,React16.8中正式增加了hooks。通过hooks管理local state,简单易用可扩展。

在hooks中的局部状态常见的有3种,分别是useState、useRef和useReducer

(1) useState

useState是hooks中最常见的局部状态,比如:

 const [hide, setHide] = React.useState(false); const [name, setName] = React.useState('BI');
复制代码

理解useState必须明确,在react hooks中:

每一次渲染都有它自己的 Props and State

一个经典的例子就是:

 function Counter() { const [count, setCount] = useState(0); 
 function handleAlertClick() {
 setTimeout(() => {
 alert('You clicked on: ' + count);
 }, 3000);
 } 
 return ( 
 

You clicked {count} times

  setCount(count + 1)}>  Click me      Show alert    
 );  } 复制代码

如果我按照下面的步骤去操作:

  • 点击增加counter到3

  • 点击一下 “Show alert”

  • 点击增加 counter到5并且在定时器回调触发前完成

猜猜看会alert出什么?

如何优雅的使用react hooks来进行状态管理_第2张图片


结果是弹出了3,alert会“捕获”我点击按钮时候的状态,也就是说每一次的渲染都会有独立的props和state.

(2) useRef

在react hooks中,我们知道了每一次的渲染都会有独立的props和state,那么如果我们需要跟类组件一样,每次都能拿到最新的渲染值时,应该怎么做呢?此时我们可以用useRef

useRef提供了一个Mutable可变的数据

我们来修改上述的例子,来是的alert为5:

 function Counter() { const [count, setCount] = useState(0) const late = useRef(0) function handleAlertClick() {
 setTimeout(() => {
 alert('You clicked on: ' + late.current)
 }, 3000)
 }
 useEffect(() => {
 late.current = count
 }) return ( 
 

You clicked {count} times

  setCount(count + 1)}>Click me  Show alert  
 )  } 复制代码

如此修改以后就不是alert3 而是弹出5

(3) useReducer

react hooks中也提供了useReducer来管理局部状态.

当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们。

同样的用例子来说明:

 function Counter() {
 const [state, dispatch] = useReducer(reducer, initialState);
 const { count, step } = state;
 
 useEffect(() => {
 const id = setInterval(() => {
 dispatch({ type: 'tick' });
 }, 1000);
 return () => clearInterval(id);
 }, [dispatch]);
 
 return (
 <>
 

{count}

  {  dispatch({ type: 'step', step: Number(e.target.value)  });  }} />    );  }    const initialState = { count: 0, step: 1,  };    function reducer(state, action) {  const { count, step } = state; if (action.type === 'tick') {  return { count: count + step, step };  } else if (action.type === 'step') {  return { count, step: action.step };  } else { throw new Error();  }  } 复制代码

解释上面的结果主要来看useEffect部分:

 useEffect(() => { const id = setInterval(() => {
 dispatch({ type: 'tick' });
 }, 1000); return () => clearInterval(id);
 }, [dispatch]);
复制代码

在state中的count依赖与step,但是使用了useReducer后,我们不需要在useEffect的依赖变动数组中使用step,转而用dispatch来替代,这样的好处就是减少不必要的渲染行为.

此外:局部状态不推荐使用 useReducer ,会导致函数内部状态过于复杂,难以阅读。 useReducer 建议在多组件间通信时,结合 useContext 一起使用。

三、react hooks如何解决组件间的通信

react hooks中的局部状态管理相比于类组件而言更加简介,那么如果我们组件采用react hooks,那么如何解决组件间的通信问题。

###(1) UseContext

最基础的想法可能就是通过useContext来解决组件间的通信问题。

比如:

function useCounter() { let [count, setCount] = useState(0) let decrement = () => setCount(count - 1) let increment = () => setCount(count + 1) return { count, decrement, increment }
}let Counter = createContext(null)function CounterDisplay() { let counter = useContext(Counter) return ( 
 -  

You clicked {counter.count} times

 +  
 ) }function App() { let counter = useCounter() return (         ) } 复制代码

在这个例子中通过createContext和useContext,可以在App的子组件CounterDisplay中使用context,从而实现一定意义上的组件通信。

此外,在useContext的基础上,为了其整体性,业界也有几个比较简单的封装:

github.com/jamiebuilds… github.com/diegohaz/co…

但是其本质都没有解决一个问题:

如果context太多,那么如何维护这些context

也就是说在大量组件通信的场景下,用context进行组件通信代码的可读性很差。这个类组件的场景一致,context不是一个新的东西,虽然用了useContext减少了context的使用复杂度。

###(2) Redux结合hooks来实现组件间的通信

hooks组件间的通信,同样可以使用redux来实现。也就是说:

在React hooks中,redux也有其存在的意义

在hooks中存在一个问题,因为不存在类似于react-redux中connect这个高阶组件,来传递mapState和mapDispatch, 解决的方式是通过redux-react-hook或者react-redux的7.1 hooks版本来使用。

  • redux-react-hook

在redux-react-hook中提供了StoreContext、useDispatch和useMappedState来操作redux中的store,比如定义mapState和mapDispatch的方式为:

import {StoreContext} from 'redux-react-hook';
ReactDOM.render( 
 
 ,
 document.getElementById('root'),
);
import {useDispatch, useMappedState} from 'redux-react-hook';
export function DeleteButton({index}) {
 // Declare your memoized mapState function
 const mapState = useCallback( state => ({
 canDelete: state.todos[index].canDelete,
 name: state.todos[index].name,
 }),
 [index],
 );
 // Get data from and subscribe to the store
 const {canDelete, name} = useMappedState(mapState);
 // Create actions
 const dispatch = useDispatch();
 const deleteTodo = useCallback(
 () =>
 dispatch({
 type: 'delete todo',
 index,
 }),
 [index],
 );
 return ( 
 Delete {name}
 
 );
}
复制代码
  • react-redux 7.1的hooks版

这也是官方较为推荐的,react-redux 的hooks版本提供了useSelector()、useDispatch()、useStore()这3个主要方法,分别对应与mapState、mapDispatch以及直接拿到redux中store的实例.

简单介绍一下useSelector,在useSelector中除了能从store中拿到state以外,还支持深度比较的功能,如果相应的state前后没有改变,就不会去重新的计算.

举例来说,最基础的用法:

import React from 'react'import { useSelector } from 'react-redux'export const TodoListItem = props => { const todo = useSelector(state => state.todos[props.id]) return 
{todo.text}
} 复制代码

实现缓存功能的用法:

import React from 'react'import { useSelector } from 'react-redux'import { createSelector } from 'reselect'const selectNumOfDoneTodos = createSelector( state => state.todos,
 todos => todos.filter(todo => todo.isDone).length
)export const DoneTodosCounter = () => { const NumOfDoneTodos = useSelector(selectNumOfDoneTodos) return 
{NumOfDoneTodos}
}export const App = () => { return ( <>  Number of done todos:      ) } 复制代码

在上述的缓存用法中,只要todos.filter(todo => todo.isDone).length不改变,就不会去重新计算.

四、总结

react中完整的状态管理分为全局状态和局部状态,而react hooks简化了局部状态,使得管理局部状态以及控制局部渲染极其方便,但是react hooks本质上还是一个视图组件层的,并没有完美的解决组件间的通信问题,也就是说,redux等状态管理机和react hooks本质上并不矛盾。

在我的实践中,用redux实现组件间的通信而react hooks来实现局部的状态管理,使得代码简单已读的同时,也减少了很多不必要的redux样板代码.

如何优雅的使用react hooks来进行状态管理_第3张图片