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


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

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

原文在个人博客中:https://github.com/forthealll...
欢迎订阅git


1、如何使用redux

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

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

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

(1)全量使用redux

  笔者在早期也犯了这个问题,在应用中,无论什么状态,按页面级路由拆分,所有放在redux中,页面任何状态的更改,经过react-redux的mapState和mapDispatch来实现。缓存

Lark20190919-153334

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

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

若是将全部状态都保存在redux中,那么每个状态必须走这几步流程,及其繁琐,毫无疑问增长了代码量ide

(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类组件使用,能够将数据范式化,简化复杂的判断逻辑。

2、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出什么?
counter-46c55d5f1f749462b7a173f1e748e41e

结果是弹出了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 一块儿使用。

3、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不改变,就不会去从新计算.

4、总结

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

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

相关文章
相关标签/搜索