拒絕Redux?使用原生React管理狀態

Banner

自從我們學習React的第一天起,我們就知道不要在React中使用太多狀態。我們也知道應該儘可能多使用無狀態的函數式組件,少使用有狀態的類組件。

這些建議很容易讓我們形成這樣的判斷,那就是我們完全不應該使用狀態,當我們需要用到狀態的時候,應該使用Redux之類的第三方狀態管理庫。

我們不喜歡使用React原生狀態並不是沒有原因的:

  1. 當項目變大的時候,散落在許多組件中的局部狀態很快會變得無法追蹤;
  2. 組件之間難以共享狀態;
  3. 從一個外部組件一層層將prop傳遞到內部組件很麻煩;這個問題被稱作prop drilling
  4. 狀態邏輯通常和UI邏輯混合在一起,導致調試和重用狀態邏輯都很困難;
  5. 在編譯時,類組件比函數式組件更難優化。

嚴格來講,prop drilling並不屬於狀態的問題,而是一個設計失誤。它通常是由過多的不必要封裝導致的。

Redux的優缺點

Redux是最流行的狀態管理庫,它通過將狀態與UI隔離並中心化管理的方式減輕了以上所說的這些痛點。

它的整個邏輯十分簡單,但卻蘊含着強大的擴展性。我們可以用一個單向數據流圖來描述Redux背後的運行機制:

Redux Mechanism

除了store自身,所有其他的組件都是純函數。這是一個函數式編程中的概念,指的是函數的結果只取決於函數的輸入,而不取決於任何其他的狀態。

純函數更容易測試、理解和調試。通過強制使用函數式編程這一編程風格,Redux減少了維護狀態邏輯的負擔。

然而,Redux也有自己特有的麻煩。一個人們廣爲詬病的問題就是在搭建項目初期,它需要寫太多樣板代碼。

對於小項目而言,這個問題尤爲嚴重。添加一個新的action通常需要我們定一個新action、添加一個新action creator、修改reducer、更新container等一系列操作。爲了完成這個功能,我們需要在許多個不同的文件之間跳轉,最後把一個5行之內可以做完的事情擴展成20行。

甚至說,我們到底要不要中心化的store也是一個值得討論的問題,就像我們要不要用全局變量一樣。

和局部狀態相比,全局狀態更難重用。重構全局狀態可能會意外地導致部分現有代碼不可用。不合理地使用Redux container也會有性能問題。

在另一邊,近幾年React爲了讓狀態管理更好用做出了許多努力,引入了Context APIHooks等新特性來解決過去的痛點。

原生React

React支持兩種組件:類組件(支持狀態和hook,也就是componentDidMount等函數)和函數式組件(不支持狀態,更加簡單)。

在過去,如果我們想使用函數式組件,又想維護某些內部狀態,唯一的辦法就是使用Redux之類的狀態管理庫。

在React增加了Context API和hooks之後,我們有了更多的選擇。

因爲這兩個新API是React官方支持的,我建議在使用第三方的狀態管理庫之前,先了解一下它們再做決定。

React Hooks

React hooks API提供了一種在函數式組件中管理狀態的方法。

這句話第一眼看上去自相矛盾,因爲函數式在某種程度上就意味着無狀態。但是,我們先將這一問題擱置在一邊,只把函數式組件當作一種設計模式來看待。

和類組件相比,函數式組件有更緊湊的形式,需要更少的樣板代碼,更可讀,對編譯器來說也更容易分析和優化。

然而,不能夠在函數式組件中維護狀態爲我們帶來了極大的不便:即使是引入一個boolean這樣小的狀態,都需要我們將整個函數式組件重寫爲類組件。

React hooks API通過允許我們在函數式組件中使用狀態來解決了這個困境。而且,它也允許我們將狀態邏輯從UI邏輯中剝離出來,重用到其他的UI組件中。

下面這個簡短的例子展示了我們如何在函數式組件中使用React hooks:

import React, { useState } from 'react'

const Counter: React.FC = () => {
  const [counter, setCounter] = useState(0)
  return (
    <div>
      <p>Counter: {counter}</p>
      <button onClick={() => setCounter(counter + 1)}>
        Increment
      </button>
    </div>
  )
}

useState返回當前狀態和一個函數(這個函數可以用來更新狀態),它的參數是初始狀態。這個函數第一次被調用的時候,它將組件的內部狀態初始化爲給定的初始值。

這個狀態是維護在這個組件之內的,不會被同一個組件的多個實例之間共享。

我們可以在一個函數式組件中調用useState許多次。React hooks API鼓勵我們將一個複雜的狀態分解成多個可重用的簡單狀態。

我們可以進一步將狀態邏輯封裝在單獨的函數中(稱爲custom hook),以便我們在其他組件中重用這個狀態邏輯:

import React, {useState} from 'react'

// A custom hook
const useCounter = (initial: number) => {
  const [counter, setCounter] = useState(initial)

  return {
    counter,
    increment () {
      setCounter(counter + 1)
    },
    reset () {
      setCounter(initial)
    }
  }
}

const Counter: React.FC = () => {
  const { counter, increment, reset } = useCounter(0)
  return (
    <div>
      <p>Counter: {counter}</p>
      <button onClick={increment}>
        Increment
      </button>
      <button onClick={reset}>
        Reset
      </button>
    </div>
  )
}

每一個使用setState的地方,我們都可以用React hooks代替,因爲React hooks全方面地超越了原來的setState:它允許將一個組件的狀態拆分成小的可重用的狀態,鼓勵我們多用函數式組件,少用類組件。

React Context API

React Context API比React hooks更早引入React,但是它是用來解決一個完全不同的問題的:狀態共享prop drilling

這個功能可能會讓你聯想到Redux的用途,但是React Context API事實上並不鼓勵用它來維護一個巨大的中心化store。官方文檔中說到:

Context主要是用在數據需要被不同層次的多個組件訪問的時候。儘可能少用它,因爲它會使組件重用更加困難。

React Context API和React hooks的設計哲學是相同的,那就是儘可能避免狀態共享

我們不得不使用React Context API的典型場景有:

  • 用戶登錄信息;
  • UI主題;
  • locale設置。

例如,我們可以通過把UI主題放到Context中的方法來避免手動逐層傳遞:

import React, { useContext } from 'react'

const ThemeContext = React.createContext('light')

const UserComponent: React.FC = () => {
  const theme = useContext(ThemeContext)
  return (
    <div>
      Current theme: {theme}
    </div>
  )
}

const App: React.FC = () => {
  return (
    <ThemeContext.Provider value="dark">
      <UserComponent />
    </ThemeContext.Provider>
  )
}

通過合理地組合React Context API和React hooks,我們可以在完全不用Redux的情況下管理程序狀態。

然而,就像編程世界中的其他開放性問題一樣,狀態管理也沒有萬金油。具體怎麼做取決於業務邏輯的複雜性、程序的規模和其他各種因素。

我們應該在實際中選擇最合適的方法。我的建議是,在項目初期,可以使用Context API和React hooks作爲開始,隨着項目的進行,只在必要的時候才引入Redux。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章