動態

詳情 返回 返回

再聊 Reducer Context 和 Redux - 動態 詳情

原文鏈接

這是一次突發奇想的感悟,感覺還挺神奇的,遂記錄一下。

前言

作為一個React的開發者已經蠻久的了,大大小小的應用也開發了不少,除了一開始學習React時用過Redux以外,後來基本都不碰了,不管多麼複雜的應用,我也簡單的覺得使用Context就能夠解決我所有的問題。説來慚愧,我基本沒有思考過Redux存在的原因,可能是React真的做的太好了,又或者是我們現在的設備性能已經嚴重過剩了,讓我完全不需要考慮應用優化的問題。

今天又冒出為什麼這麼多人用Redux的問題,所以又看了一下ReactRedux的文檔,結果有蠻大的收穫(每次看文檔都有新收穫,推薦大家沒事多看看),突然讓我回憶起曾經好多次使用useState更新數組時的彆扭(雖然沒什麼問題,但是總覺得過於複雜了),今天我們就來聊聊這些。

隨便推薦使用我寫的理解例子examples/reducer-context-redux一起服用效果更佳哦,以下所有完整代碼皆可在例子中找到。

需求

脱離真實需求聊一些技術的東西,總讓人覺得比較虛,所以我們今天就來聊一下一個比較簡單的需求,比較幾種不同的方式演變的代碼的區別來幫助我們理解RedcuerContextRedux這些概念。

簡單的描述一下需求,一個可以創建todo的輸入框,一個展示todo的列表,todo本身可以修改名稱,標記為完成或者刪除,具體看下圖。

Base App

一般實現

從圖片也可以看出來這是一個非常簡單的需求,讓我們來快速的實現一下,完整代碼見examples/reducer-conetxt-redux/base

...
// 定義一個 Todos
const [todos, setTodos] = useState([]);

// 定義幾個方法分別用來 創建,更新,刪除 Todo
const handleAddTodo = (name) => {
  setTodos([...todos, { id: nextId++, name, done: false }]);
};

const handleChangeTodo = (todo) => {
  setTodos(
    todos.map((t) => {
      if (t.id === todo.id) {
        return todo;
      } else {
        return t;
      }
    })
  );
};

const handleDelTodo = (id) => {
  setTodos(todos.filter((t) => t.id !== id));
};

為什麼我只貼了一部分代碼?因為這部分的代碼將會進行第一步演化,我相信大部分的人應該都會這麼寫吧(如果不是的話,別噴我,至少我在大部分情況下都是這麼寫的)。

但是它有什麼問題呢?其實沒有什麼問題,如果你還沒有遇到問題的話,它的確沒有問題,感覺自己在説廢話呢,那我給幾個你可能會遇到問題的情況吧:

  1. 如果 Todo 需要通過接口來完成創建、更新和刪除,那當你同時進行多個操作時,會導致你的todos只完成了最後一次更新。
  2. 關於1有一個非常難受的地方,就是你不太能容易發現todos為什麼沒有正確更新成你期望的樣子,這個排查是很痛苦的,不知道你們有沒有遇到過?
  3. 雖然現在useState好像非常直觀的展示了todos的更新機制,但如果我添加更多的功能,比如多狀態的todo,這個時候你需要更多的setTodos來更新todos,這好像比較難理解,那我們可以將更新這個操作改為上一步或者下一步,這樣你是不是就需要拆開handleChangeTodo這個方法了?

好了,差不多第一版就這些,那麼如何來優化它?

Reducer

要用它,就得先知道它是什麼吧?簡單來説,就是把所有狀態更新邏輯合併到一個函數中,就叫Reducer。它的定義已經出來了,我覺得這個時候你可能已經想到了如何用Reducer來更新上面的第一版了,完整代碼見examples/reducer-context-redux/reducer。

function todoReducer(todos, action) {
  switch (action.type) {
    case 'added': {
      return [...todos, { id: nextId++, name: action.name, done: false }];
    }
    case 'changed':
      return todos.map((t) => {
        if (t.id === action.todo.id) {
          return action.todo;
        } else {
          return t;
        }
      });
    case 'deleted': {
      return todos.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const [todos, dispatch] = useReducer(todoReducer, []);

const handleAddTodo = (name) => {
  dispatch({
    type: 'added',
    name,
  });
};

const handleChangeTodo = (todo) => {
  dispatch({
    type: 'changed',
    todo,
  });
};

const handleDelTodo = (id) => {
  dispatch({
    type: 'deleted',
    id,
  });
};

直觀看,代碼好像比上面更多了?這個例子確實是,但是就像我説的第一版中問題3那樣,當你的狀態越來越複雜,兩種方式帶來的代碼增長將不會一樣,也就是説,狀態多到一定的程度後,這樣寫的代碼會更少,不過這好像也不能成為一個這麼寫的充分理由。

那我在來補充幾個這樣寫的好處吧:

  1. 所有的狀態變更都收在了todoReducer函數裏面,你可以方便的在這個函數裏面console.log來感知狀態的變化。 -> 方便調試
  2. todoReducer作為一個乾淨的函數,你可以輕易的寫出它的測試用例。 -> 方便寫測試用例,增強穩定性
  3. 狀態的變化一目瞭然。 -> 增強可讀性

當然這些都不是必要的,你完全可以按照你的喜好和場景來使用useState或者useReducer,不過你應該要知道它們的區別。

Context

上面的代碼好像和Context並沒有什麼直接的聯繫,我又為什麼要把它也拿來一起看看呢?是因為Redux就像是ReducerContext的結合體,所以你現在已經知道了Reducer是什麼了,當然也要知道Context是什麼,和上述一樣完整代碼見examples/reducer-conetxt-redux/context

那麼Context又是什麼呢?簡單來講,就是兩個沒有直接聯繫的組件共享狀態。比如A -> B -> C -> DD想要接收到A的狀態,需要經過BC,那如果你用Context就可以跳過BC,看起來是不是很好用?確實是,但是它有一個很大的缺點,也是我們不希望它被濫用的原因,因為你需要從A定義這個狀態,那麼如果這個狀態發生了變化,A所有的子組件都會更新,那你要是在一個特別大的應用的根上定義了一個經常變化的狀態,那這個應用就得經常更新,是一件比較可怕的事情。

再來看Context的主要目的是為了跨組件共享狀態,它並不具有狀態定義和管理的功能,也就是需要搭配useState或者useReducer來使用,這也是為什麼我説Redux就像是ReducerContext的結合體,來看下代碼演進吧。

// TodoContext.jsx
import { createContext } from 'react';

export function todoReducer(todos, action) {
  switch (action.type) {
    case 'added': {
      return [...todos, { id: nextId++, name: action.name, done: false }];
    }
    case 'changed':
      return todos.map((t) => {
        if (t.id === action.todo.id) {
          return action.todo;
        } else {
          return t;
        }
      });
    case 'deleted': {
      return todos.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

export default createContext();

//
import TodoContext, { todoReducer } from './context';

const [todos, dispatch] = useReducer(todoReducer, []);

return (
  <TodoContext.Provider value={{ todos, dispatch }}>
    <ContextApp />
  </TodoContext.Provider>
);

當然這裏你依然可以用useState去取代useReducer,看你如何考慮吧!

Redux

終於到了最後一步了,其實你不用Redux,好像也不會阻塞你做這個需求,或者説任何別的需求,但是為什麼要用它,這就是今天要探究的內容了,一樣完整代碼見examples/reducer-conetxt-redux/redux

先看代碼改造吧,由於新的Redux使用Toolkit來組織代碼,但是為了方便理解,我還是單純的使用redux來演示這個改造。

// store.jsx
import { createStore } from 'redux';

function todoReducer(todos = [], action) {
  switch (action.type) {
    case 'added': {
      return [...todos, { id: nextId++, name: action.name, done: false }];
    }
    case 'changed':
      return todos.map((t) => {
        if (t.id === action.todo.id) {
          return action.todo;
        } else {
          return t;
        }
      });
    case 'deleted': {
      return todos.filter((t) => t.id !== action.id);
    }
    default:
      return todos;
  }
}

export default createStore(todoReducer);

//
import { Provider } from 'react-redux';

export default () => (
  <Provider store={store}>
    <ReduxApp />
  </Provider>
);

其實對比Context的版本來看,一眼就能明白我説的Redux就像是ReducerContext的結合體,那為什麼要用它呢?直接上理由吧:

  1. 不知道你還記得Context的缺點嗎?狀態的變化會導致所有組件的變化,但是Redux只會影響訂閲對應狀態的組件。
  2. 不知道你還記得在最初版還有一個問題沒有解決,就是異步請求後的狀態更新,Redux有很多很好用的中間件來處理這些事情,比如redux-thunk,當然你也可以自己寫,但是意義是什麼呢?
  3. 瀏覽器插件Redux DevTools,讓你清晰的看到各個狀態的變化。
  4. 脱離於不同UI的狀態管理,比如你同時有多個應用共享一套狀態,或者説ReactVue寫的兩個應用共享一套狀態。

這就是我理解Redux最為強大的優勢吧,也就是當你沒有這些問題的時候,你完全不需要它。

總結

就像我説的,當你還沒有意識到你要不要用Redux時,你可能不太需要它,當你在思考如何組織你的狀態,或者你已經被你的狀態搞的焦頭爛額了,你可能需要考慮它了,拋開需求談技術都挺扯蛋的,你完全有足夠的時間去不斷的優化你的代碼,而不是一開始就把所有的工具集成到一個應用裏,不管它是不是真的需要,這樣你永遠也不會明白用它的意義是什麼。

Add a new 評論

Some HTML is okay.