Nay mình sẽ làm một ví dụ về Redux Toolkit + Thunk Middleware + Restful API. Trong bài này tương đối là dài, mong mọi người chịu khó xem nhé!
Ở các bài trước mình đã chia sẻ về Redux-Toolkit rồi nên ở phần này mình đi vào trọng tâm xử lý hành động thôi. Bạn nào chưa xem bài trước có thể xem lại tại đây :
Okay bắt đầu thôi!
Video Demo:
# Tạo project Redux-Toolkit
npx create-react-app my-app --template redux
Tiếp theo ta cần cài thư viện Redux-Toolkit , bằng lệnh sau:
npm install @reduxjs/toolkit
Nếu mọi thứ tiến triển ok, bạn sẽ được một project chứa template redux sẵn, có sẵn mọi thứ, việc còn lại chỉ cần setup các component xử lý thôi
Các bước bên dưới sẽ tạo ra thư mục product trong folder src/features như hình sau:
# Call Middleware Thunk in Redux-Toolkit
Như mình đã nói ở bài trước là, Redux-Toolkit đã mặt định thêm cho ta middleware Thunk rồi, nên ta chỉ cần khai báo và dùng thôi
Tạo file productSlice.js trong đường dẫn ./features/products, và import thư viện sau
import { createAsyncThunk, createSlice, current } from '@reduxjs/toolkit';
# Tạo các actions từ hàm createAsyncThunk()
Tại file productSlice.js ta cấu hình các action từ hàm createAsyncThunk() , đoạn code dưới đây nó giúp ta request api , nó sẽ trả về một promise, chứa 3 tham số(pending, fulfilled, rejected) ta sẽ kiểm tra nó trong extraReducers của createSlice, mình sẽ làm ở các bước sau
Chúng ta cũng biết được là createAsyncThunk() có 2 tham số : (string, callback)
string : ta đặt name cho nó và nó được đặt theo cú pháp như sau: [slice name/ action name]
callback() : nó xử lý bất đồng bộ , trả về một promise, như mình mới nói ở bên trên
export const getProducts = createAsyncThunk( 'products/getProducts', async (thunkAPI) => { //call api const res = await fetch('https://5adc8779b80f490014fb883.mockapi.io/products').then( (data) => data.json() ) return res })
thunkAPI: một đối tượng chứa tất cả các tham số thường được truyền cho chức năng thunk của Redux, cũng như các tùy chọn bổ sung: dispatch, getState, extra, rejectWithValue(value, [meta]), ...
Bạn có thể xem thêm ở link này :thunkAPI: https://redux-toolkit.js.org/api/createAsyncThunk#payloadcreator
Chính vì thế ta có thể dispatch action khác , VD: dispatch(actionLoading()) bên trong hàm aysnc()
Tương tự như tạo các action từ createAsyncThunk() bên trên ta sẽ tạo các action để dispatch xử lý các tác vụ( Create, Edit, Update,Delete), bạn chú ý đoạn code bên dưới, ta có chèn tham số nửa nhé
export const getProductId = createAsyncThunk( 'products/getProductId', async (productId, { dispatch }) => { const response = await fetch(`https://5adc8779b80f490014fb883.mockapi.io/products/${productId}`).then( (data) => data.json() ) const finalPayload = response //dispatch(someOtherAction()) return finalPayload; // will dispatch `fulfilled` action } ) export const postAddProduct = createAsyncThunk( 'products/postAddProduct', async (product, { dispatch }) => { const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(product) }; const response = await fetch(`https://5adc8779b80f490014fb883.mockapi.io/products`,requestOptions).then( (data) => data.json() ) const finalPayload = response return finalPayload; } ) export const postUpdateProduct = createAsyncThunk( 'products/postUpdateProduct', async (product, { dispatch }) => { const requestOptions = { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(product) }; const response = await fetch(`https://5adc8779b80f490014fb883.mockapi.io/products/${product.id}`,requestOptions).then( (data) => data.json() ) const finalPayload = response return finalPayload; } ) export const postDeleteProduct = createAsyncThunk( 'products/postDeleteProduct', async (product, { dispatch }) => { const requestOptions = { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(product) }; const response = await fetch(`https://5adc8779b80f490014fb883.mockapi.io/products/${product.id}`,requestOptions).then( (data) => data.json() ) const finalPayload = response return finalPayload; } )
Bạn có thể tạo một file riêng để viết các request api cho dễ nhìn nhé, tại ở đây mình làm chung nên nó hơi nhiều code
# Tạo hàm từ createSlice(), để kiểm trả các promise trong extraReducers
Trước tiên mình sẽ tạo một initialState để chưa data
//setup state const initialState = { loading:false, products:[], isFetchProductID:false, product:{} };
Okay tiếp theo là tạo hàm từ createSlice() thôi
export const productSlice = createSlice({ name: 'products', initialState, reducers: {}, extraReducers: { //set get all product [getProducts.pending]: (state) => { state.loading = true }, [getProducts.fulfilled]: (state, { payload }) => { state.loading = false state.products = payload }, [getProducts.rejected]: (state) => { state.loading = false }, //set get product Id [getProductId.pending]: (state) => { state.isFetchProductID = true }, [getProductId.fulfilled]: (state, { payload }) => { state.isFetchProductID = false state.product = payload }, [getProductId.rejected]: (state) => { state.isFetchProductID = false }, //set post Product [postAddProduct.fulfilled]:(state,{payload})=>{ state.products.push(payload); }, //set update Product [postUpdateProduct.fulfilled]:(state,{payload})=>{ const index = state.products.findIndex(product => product.id === payload.id); //console.log(index) // console.log(payload) state.products[index] = payload; }, //set delete Product [postDeleteProduct.fulfilled]:(state,{payload})=>{ const index = state.products.findIndex(product => product.id === payload.id); state.products.splice(index, 1); } }, });
Mình sẽ nói qua về đoạn mã trên cho mọi người hiểu thế này. Bạn chú ý ở chổ extraReducers mình cần xác nhận các giai đoạn sau :
getProducts.pending : hành động của ta đang thực thi, có nghĩa là nó đang request api
getProducts.fulfilled : nếu xử lý thành công, ta tiến hành cập nhật state của ta
getProducts.rejected : nếu xử lý không thành công
Okay, mấy đoạn code phía trên cũng tương đối đơn giản, nên mình sẽ không nói nhiều sẽ làm bài viết dài thêm
Đây là fullcode của file productSlice.js
import { createAsyncThunk, createSlice, current } from '@reduxjs/toolkit'; export const getProducts = createAsyncThunk( 'products/getProducts', async (thunkAPI) => { //call api const res = await fetch('https://5adc8779b80f490014fb883.mockapi.io/products').then( (data) => data.json() ) return res }) export const getProductId = createAsyncThunk( 'products/getProductId', async (productId, { dispatch }) => { const response = await fetch(`https://5adc8779b80f490014fb883.mockapi.io/products/${productId}`).then( (data) => data.json() ) const finalPayload = response //dispatch(someOtherAction()) return finalPayload; // will dispatch `fulfilled` action } ) export const postAddProduct = createAsyncThunk( 'products/postAddProduct', async (product, { dispatch }) => { const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(product) }; const response = await fetch(`https://5adc8779b80f490014fb883.mockapi.io/products`,requestOptions).then( (data) => data.json() ) const finalPayload = response return finalPayload; } ) export const postUpdateProduct = createAsyncThunk( 'products/postUpdateProduct', async (product, { dispatch }) => { const requestOptions = { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(product) }; const response = await fetch(`https://5adc8779b80f490014fb883.mockapi.io/products/${product.id}`,requestOptions).then( (data) => data.json() ) const finalPayload = response return finalPayload; } ) export const postDeleteProduct = createAsyncThunk( 'products/postDeleteProduct', async (product, { dispatch }) => { const requestOptions = { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(product) }; const response = await fetch(`https://5adc8779b80f490014fb883.mockapi.io/products/${product.id}`,requestOptions).then( (data) => data.json() ) const finalPayload = response return finalPayload; } ) //setup state const initialState = { loading:false, products:[], isFetchProductID:false, product:{} }; export const productSlice = createSlice({ name: 'products', initialState, reducers: {}, extraReducers: { //set get all product [getProducts.pending]: (state) => { state.loading = true }, [getProducts.fulfilled]: (state, { payload }) => { state.loading = false state.products = payload }, [getProducts.rejected]: (state) => { state.loading = false }, //set get product Id [getProductId.pending]: (state) => { state.isFetchProductID = true }, [getProductId.fulfilled]: (state, { payload }) => { state.isFetchProductID = false state.product = payload }, [getProductId.rejected]: (state) => { state.isFetchProductID = false }, //set post Product [postAddProduct.fulfilled]:(state,{payload})=>{ state.products.push(payload); }, //set update Product [postUpdateProduct.fulfilled]:(state,{payload})=>{ const index = state.products.findIndex(product => product.id === payload.id); //console.log(index) // console.log(payload) state.products[index] = payload; }, //set delete Product [postDeleteProduct.fulfilled]:(state,{payload})=>{ const index = state.products.findIndex(product => product.id === payload.id); state.products.splice(index, 1); } }, }); export default productSlice.reducer;
# Cài đặt configureStore từ Redux-Toolkit
Hãy mở file app/store.js và chỉnh sửa như sau
import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; import productReducer from '../features/products/productSlice'; export const store = configureStore({ reducer: { // counter: counterReducer, products: productReducer }, // middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(customMiddleware), });
Các bạn nhìn trong code trên ta cần gọi productReducer từ '../features/products/productSlice' , sau đó config nó trong hàm configureStore(), nếu bạn không làm điều này, thì các dispatch action sẽ không được thực thi
Mình có note lại middleware bên trên , ta có thể custom middleware nhé, bài trước mình đã có giới thiệu rồi, bạn nào chưa xem , thì xem lại tại đây: Creating custom Middleware in React/Redux
# Cài đặt Component Product
Giờ là lúc ta triển khai ứng dụng của ta thôi, hãy tạo file Layout.js trong đường dẫn như hình project bên trên, à quên bạn cần cài đặt thư viện react-router-dom để thiết lập các Route cho ứng dụng nhé
Bạn có thể xem bài hướng dẫn cài đặt React Router Dom ở link này : REACT ROUTE DOM V6 . Còn bạn nào sử dụng phiên bản thấp hơn thì xem link này: URL Router in React
import React from 'react' import { Outlet } from 'react-router-dom' export default function ProductLayout() { return ( <div> <Outlet /> </div> ) }
+ File List.js : hiển thị danh sách product
import React, { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; import { getProducts, postDeleteProduct } from '../products/productSlice'; export default function ProductsList() { const dispatch = useDispatch(); const { products, loading } = useSelector((state) => state.products) let fetchMount = true; useEffect(() => { if(fetchMount){ dispatch(getProducts()) } return ()=>{ fetchMount = false; } }, []) if(loading) return <div>Loading...</div> return ( <div> <h2>All Products</h2> <ul> {products.map((product) => ( <li key={product.id}> <Link to={`/products/${product.id}`} style={{fontSize:20}}>{product.title}</Link>--- [<span style={{color:"red",cursor:"pointer"}} onClick={()=>dispatch(postDeleteProduct(product))}>Delete</span> | <Link to={`/products/${product.id}/edit`} style={{color:"green",cursor:"pointer"}}>Edit</Link>] </li> ))} </ul> <br/> <Link to={`/products/new`} style={{backgroundColor:'blue',color:"#fff",padding:5}}>Add Product</Link> </div> ) }
Mấy cái dispatch() thì chắc mọi người cũng đã hiểu qua rồi, bạn nào chưa hình dung , hãy qua Redux trước nhé
+ File Item.js : Hiển thị bài viết theo ID
import React, { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux'; import { Link, useParams } from 'react-router-dom' import { getProductId } from './productSlice'; export default function ProductItem() { const {productId} = useParams(); const dispatch = useDispatch(); const {product, isFetchProductID} = useSelector((state)=>state.products) let fetchMount = true; useEffect(()=>{ if(fetchMount) dispatch(getProductId(productId)) //unmount return ()=>fetchMount = false; },[]) if(isFetchProductID) return <div>Loading...!</div> return ( <div> <img src={product.image} style={{width:100,height:100}} /><br/> <span>Id: {productId}</span><br/> <span>Title: {product.title}</span><br /> <span>Price: {product.price}</span><br /> <Link to={"/"}>Back Home</Link> </div> ) }
+ Firle Form.js : dùng để create product
import React, { useState } from 'react' import { useDispatch } from 'react-redux' import { useNavigate } from 'react-router-dom'; import { postAddProduct } from './productSlice'; export default function ProductForm() { const dispatch = useDispatch(); const navigate = useNavigate(); const [title,setTitle] = useState(""); const [image,setImage] = useState(""); const [price,setPrice] = useState(0); const add_product = async ()=>{ let product = { title:title, image:image, price:price } console.log(product) //dispatch action postAddProduct từ createAsyncThunk await dispatch(postAddProduct(product)) navigate(`/products`); } return ( <div> <h1>New Product</h1> <input type="text" name="title" placeholder="Title" onChange={(e)=>setTitle(e.target.value)}/> <br/> <input type="text" name="image" placeholder="Link Image" onChange={(e)=>setImage(e.target.value)}/><br/> <input type="price" name="price" placeholder="Price" onChange={(e)=>setPrice(e.target.value)}/><br/> <button onClick ={()=>add_product()}>Send Form</button> </div> ) }
+ File Edit.js : dùng cập nhật product
import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useParams, useNavigate } from 'react-router-dom'; import { postUpdateProduct } from './productSlice'; export default function ProductEdit() { //Get Params productId từ URL const {productId} = useParams(); const navigate = useNavigate(); const dispatch = useDispatch(); const {products,loading} = useSelector((state)=>state.products) const [title,setTitle] = useState(""); const [image,setImage] = useState(""); const [price,setPrice] = useState(0); useEffect(()=>{ products.map(item=>{ if(item.id === productId){ setTitle(item.title) setImage(item.image) setPrice(item.price) } }) },[loading]) const update_product = async ()=>{ let product = { id:productId, title:title, image:image, price:price } console.log(product) //dispatch action postUpdateProduct từ createAsyncThunk await dispatch(postUpdateProduct(product)) navigate(`/products/${productId}`); } if(loading) return <div>Loading...!</div> return ( <div> <h1>Update Product</h1> <input type="text" name="title" value={title} placeholder="Title" onChange={(e)=>setTitle(e.target.value)}/> <br/> <input type="text" name="image" value={image} placeholder="Link Image" onChange={(e)=>setImage(e.target.value)}/><br/> <input type="price" name="price" value={price} placeholder="Price" onChange={(e)=>setPrice(e.target.value)}/><br/> <button onClick ={()=>update_product()}>Update Product</button> </div> ) }
# Cầu hình Route cho ứng dụng
Tại file src/App.js ta cần cấu hình đường dẫn Route cho các component , nảy mình có cài thư viện React Router Dom rồi, thì việc còn lại chỉ cấu hình điều hướng các Route thôi
import React from 'react'; import './App.css'; import { Routes, Route, Link } from "react-router-dom"; import ProductsList from './features/products/List'; import ProductLayout from './features/products/Layout'; import ProductForm from './features/products/Form' import ProductItem from './features/products/Item'; import ProductEdit from './features/products/Edit'; function App() { return ( <div className="App"> <header> <h1>Welcome to Hoanguyenit!</h1> </header> <Routes> <Route path="/" element={<ProductsList />} /> <Route path="products" element={<ProductLayout />}> <Route index element={<ProductsList />} /> <Route path="new" element={<ProductForm />} /> <Route path=":productId" element={<ProductItem />} /> <Route path=":productId/edit" element={<ProductEdit />} /> </Route> </Routes> </div> ); } export default App;
# Chỉnh sửa tập tin index.js
import React from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { store } from './app/store'; import App from './App'; import reportWebVitals from './reportWebVitals'; import './index.css'; import { BrowserRouter } from "react-router-dom"; const container = document.getElementById('root'); const root = createRoot(container); root.render( <React.StrictMode> <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider> </React.StrictMode> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
# Hình ảnh Demo
Hình 1: Load danh sách product, đó mặc định chạy comonent List.js
Hình 2 : Form tạo product, component Form.js
Hình 3 : Sau khi thêm product thành công, thì redirect về trang danh sách, bạn nhìn mấy chổ console nhé
Hình 4 : hiển thị product theo id được click, component Item.js