如何优雅的使用react-hooks来进行状态管理

48次阅读

共计 7488 个字符,预计需要花费 19 分钟才能阅读完成。


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

  • 如何使用 redux
  • react hooks 管理 local state
  • react hooks 如何解决组件间的通信

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


一、如何使用 redux

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

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

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

(1) 全量使用 redux

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

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/paularmstr… 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 (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
          <button onClick={handleAlertClick}>
            Show alert
          </button>
        </div>
      );
    }

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

  • 点击增加 counter 到 3
  • 点击一下“Show alert”
  • 点击增加 counter 到 5 并且在定时器回调触发前完成

猜猜看会 alert 出什么?

结果是弹出了 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 (
            <div>
                <p>You clicked {count} times</p>
                <button onClick={() => setCount(count + 1)}>Click me</button>
                <button onClick={handleAlertClick}>Show alert</button>
            </div>
        )
    }

如此修改以后就不是 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 (
        <>
          <h1>{count}</h1>
          <input value={step} onChange={e => {
            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 (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {let counter = useCounter()
  return (<Counter.Provider value={counter}>
      <CounterDisplay />
      <CounterDisplay />
    </Counter.Provider>
  )
}

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

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

https://github.com/jamiebuild…
https://github.com/diegohaz/c…

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

如果 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(<StoreContext.Provider value={store}>
    <App />
  </StoreContext.Provider>,
  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 (<button disabled={!canDelete} onClick={deleteTodo}>
      Delete {name}
    </button>
  );
}
  • 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 <div>{todo.text}</div>
}

实现缓存功能的用法:

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 <div>{NumOfDoneTodos}</div>
}

export const App = () => {
  return (
    <>
      <span>Number of done todos:</span>
      <DoneTodosCounter />
    </>
  )
}

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

四、总结

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

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

正文完
 0