Todo list với React Redux Starter Kit
Bài đăng này đã không được cập nhật trong 7 năm
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à TodoObject
và TodoStateObject
.
# ./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áchtodos
rỗng
- Lần đầu thì
- 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êmcurrent
todo vào danh sáchtodos
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à requestTodo
và saveCurrentTodo
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