Từng bước build một ứng dụng React-Redux

Redux là một công cụ tuyệt vời để build 1 ứng dụng React. Và có hàng tấn ví dụ về nó trên internet. Nhưng chắc bạn cũng thấy - 1 ứng dụng React-Redux có quá nhiều thành phần như: “Reducers”, “Actions”, “Action Creators”, “State”, “Middleware”... Trông có vẻ quá tải đối với người mới tiếp cận. Khi bắt đầu học React-Redux, tôi không thể tìm ra blogs nào nói về việc "Phần nào của React-Redux cần phải build trước?" hay làm sao để tiếp cận được nó một cách dễ dàng. Vì thế tôi đọc qua hàng đống ví dụ và blogs, cuối cùng viết bài viết này nhằm đưa ra các bước chung để tiếp cận cách build hầu hết các ứng dụng React-Redux. Lưu ý: Tôi đang sử dụng 'Mocks' để dựng sườn cơ bản và không đi sâu vào chi tiết. Và ví dụ trong bài viết này là ứng dụng "Todo list app" cơ bản, cách build các ứng dụng lớn khác đều dựa trên bộ khung này. Nếu app của bạn có nhiều màn hình, phức tạp hơn, thì đơn giản chỉ cần lặp lại quá trình này cho từng màn hình.

Tại sao lại là Redux?

React - Một thư viện JS giúp chúng ta phân chia app thành nhiều components nhưng việc theo dõi, quản lý dữ liệu và sự kiện (State and Action) không được rõ ràng. Redux - Một thư viện miễn phí cung cấp cho React một cách tiếp cận với vấn đề trên dễ dàng hơn

Về cơ bản, Redux cho phép chúng ta build một ứng dụng React như bình thường vẫn làm, còn về State và Actions thì Redux sẽ lo. BTW, có đến 8 bước để build 1 app đơn giản như Todo List. Về lí thuyết thì là thế, những framework trước đây build Todo App thì dễ còn những app thật thì khó bỏ xừ. Nhưng React Redux build Todo app thì có vẻ nhiều việc phải làm, tuy nhiên khi áp dụng vào các app thực tế thì lại đơn giản

Bắt đầu thôi:

Viết mô tả chi tiết của màn hình

Mock cần bao gồm toàn bộ data và các hiệu ứng hình ảnh( ví dụ như đường màu đỏ gạch ngang TodoItem, hoặc "All" là text bình thường thay vì được gạch chân như một link..)

Chia app ra thành các Components

Chia app thành các components(thành phần) nhỏ dựa trên mục đích của chúng. Ở đây chúng ta có 3 components là "AddTodo", "TodoList" và "Filter".

TERM: "Actions" và "States":

Mỗi components làm 2 việc:

  1. Render DOM dựa trên data. Data này được gọi là một "state".
  2. Lắng nghe người dùng và các sự kiện khác và gửi chúng đến các functions. Những hành động này gọi là "Actions".

Danh sách State và Actions cho mỗi Component

Mời các bạn xem lại cẩn thận các component trong bước 2, và danh sách States và Actions cho mỗi component đó.

Chúng ta có 3 components là "AddTodo", "TodoList" và "Filter". Hãy list ra dánh sách các Actions và State cho mỗi đứa.

1. AddTodo Component - State và Actions

Component này không có state, nhưng nó cần phải để cho các components khác biết khi nào user tạo 1 Todo mới. Hãy đặt tên cho action này là "ADD_TODO".

2. TodoList Component - State và Actions

TodoList component cần một mảng của những thằng Todo để render chính nó ra, vì thế nó cần 1 state, hãy gọi state này là "Todos"(dạng Array). Nó cũng cần biết "Filter" nào đang bật để hiện hay ẩn (show or hide) các Todo items, nên nó cần 1 state khác, hãy tạm gọi là "VisibilityFilter" (dạng boolean).

Hơn nữa, nó cho phép chúng ta thay đổi status của Todo về hoàn thành hoặc chưa hoàn thành (completed and not completed). Chúng ta cần để cho các components khác biết thay đổi này. Ok vậy cho action này tên "TOGGLE_TODO".

3. Filter Component - State và Actions

Filter component render chính nó giống như 1 cái Link hay 1 dòng text bình thường dựa vào việc nó có active hay ko. Hãy gọi state này là "CurrentFilter".

Filter component cũng cần để các components khác biết khi nào user click vào nó. Hãy gọi action này là "SET VIBILITY FILTER"

TERM: "Action Creators":

Action Creator là các function đơn giản dùng để nhận data từ DOM event, format dưới dạng JSON object và trả về kiểu object(hay còn còn là Action). Hơn nữa, nó cho phép các component gửi (còn gọi là dispatch) những actions này đến các component khác.

4. Tạo Action Creators cho mỗi Action

Chúng ta có 3 actions: ADD_TODO, TOGGLE_TODO và SET_VISIBILITY_FILTER. Hãy tạo các Action Creators cho mỗi đứa chúng nó.

//1. Takes the text from AddTodo field and returns proper “Action” JSON to send to other components.
export const addTodo = (text) => {
 return {
 type: ‘ADD_TODO’,
 id: nextTodoId++,
 text,  //<--ES6. same as text:text, in ES5
 completed: false //<-- initially this is set to false
 }
}
 
//2. Takes filter string and returns proper “Action” JSON object to send to other components.
export const setVisibilityFilter = (filter) => {
 return {
 type: ‘SET_VISIBILITY_FILTER’,
 filter
 }
}
 
//3. Takes Todo item’s id and returns proper “Action” JSON object to send to other components.
export const toggleTodo = (id) => {
 return {
 type: ‘TOGGLE_TODO’,
 id
 }
}

TERM: “Reducers”

Reducers là những hàm nhận "state" từ Redux và "action" JSON object, trả về một "state" mới sẽ được Redux lưu trữ lại.

  1. Reducer được gọi bởi "Comtainer" khi có một user action.
  2. Nếu reducer thay đổi state, Redux sẽ truyền state mới này cho cho các component và React render lại các component đó.
For example the below function takes Redux’ state(an array of previous todos), and returns a **new** array of todos(new state) w/ the new Todo added if action’s type is “ADD_TODO”.
const todo = (state = [], action) => {
 switch (action.type) {
  case ‘ADD_TODO’:
     return 
       […state,{id: action.id, text: action.text, completed:false}]; 
 }

5. Viết Reducers cho mỗi Action

const todo = (state, action) => {
  switch (action.type) {
     case ‘ADD_TODO’:
      return […state,{id: action.id, text: action.text, 
              completed:false}]
 
     case ‘TOGGLE_TODO’:
        return state.map(todo =>
                if (todo.id !== action.id) {
                  return todo
                }
                 return Object.assign({}, 
                    todo, {completed: !todo.completed})
            )
 
      case ‘SET_VISIBILITY_FILTER’: {
       return action.filter
      }
 
     default:
      return state
    } 
}

TERM: “Presentational” và “Container” Components

Giữ logic React và Redux bên trong các component có thể gây lộn xộn, vì thế Redux khuyến khích tạo một component "ảo" gọi là ""Presentational" và 1 component cha bọc ngoài gọi là "Container". Container cha truyền data xuống Presentational, xử lý các sự kiện, làm việc với React thay cho Presentational component. Chú thích: Các gạch đứt màu vàng = "Presentational" component. Các gạch đứt màu đen = "Container" component.

6. Implement mỗi Presentational component.

Vào việc nào:

6.1 Implement AddTodoForm Presentational Component.

6.2 — Implement TodoList Presentational Component

6.3 — Implement Link Presentational Component

Lưu ý: Link presentational component được bọc bên trong "FilterLink" container component. Và sau đó 3 "FilterLink" components được hiển thị trong "Footer" presentational component.

7. Tạo Container component cho tất cả Presentational component

7.1 Tạo Container component - AddTodo

import AddTodoForm from '../components/AddTodoForm'

import { connect } from 'react-redux'
import { addTodo } from '../actions'


const mapDispatchToProps = (dispatch) => {
  return {
    onSubmit: (text) => {
      dispatch(addTodo(text))
    }
  }
}

let AddTodo = connect(null, mapDispatchToProps)(AddTodoForm)

export default AddTodo

7.2 Tạo Container Component — TodoList Container

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

7.4 Tạo Container Component — Filter Container

import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onClick: () => {
      dispatch(setVisibilityFilter(ownProps.filter))
    }
  }
}

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

export default FilterLink

8. Gom tất cả lại nào

import React from ‘react’ // ← Main React library
import { render } from ‘react-dom’ // ← Main react library
import { Provider } from ‘react-redux’ //← Bridge React and Redux
import { createStore } from ‘redux’ // ← Main Redux library
import todoApp from ‘./reducers’ // ← List of Reducers we created 
//Import all components we created earlier
import AddTodo from ‘../containers/AddTodo’
import VisibleTodoList from ‘../containers/VisibleTodoList’
import Footer from ‘./Footer’ // ← This is a presentational component that contains 3 FilterLink Container comp
//Create Redux Store by passing it the reducers we created earlier.
let store = createStore(reducers)
render(
 <Provider store={store}> ← The Provider component from react-redux injects the store to all the child components
 <div>
 <AddTodo />
 <VisibleTodoList />
 <Footer />
 </div>
 </Provider>,
 document.getElementById(‘root’) //<-- Render to a div w/ id "root"
)

Xong.

Tham khảo:

Bài viết mình dịch lại của tác giả rajaraodv từ Medium