Redux 101 (Part 1)

Redux 101 (Part 1)

Welcome to tệ xá thưa quý vị. Và sau bao hôm lười suy nghĩ nát óc không biết viết gì để câu view cho blog chia sẻ hôm nay tôi xin phép được ra một series về Redux giúp thông não cho tất cả các bạn và cả tôi, với tư cách một noob dev thì tôi sẽ cố gắng làm cho chính tôi có thể nạp vào đầu một cách tổng quát và chi tiết nhất, và khi một noob như tôi hiểu thì hiển nhiên các bạn cũng sẽ thông hết.
Vậy thì bắt đầu thôi nào!

Series này sẽ bao gồm các phần sau: (Sẽ update dần)
1. Giới thiệu về Flux và kiến trúc của Redux, cũng như một số thuật ngữ cơ bản
2....

Lịch sử của Redux

Năm 2013, sau khi giới coder ăn hành đủ từ bộ Angular của Google, Facebook quyết định nhảy vào cuộc chơi với việc giới thiệu React.
Nhưng tất nhiên, React chỉ là một library render view nên không thể so sánh với một framework như Angular. Do đó, Facebook đã tung ra thêm một kiến trúc gọi là Flux dùng để handle dữ liệu, nhưng Flux bị đánh giá tương đối là khó hiểu và quá phức tạp.
Vào năm 2015 tại hội nghị React Europe, Dan Abramov và Andrew Clark đã lần đầu giới thiệu Redux, nhưng cuộc diễn thuyết lần này chỉ là về những vấn đề dẫn tới việc Dan phải triển khai Redux.
Và một năm sau đó, tức hội nghị React Europe 2016. Dan Abramov đã suy ngẫm về hành trình của Redux và sự thành công của nó. Anh ấy đã đề cập đến một vài điều đã làm nên thành công của Redux theo quan điểm của mình.

Flux

Tại sao lại có Flux ở đây nhỉ, rõ ràng tên bài là Redux 101 cơ mà. Và đúng là sự thật thì bạn không cần biết về Flux.
Okay, trước khi skip phần này và chửi thầm tôi thì bạn nên biết là Redux được xây dựng và thiết kế dựa trên kiến trúc Flux, vậy thì tốt nhất ta nên ngó qua xem Flux là cái khỉ khô gì đã nhỉ?
Như chúng ta đã biết, cách thức truyền dữ liệu phổ biến nhất giữa các components trong React đơn giản là từ:

component cha -> component con

Và thằng Flux cũng dựa trên cách thức này.

Có 3 thành phần với vai trò khác nhau tham gia xử lý dữ liệu trong method của Flux (Flux methodology):

  • Dispatcher
  • Stores
  • Views (components)

Ý tưởng khởi nguồn của Flux đó là Single source of truth (stores) và nó chỉ có thể cập nhật bởi cách trigger các actions. Các actions này có trách nhiệm gọi dispatcher, mà các stores nào đã subcribe có thể thay đổi và update dữ liệu của chúng.

Khi một dispatch được trigger, và stores thực hiện những update, nó sẽ phát ra một change event giúp những views tương ứng có thể render chính xác.

flux-simple-f8-diagram-1300w

Điều này có vẻ phức tạp một cách không cần thiết, nhưng cấu trúc này giúp chúng ta vô cùng dễ dàng theo dõi được việc dữ liệu của chúng ta đến từ đâu, nguyên nhân khiến nó thay đổi, cách nó thay đổi và cho phép chúng ta theo dõi các luồng người dùng cụ thể,...

Chốt lại ý tưởng cốt lõi của Flux là:
Luồng dữ liệu chỉ đi theo một hướng và được lưu giữ trong những store

Redux

Redux là một thư viện nhỏ lấy cảm hứng thiết kế từ Flux, nhưng bản thân nó không phải là một triển khai theo flux thuần túy. Nó cung cấp các nguyên tắc chung giống nhau về cách cập nhật dữ liệu trong ứng dụng, nhưng theo một cách hơi khác.

Không giống như Flux, Redux không sử dụng dispatcher mà thay vào đó nó sử dụng các hàm thuần túy (pure functions) để xác định các hàm thay đổi dữ liệu. Redux vẫn sử dụng store và action, và có thể được liên kết trực tiếp với các React component.

3 nguyên tắc chính của Redux phải nhớ khi triển khai Redux trong ứng dụng của mình là:

  • Changes are made with pure functions: Để chỉ định cách state tree được update bằng các actions, bạn phải tạo ra các pure reducer
  • state is read-only: Cách duy nhất thay đổi một state là tạo một action
  • Single source of truth State của toàn bộ ứng dụng được lưu trong trong một store duy nhất là một Object mô hình cây, và chỉ có duy nhất một và chỉ một

Một điểm khác biệt lớn với Redux và Flux là khái niệm middleware. Mình sẽ nói kĩ hơn ở phần redux middleware.

Cách quản lý state của Redux

axnmkq4b19fxeso4lb7z

Ví dụ app của chúng ta hoạt động bằng cách truyền state giữa các node với nhau, node này sẽ nhận state truyền từ node cha của nó

Theo hình trên, ví dụ nếu bạn muốn thực hiện action ở D trigger thay đổi state ở E mà không sử dụng redux: Thì bạn phải truyền theo cách truyền thống (Prop drilling), đó là:

D -> B -> A -> C -> E

OK, nếu app bạn nhỏ và không có quá nhiều node hoặc không quá phức tạp, thì không cần thiết phải dùng dao mổ trâu (redux) giết gà làm gì cả. Nhưng nếu như trong trường hợp app bạn có nhiều node hơn thì sao? Đó là lúc chúng ta phải dùng dao mổ trâu để mổ trâu.

5n276dibl7bxch5mls63

Như hình, thì node D chỉ việc cung cấp dữ liệu cho "Store", sau đó node E chỉ việc subcrible "Store" và yêu cầu lấy dữ liệu về dùng. Nghĩa là tất cả state giờ đây đều nằm trong Store

One Way Data Flow và Unidirectional Data Flow

Đây là những Structural Pattern dùng cho React app của bạn khi quản lý state.

  1. One-way data flow : Redux
    1_T_Q66EkNEhca6TyrvY1xBQ
  2. Unidirectional way flow: Props drilling (parent to child)
    1_PBgAz9U9SrkINPo-n5glgw

Trước tiên, Unidirectional way không hề xấu nhé, vì nó giúp đơn giản luồng dữ liệu với các app nhỏ. Nhưng khi app của bạn phình to ra, thì dữ liệu đi qua ở nhiều node có thể không bao giờ được sử dụng, và bạn sẽ mất rất nhiều công sức để track luồng dữ liệu này.

Không như cách quản lý state theo Unidirectional way,
One-way data (Redux) quản lý tất cả state ở một nơi nhất định (store) và thao tác với trạng thái bằng reducer - không hơn gì ngoài một function. Nó đơn giản là lấy state của app, action và xử lý chúng.

Các bước của One-way data flow được mô tả như sau:

  • State mô tả tình trạng hiện tại của ứng dụng tại một thời điểm cụ thể
  • UI được render dựa trên states đó
  • Khi có gì đó xảy ra (như khi người dùng click vào một button), state sẽ được update dựa theo những gì đã xảy ra
  • UI re-render lại dựa trên state mới

Data flow của Redux

ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26

Úi giời ơi kiếm được quả hình dễ hiểu vãi l..

Như trong Redux, chi tiết hơn thì:

Initial setup:

  1. Một redux store được tạo bằng cách sử dụng một root reducer function
  2. Store sẽ gọi đến root reducer một lần, và lưu những giá trị trả về như là initial state
  3. Khi UI được render lần đầu, UI components yêu cầu truy cập đến state hiện tại của redux store, và sử dụng data đó để quyết định render cái gì. Chúng đồng thời cũng subcribe để biết được nếu trong tương lai state có thay đổi hay không.

Updates:

  1. Có event gì đó xảy ra trong app, như là user click một button chẳng hạn
  2. Phần code dispatches trong app của bạn (mà bạn sẽ phải viết) sẽ dispatch một action đến Redux store, ví dụ như: dispatch({type: 'counter/increment'})
  3. Store sẽ khởi chạy reducer function lần nữa với state trước và action hiện tại, và trả về một state mới.
  4. Store lúc này sẽ thông báo đến tất cả các phần của UI components mà đã subcribe rằng store đã update, đến mà xem này.
  5. Mỗi UI component dùng dữ liệu từ store sẽ kiểm tra xem phần dữ liệu mà mình dùng có thay đổi hay không?
  6. Mỗi component nhận thấy dữ liệu của mình bị thay đổi sẽ buộc phải re-render lại với dữ liệu mới, nhờ vậy những thay đổi sẽ được hiện lại trên màn hình

Thuật ngữ cơ bản cần biết trong Redux

State

State (hay còn được gọi là state tree) là một thuật ngữ rộng. Nhưng trong API Redux, nó thường đề cập đến giá trị trạng thái duy nhất được quản lý bởi store và được trả về bởi getState(). Nó đại diện cho toàn bộ state của ứng dụng Redux, thường là một deeply nested object.

Theo quy ước thì top-level state phải là một object hoặc một tập hợp key-value, nhưng về mặt kỹ thuật thì nó có thể là bất kì kiểu nào. Tuy nhiên, tốt nhất là bạn nên giữ state serializable. Đừng đặt thành bất cứ thứ gì mà bạn không thể dễ dàng biến nó thành JSON.

Action

Một action là một plain object đại diện cho mục đích bạn thay đổi state. Bạn có thể nghĩ đơn giản một action như một sự kiện mô tả một cái gì đó đã xảy ra trong ứng dụng của bạn.

Như đã nói ở trên, cách duy nhất thay đổi state trong redux là dispatch actions, vậy nên actions là cách duy nhất bạn đưa dữ liệu vào trong store. Bất kể dữ liệu nào, cho dù từ UI events, network callbacks, hoặc bất kể nguồn nào khác như WebSockets đều cần phải được dispatch như những actions. Cóc cần biết dữ liệu hay event của bạn là gì, muốn thay đổi state trong store thì chỉ có dispatch actions, thế nhé!

Một action bắt buộc phải có trường type, và nó có thể có các trường khác để bổ sung cho thông tin về những gì đã xảy ra. Theo quy ước thì thông tin thường được đặt vào trường payload.

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk Noel'
}
Action

Hoặc ngoài ra bạn có thể sử dụng action creator, nó chỉ đơn giản là một function return một action object để tránh việc bạn phải viết nhiều action, bình thường thì người ta hay làm vậy

const addTodo = text => {
  return {
    type: 'todos/todoAdded',
    payload: text
  }
}

/** Thay vì dispatch({
  type: 'todos/todoAdded',
  payload: 'Buy milk Noel'
 })
*/ thì chỉ cần dispatch(addTodo('Buy milk Noel'))
Action creator

Reducers

Là một function nhận vào state hiện tại và action object, quyết định xem có cần phải update state hay không, nếu có thì sẽ trả về state mới: ( state, action ) => newState.
Bạn có thể nghĩ đơn giản là reducer như là một event listener, sẽ dựa vào tuỳ type trong action mà xử lý. bạn có thể dùng switch-case, if-else, v.v
Thằng này cũng tương tự như Array.reducer() vậy

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // Kiểm tra xem type của action có đúng hay không
  if (action.type === 'counter/increment') {
    // Nếu đúng, trả về một bản sao tạo từ state
    return {
      ...state,
      // với value mới được update
      value: state.value + 1
    }
  }
  // Nếu type không đổi thì trả lại state ban đầu
  return state
}

Store

State trong redux app của bạn sẽ nằm ở trong một object gọi là store
Store được tạo bằng cách truyền vào một reducer và có một method gọi là getState trả về giá trị state hiện tại:

import { createStore } from 'redux'


const store = createStore(counterReducer)

console.log(store.getState())
//{value: 0}

Dispatch

Redux store có một method gọi là dispatch. Và cách duy nhất bạn có thể update state là gọi store.dispatch() và truyền vào một action. Sau đó store sẽ khởi chạy reducer function của nó và lưu lại state mới, lúc này ta có thể gọi getState() để nhận về giá trị đã update

Bạn có thể coi các dispatch actions giống như trigger một event trong ứng dụng vậy. Có gì đó đã xảy ra và chúng ta muốn store biết về việc đó. Và lúc này reducer giống như một event listener, nếu nó thấy action nào mà nó có liên quan thì sẽ thực hiện update state tương ứng type của action để phản hồi.

const increment = () => { //action
  return {
    type: 'counter/increment'
  }
}

store.dispatch(increment())

console.log(store.getState())
// {value: 1}

Selectors

Đây chỉ đơn giản là một function (Đúng hơn là một đoạn logic) lấy một phần hoặc toàn bộ thông tin được lưu trữ trong state của store để lưu trữ hoặc tính toán hoặc làm gì đó vân vân và mây mây.

Nhược điểm là khi state thay đổi thì tất cả selector đều sẽ phải tính toán lại. Nếu nó quá phức tạp sẽ gây ảnh hưởng đến performance. Có một thư viện khắc phục điều này tên là reselect

const selectStateValue = state => state.value  
const selectStateValue2 = state => state.value + 1
const currentValue = selectStateValue(store.getState())
console.log(currentValue)
// 1
const currentValue2 = selectStateValue2(store.getState())
console.log(currentValue2)
// 2

Đến đây là đã khá đầy đủ những phần của Redux thuần mà bạn nên biết rồi.Phần này thì mình không để quá nhiều code mà chỉ đi vào lý thuyết cũng như cơ chế. Nhưng ngoài ra bạn còn phải biết về các Middleware của Redux để xử lý các side-effect nữa thì mình sẽ giới thiệu vào các loạt bài sau.
Thân ái và hẹn gặp lại

Nguồn tham khảo: