如何優雅的使用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樣板代碼.

相關文章
相關標籤/搜索