Viblo Learning
+3

Todo list với React Redux Starter Kit

Nếu các bạn đọc bài viết này thì chắc hẳn các bạn đã theo dõi bài trước của mình. Hôm này mình sẽ giới thiệu với các bạn cách lấy dữ liệu từ một api và tạo một danh sách đơn giản để làm quen với cấu trúc của React Redux Starter Kit. Bài viết của mình là tham khảo từ đây. Mình sử dụng code như ở đó và sẽ viết lại theo ý hiểu của mình. Các bạn có thể kham khảo cả ở đó để hiểu rõ hơn nhé. Tạm thời mình đặt tên là Todo thay cho Zen để làm việc cho dễ hiểu.

Routes index

Thêm TodoRoute(store) vào children trong ./src/routes/index.js

# ./src/routes/index.js
....
import TodoRoute from './Todo'
...
  childRoutes : [
    ...
    TodoRoute(store)
  ]
....

Header

Thêm link tới todo vào trong header thôi.

# ./src/components/Header/Header.js
{' · '}
<Link to='/todo' activeClassName='route--active'>
  Todo
</Link>

Routes Todo

Khi chúng ta thêm một trang mới thì phải tạo một routes cho nó. Thông thường các thành phần như components, containers, interfaces, modules đều được đặt ở trong một thư mục con trong ./src/routes. Ở đây ta đặt nó trong thư mục ./src/routes/Todo. Và file định nghĩa routes cho trang Todo là ./src/routes/Todo/index.js. Cũng giống như với Couter ta chỉ việc đổi lại tên và đường dẫn thôi. 😄

# ./src/routes/Todo/index.js
import { injectReducer } from '../../store/reducers'
export default (store) => ({
  path: 'todo',
  getComponent (nextState, next) {
    require.ensure([
      './containers/TodoContainer',
      './modules/todo'
    ], (require) => {
      const Todo = require('./containers/TodoContainer').default
      const todoReducer = require('./modules/todo').default
      injectReducer(store, {
        key: 'todo',
        reducer: todoReducer
      })
      next(null, Todo)
    })
  }
})

Interfaces

Đây là một thành phần mới mà Counter không có. Với Counter thì ta chỉ có 1 object nên có thể bỏ qua. Nhưng ở đây ta quản lý danh sách các đối tượng là Todo. Nên ta khai bảo 2 đối tượng trong này là TodoObjectTodoStateObject.

# ./src/routes/Todo/interfaces/todo.js

export type TodoObject = {
  id: number,
  value: string
}
export type TodoStateObject = {
  current: ?number,
  fetching: boolean,
  saved: Array<number>,
  todos: Array<TodoObject>
}

Với 2 đối tượng như trên, hệ thống sẽ lưu lại state của Todo với cấu trúc như sau:

state: {  
  todo: {
    current: 2,
    fetching: false,
    todos: [
      { id: 0, value: "..." },
      { id: 1, value: "..." },
      { id: 2, value: "..." }
    ]
    saved: [
      0,
      1
    ]
  }
}

Ý nghĩa các tham số trong đó là:

  • current: Là id của đối tượng Todo đang thao tác
  • todos: Là danh sách các Todo hiện tại
  • saved: Là danh sách các Todo đã được lưu lại.

Data Follow chính của app là:

  • Khi vào sẽ hiển thị danh sách các Todo đã được lưu.
    • Lần đầu thì current là nil, danh sách todos rỗng
  • Có một button Fetch Todo lấy 1 Todo bất kỳ từ api về và hiển thị nó.
    • current sẽ là id của Todo mới lấy về (id này sẽ tăng liên tục).
    • todos gồm tất cả các Todo được fetch về
  • Có 1 button Save Todo dùng dể lưu Todo vừa fetch về vào trong mảng saved.
    • current vẫn là id của Todo mới lấy về
    • saved sẽ được thêm current todo vào danh sách
    • todos không đổi.

Chú ý: Kể cả không khai báo interface này thì hệ thống vẫn chạy ổn định mà không vấn đề gì. Việc khai báo ở đây là để ta có thể hình dung ra được cấu trúc data mình sử dụng. Nó giúp cho chúng người khác đọc hiểu code của mình một cách dễ dàng và nhanh chóng hơn.

Modules

Tại đây, mình handler các action chính để sử dụng.

# ./src/routes/Todo/modules/todo.js

import type { TodoObject, TodoStateObject } from '../interfaces/todo.js'
// ------------------------------------
// Constants
// ------------------------------------
export const REQUEST_TODO: Yêu cầu một todo
= 'REQUEST_TODO'
export const RECIEVE_TODO = 'RECIEVE_TODO'
export const SAVE_CURRENT_TODO = 'SAVE_CURRENT_TODO'
// ------------------------------------
// Actions
// ------------------------------------
export function requestTodo(){
  return {
    type: REQUEST_TODO
  }
}
let availableId = 0
export function recieveTodo(value: string) {
  return {
    type: RECIEVE_TODO,
    payload: {
      value,
      id: availableId++
    }
  }
}
export function saveCurrentTodo() {
  return {
    type: SAVE_CURRENT_TODO
  }
}
...

Chắc hẳn đọc đoạn code trên thì các bạn cũng nhận thấy ở đây có 3 trạng thái là: REQUEST_TODO: Yêu cầu một Todo RECIEVE_TODO: Nhận được một Todo SAVE_CURRENT_TODO: Lưu Todo hiện tại

Next, định nghĩa một hàm gọi api để fecth Todo về.

export const fetchTodo = () => {
  return (dispatch): Promise => {
    dispatch(requestTodo())
    return fetch('https://api.github.com/zen')
      .then(data => data.text())
      .then(text => dispatch(recieveTodo(text)))
  }
}

Định nghĩa các action

export const actions = {  
  requestTodo,
  recieveTodo,
  fetchTodo,
  saveCurrentTodo
}

Ở đây, theo như ví dụ thì ta định nghĩa 4 action. Nhưng trên view chỉ dùng có 2 action là requestTodosaveCurrentTodo nên theo mình thì định nghĩa 2 như sau cũng được

export const actions = {  
  fetchTodo,
  saveCurrentTodo
}

Tiêp theo ta handler các reducers cho các state này:

const TODO_ACTION_HANDLERS = {
  [REQUEST_TODO]: (state: TodoStateObject): TodoStateObject => {
    return ({ ...state, fetching: true })
  },
  [RECIEVE_TODO]: (state: TodoStateObject, action: {payload: TodoObject}): TodoStateObject => {
    return ({ ...state, todos: state.todos.concat(action.payload), current: action.payload.id, fetching: false })
  },
  [SAVE_CURRENT_TODO]: (state: TodoStateObject): TodoStateObject => {
    return state.current != null ? ({ ...state, saved: state.saved.concat(state.current) }) : state
  }
}

Cuối cùng là init State, reducers

const initialState: TodoStateObject = { fetching: false, current: null, todos: [], saved: [] }

export default function todoReducer (state: TodoStateObject = initialState, action: Action): TodoStateObject {
  const handler = TODO_ACTION_HANDLERS[action.type]
  return handler ? handler(state, action) : state
}

Containers

Cấu trúc cũng giống như Counter nhưng ở đây mình import actions trong module Todo luôn chứ không phải là import tên từng action một

import { connect } from 'react-redux'
import { actions } from '../modules/todo'
import Todo from '../components/Todo'

const mapStateToProps = (state) => ({
  todo: state.todo.todos.find(todo => todo.id === state.todo.current),
  todos: state.todo.todos,
  saved: state.todo.todos.filter(todo => state.todo.saved.indexOf(todo.id) !== -1)
})
export default connect(mapStateToProps, {...actions})(Todo)

Với mỗi state được gọi thì ta cập nhật lại 3 attributes để hiển thị trên view (todo, todos, saved)

Components

Cuối cùng là viết view cho trang todo này.

# ./src/routes/Todo/components/todo.js

import React from 'react'
import classes from './Todo.scss'
import type { TodoObject } from '../interfaces/todo'

type Props = {
  todo: ?TodoObject,
  saved: Array<TodoObject>,
  todos: Array<TodoObject>,
  fetchTodo: Function,
  saveCurrentTodo: Function
}

export const Todo = (props: Props) => (
  <div>
  {console.log(props)}
    <div>
      <h2>

        {props.todo ? props.todo.value : ''}
      </h2>
      <button className='btn btn-default' onClick={props.fetchTodo}>
        Fetch Todo
      </button>
      {' '}
      <button className='btn btn-default' onClick={props.saveCurrentTodo}>
        Save Todo
      </button>
    </div>
    {props.saved.length
      ? <div className='savedWisdoms'>
        <h3>
          SAEVES LIST (saved)
        </h3>
        <ul>
          {props.saved.map(todo =>
            <li key={todo.id}>
              {todo.value}
            </li>
          )}
        </ul>
      </div>
      : null
    }
    {props.todos.length
      ? <div className='savedWisdoms'>
        <h3>
          FETCH LIST (todos)
        </h3>
        <ul>
          {props.todos.map(todo =>
            <li key={todo.id}>
              {todo.value}
            </li>
          )}
        </ul>
      </div>
      : null
    }
  </div>
)
Todo.propTypes = {
  todo: React.PropTypes.object,
  saved: React.PropTypes.array.isRequired,
  todos: React.PropTypes.array.isRequired,
  fetchTodo: React.PropTypes.func.isRequired,
  saveCurrentTodo: React.PropTypes.func.isRequired
}

export default Todo

Lưu ý: Định nghĩa kiểu trả về của prop hay type là ko cần thiết. Nó chỉ giúp ta hiểu cấu trúc dữ liệu thôi. Vì thế ta có thể viết lại như sau:

import React from 'react'
import classes from './Todo.scss'

export const Todo = (props) => (
  <div>
  {console.log(props)}
    <div>
      <h2>

        {props.todo ? props.todo.value : ''}
      </h2>
      <button className='btn btn-default' onClick={props.fetchTodo}>
        Fetch Todo
      </button>
      {' '}
      <button className='btn btn-default' onClick={props.saveCurrentTodo}>
        Save Todo
      </button>
    </div>
    {props.saved.length
      ? <div className='savedWisdoms'>
        <h3>
          SAEVES LIST (saved)
        </h3>
        <ul>
          {props.saved.map(todo =>
            <li key={todo.id}>
              {todo.value}
            </li>
          )}
        </ul>
      </div>
      : null
    }
    {props.todos.length
      ? <div className='savedWisdoms'>
        <h3>
          FETCH LIST (todos)
        </h3>
        <ul>
          {props.todos.map(todo =>
            <li key={todo.id}>
              {todo.value}
            </li>
          )}
        </ul>
      </div>
      : null
    }
  </div>
)

export default Todo

Source code tại đây


All Rights Reserved