ReactJS: Cách quản lý state trong React với Redux

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Giới thiệu

Redux là một kho dữ liệu phổ biến cho các ứng dụng JavaScript và React. Nó tuân theo một nguyên tắc trung tâm rằng ràng buộc dữ liệu phải chảy theo một hướng và phải được lưu trữ như một nguồn thật duy nhất. Redux trở nên phổ biến vì sự đơn giản của ý tưởng thiết kế và việc triển khai tương đối nhỏ.

Redux hoạt động theo một vài khái niệm. Đầu tiên, store (kho) là một đối tượng duy nhất với các trường cho mỗi lựa chọn dữ liệu. Bạn cập nhật dữ liệu bằng cách thực hiện một hành động cho biết dữ liệu sẽ thay đổi như thế nào. Sau đó, bạn diễn giải các hành động và cập nhật dữ liệu bằng cách sử dụng các reducer. Reducer là hàm áp dụng các hành động cho dữ liệu và trả về một state mới, thay vì thay đổi state trước đó.

Trong các ứng dụng nhỏ, bạn có thể không cần kho dữ liệu toàn cầu (global). Bạn có thể sử dụng kết hợp state cục bộ và ngữ cảnh để quản lý state. Nhưng khi ứng dụng của bạn mở rộng quy mô, bạn có thể gặp phải các tình huống mà việc lưu trữ thông tin một cách tập trung sẽ có giá trị để thông tin sẽ tồn tại trên các route và component. Trong tình huống đó, Redux sẽ cung cấp cho bạn một cách chuẩn để lưu trữ và truy xuất dữ liệu một cách có tổ chức.

Trong hướng dẫn này, bạn sẽ sử dụng Redux trong ứng dụng React bằng cách xây dựng một ứng dụng thử nghiệm xem chim. Người dùng sẽ có thể thêm những con chim mà họ đã nhìn thấy và tăng thêm một con chim mỗi khi họ nhìn thấy lại. Bạn sẽ xây dựng một kho dữ liệu duy nhất và bạn sẽ tạo các hành động và reducer để cập nhật cửa hàng. Sau đó, bạn sẽ kéo dữ liệu vào các component của mình và gửi các thay đổi mới để cập nhật dữ liệu.

Bước 1 - Tạo một dự án trống

Trong bước này, bạn sẽ tạo một dự án mới bằng Create React App. Sau đó, bạn sẽ xóa dự án mẫu và các tệp liên quan được cài đặt khi bạn khởi động dự án. Cuối cùng, bạn sẽ tạo một cấu trúc tệp đơn giản để tổ chức các component của mình. Điều này sẽ cung cấp cho bạn một cơ sở vững chắc để xây dựng ứng dụng mẫu của hướng dẫn này để tạo style trong bước tiếp theo.

Để bắt đầu, hãy thực hiện một dự án mới. Bạn mở terminal và chạy tập lệnh sau để cài đặt một dự án mới bằng cách sử dụng create-react-app:

npx create-react-app redux-tutorial

Sau khi dự án kết thúc, hãy thay đổi vào thư mục:

cd redux-tutorial

Khởi động dự án với lệnh:

npm start
Bạn sẽ nhận được một máy chủ cục bộ đang chạy. Nếu dự án không mở trong cửa sổ trình duyệt, bạn có thể mở nó bằng http://localhost:3000/. Nếu bạn đang chạy điều này từ một máy chủ từ xa, địa chỉ sẽ là .http://your_domain:3000

Trình duyệt của bạn sẽ tải với một ứng dụng React đơn giản được bao gồm như một phần của Create React App:

Dự án mẫu React

Bạn sẽ xây dựng một tập hợp các component tùy chỉnh hoàn toàn mới, vì vậy bạn sẽ cần bắt đầu bằng cách xóa một số mã soạn sẵn để bạn có thể có một dự án trống.

Để bắt đầu, hãy mở component src/App.js. Đây là component gốc được đưa vào trang. Tất cả các component sẽ bắt đầu từ đây. Bạn sửa lại file để trong đó chỉ còn chứa như sau:

import './App.css';

function App() {
  return <></>;
}

export default App;

Mở một terminal khác và thực hiện thao tác xóa file logo.svg:

rm src/logo.svg

Tạo thư mục components:

mkdir src/components

Tạo thư mục App:

mkdir src/components/App

Di chuyển các file App.* vào thư mục App:

mv src/App.* src/components/App

Mở file index.js và chỉnh sửa:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import * as serviceWorker from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Lưu file lại và quay lại trang web ta được một trang web trống.

màn hình trống trong chrome

Bây giờ bạn đã hoàn thành dự án Tạo ứng dụng React mẫu, hãy tạo một cấu trúc tệp đơn giản. Điều này sẽ giúp bạn giữ cho các component của bạn cô lập và độc lập.

Tạo một thư mục được gọi components trong thư mục src, components sẽ là nơi chứa tất cả các component tùy chỉnh của bạn.

Bước 2 - Thiết lập cửa hàng

Trong bước này, bạn sẽ cài đặt Redux và kết nối nó với thành phần gốc của bạn. Sau đó, bạn sẽ tạo một store cơ sở và hiển thị thông tin trong component của mình. Đến cuối bước này, bạn sẽ có một phiên bản Redux đang hoạt động với thông tin hiển thị trong các component của bạn.

Để bắt đầu, ta sẽ cài đặt redux và react-redux. Gói redux này là framework bất khả tri và sẽ kết nối các hành động của bạn và các reducer. Gói react-redux chứa các ràng buộc để chạy một store Redux trong một dự án React. Bạn sẽ sử dụng mã từ react-redux để gửi các hành động từ các component của mình và để kéo dữ liệu từ store vào các component của bạn.

Sử dụng npm để cài đặt hai gói bằng lệnh sau:

npm install --save redux react-redux

Bây giờ bạn đã cài đặt xong các gói, bạn cần kết nối Redux với dự án của mình. Để sử dụng Redux, bạn sẽ cần phải bọc các component gốc của mình bằng một Provider để đảm bảo rằng store có sẵn cho tất cả các component con trong cây. Điều này tương tự như cách bạn thêm một Provider bằng context gốc của React.

Mở src/index.js ra, import component Provider từ gói react-redux. Thêm Provider vào component gốc của bạn xung quanh bất kỳ component nào khác bằng cách thực hiện các thay đổi được đánh dấu như sau:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';

ReactDOM.render(
  <React.StrictMode>
    <Provider>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Bây giờ bạn đã gói các component của mình, đã đến lúc thêm một storestore là bộ sưu tập dữ liệu trung tâm của bạn. Trong bước tiếp theo, bạn sẽ học cách tạo reducers mà sẽ đặt giá trị mặc định và cập nhật cửa hàng của bạn, nhưng bây giờ bạn sẽ mã hóa dữ liệu.

Import hàm createStore từ redux, sau đó truyền một hàm trả về một đối tượng. Trong trường hợp này, hãy trả về một đối tượng có trường được gọi là trường birds trỏ đến một mảng các loài chim riêng lẻ. Mỗi con chim sẽ có một name và một số đếm views. Lưu đầu ra của hàm tới một giá trị gọi là store, sau đó truyền store tới một prop gọi là store trong Provider:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

const store = createStore(() => ({
  birds: [
    {
      name: 'robin',
      views: 1
    }
  ]
}));

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Lưu file lại. Bây giờ bạn có một số dữ liệu, bạn cần hiển thị được nó. Mở src/components/App/App.js ra. Giống như với context, mọi component con sẽ có thể truy cập vào store mà không cần bất kỳ prop bổ sung nào. Để truy cập các mục trong store Redux của bạn, hãy sử dụng Hook có tên useSelector từ gói react-redux. Hook useSelector có một đối số là một hàm bộ chọn (selector). Hàm bộ chọn sẽ nhận state của store của bạn dưới dạng đối số mà bạn sẽ sử dụng để trả về trường bạn muốn:

import { useSelector } from 'react-redux';
import './App.css';

function App() {
  const birds = useSelector(state => state.birds);

  return <></>
}

export default App;

Vì useSelector là một Hook tùy chỉnh, component sẽ hiển thị lại bất cứ khi nào Hook được gọi. Điều đó có nghĩa là dữ liệu— birds—sẽ luôn được cập nhật.

Bây giờ bạn đã có dữ liệu, bạn có thể hiển thị nó trong một danh sách không có thứ tự. Tạo một phần tử <div> với một className là wrapper. Bên trong, thêm một phần tử <ul> và lặp qua mảng birds sử dụng map(), trả về một <li> cho mỗi phần tử. Hãy chắc chắn sử dụng bird.name như một key:

import { useSelector } from 'react-redux'
import './App.css';

function App() {
  const birds = useSelector(state => state.birds);

  return (
    <div className="wrapper">
      <h1>Bird List</h1>
      <ul>
        {birds.map(bird => (
          <li key={bird.name}>
            <h3>{bird.name}</h3>
            <div>
              Views: {bird.views}
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Lưu file lại và quay lại trang web, bạn sẽ tìm thấy danh sách chim của mình :

Danh sách các loài chim

Bây giờ bạn đã có một danh sách cơ bản, hãy thêm các component bạn cần cho ứng dụng xem chim của mình. Đầu tiên, hãy thêm một nút để tăng lượt xem sau danh sách lượt xem:

import { useSelector } from 'react-redux'
import './App.css';

function App() {
  const birds = useSelector(state => state.birds);

  return (
    <div className="wrapper">
      <h1>Bird List</h1>
      <ul>
        {birds.map(bird => (
          <li key={bird.name}>
            <h3>{bird.name}</h3>
            <div>
              Views: {bird.views}
              <button><span role="img" aria-label="add"></span></button>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Tiếp theo, tạo một <form> với một <input> trước danh sách chim để người dùng có thể thêm vào một con chim mới. Hãy chắc chắn để bao quanh <input> với một <label> và thêm một button với type là submit để đảm bảo tất cả mọi thứ có thể truy cập:

import { useSelector } from 'react-redux'
import './App.css';

function App() {
  const birds = useSelector(state => state.birds);

  return (
    <div className="wrapper">
      <h1>Bird List</h1>
      <form>
        <label>
          <p>
            Add Bird
          </p>
          <input type="text" />
        </label>
        <div>
          <button type="submit">Add</button>
        </div>
      </form>
      <ul>
        {birds.map(bird => (
          <li key={bird.name}>
            <h3>{bird.name}</h3>
            <div>
              Views: {bird.views}
              <button><span role="img" aria-label="add"></span></button>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Lưu file lại.

Tiếp theo, mở ra file App.css ra để thêm một số style:

.wrapper {
    padding: 20px;
}

.wrapper h3 {
    text-transform: capitalize;
}

.wrapper form button {
    margin: 10px 0;
    cursor: pointer;
}

.wrapper ul button {
    background: none;
    border: none;
    cursor: pointer;
}

Lưu file lại và quay lại trang web ta được:

Ứng dụng xem chim có hình thức

Các nút và form chưa được kết nối với bất kỳ hành động nào và do đó không thể tương tác với store của Redux. Bạn sẽ thêm các hành động ở Bước 3 và kết nối chúng ở Bước 4.

Trong bước này, bạn đã cài đặt Redux và tạo một store mới cho ứng dụng của mình. Bạn đã kết nối store với ứng dụng của mình bằng cách sử dụng Provider và truy cập các phần tử bên trong các component của mình bằng Hook useSelector.

Trong bước tiếp theo, bạn sẽ tạo các hành động và reducer để cập nhật thông tin mới cho store của mình.

Bước 3 - Tạo các Hành động và Reducer

Tiếp theo, bạn sẽ tạo các hành động để thêm một con chim và tăng lượt xem. Sau đó, bạn sẽ thực hiện một reducer sẽ cập nhật thông tin tùy thuộc vào loại hành động. Cuối cùng, bạn sẽ sử dụng các reducer để tạo một cửa hàng mặc định bằng cách sử dụng combineReducers.

Hành động là thông điệp bạn gửi đến store với sự thay đổi dự định. Reducer nhận các thông báo đó và cập nhật store được chia sẻ bằng cách áp dụng các thay đổi tùy thuộc vào loại hành động. Các component của bạn sẽ gửi các hành động mà chúng muốn store của bạn sử dụng và các component của bạn sẽ sử dụng các hành động để cập nhật dữ liệu trong store. Bạn không bao giờ gọi trực tiếp reducer và có những trường hợp một hành động có thể tác động đến nhiều reducer.

Có nhiều tùy chọn khác nhau để tổ chức các hành động và reducer của bạn. Trong hướng dẫn này, bạn sẽ sắp xếp theo miền. Điều đó có nghĩa là các hành động và reducer bạn sẽ được xác định theo loại tính năng mà chúng sẽ tác động.

Tạo một thư mục có tên store:

mkdir src/store

Thư mục này sẽ chứa tất cả các hành động và reducer của bạn. Một số mẫu lưu trữ chúng cùng với các component, nhưng lợi thế ở đây là bạn có một điểm tham chiếu riêng cho cấu trúc của toàn bộ store. Khi một nhà phát triển mới tham gia vào dự án, anh ta sẽ có thể đọc sơ qua cấu trúc của store.

Tạo một thư mục tên birds bên trong thư mục store. Điều này sẽ chứa các hành động và reducer cụ thể để cập nhật dữ liệu về chim của bạn:

mkdir src/store/birds

Tiếp theo, tạo một file có tên birds.js để bạn có thể bắt đầu thêm các hành động và reducer. Nếu bạn có một số lượng lớn các hành động và reducer, bạn có thể muốn chia chúng thành các tệp riêng biệt, chẳng hạn như birds.actions.js và birds.reducers.js, nhưng khi có ít, bạn có thể dễ dàng đọc hơn khi chúng ở cùng một vị trí.

Đầu tiên, bạn sẽ tạo các hành động. Hành động là thông báo mà bạn gửi từ một component đến store của mình bằng một phương thức được gọi là phương thức dispatch mà bạn sẽ sử dụng trong bước tiếp theo.

Một hành động phải trả về một đối tượng với một trường type. Nếu không, đối tượng trả về có thể bao gồm bất kỳ thông tin bổ sung nào bạn muốn gửi.

Tạo một hàm được gọi addBirds nhận bird làm đối số và trả về một đối tượng có chứa một type trong 'ADD_BIRD'và bird dưới dạng một trường:

export function addBird(bird) {
  return {
    type: 'ADD_BIRD',
    bird,
  }
}

Lưu ý rằng bạn đang export hàm để sau này bạn có thể import và gửi nó từ component của mình.

Trường type rất quan trọng để giao tiếp với reducer, vì vậy theo quy ước, hầu hết các store Redux sẽ lưu kiểu vào một biến để bảo vệ khỏi lỗi chính tả.

Tạo một lệnh const gọi ADD_BIRD để lưu chuỗi 'ADD_BIRD'. Sau đó, cập nhật hành động:

const ADD_BIRD = 'ADD_BIRD';

export function addBird(bird) {
  return {
    type: ADD_BIRD,
    bird,
  }
}

Bây giờ bạn có một hành động, hãy tạo một reducer để phản hồi lại hành động đó.

Reducer là hàm sẽ xác định trạng thái sẽ thay đổi như thế nào dựa trên các hành động. Các hành động không tự tạo ra thay đổi; reducer sẽ tiếp nhận state và thực hiện các thay đổi dựa trên các hành động.

Một reducer nhận được hai đối số: state hiện tại và hành động. State hiện tại đề cập đến state cho một phần cụ thể của store. Nói chung, tên của reducer sẽ khớp với một trường trong store. Ví dụ: giả sử bạn có một store có dạng như sau:

{
  birds: [
    // tập hợp các đối tượng chim
  ],
  gear: {
    // thông tin thiết bị
  }
}

Thì bạn sẽ tạo hai reducer: birds và gearstate cho reducer birds sẽ là mảng các loài chim. state cho reducer gear sẽ là đối tượng chứa thông tin thiết bị.

Bên trong birds.js tạo reducer tên birds truyền đi hai đối số là state và action và trả về state mà không cần bất kỳ thay đổi nào:

const ADD_BIRD = 'ADD_BIRD';

export function addBird(bird) {
  return {
    type: ADD_BIRD,
    bird,
  }
}

function birds(state, action) {
  return state;
}

Lưu ý rằng bạn không export reducer. Bạn sẽ không trực tiếp sử dụng reducer và thay vào đó sẽ kết hợp chúng thành một bộ sưu tập có thể sử dụng được mà bạn sẽ export và sử dụng để tạo store cơ sở của mình trong index.js. Cũng lưu ý rằng bạn cần phải trả về state nếu không có thay đổi. Redux sẽ chạy tất cả các reducer bất cứ khi nào bạn thực hiện một hành động, vì vậy nếu bạn không trả về state, bạn có nguy cơ mất các thay đổi của mình.

Cuối cùng, vì Redux trả về state nếu không có thay đổi, hãy thêm state mặc định bằng cách sử dụng các tham số mặc định.

Tạo một mảng defaultBirds mà sẽ có một placeholder thông tin về chim. Sau đó, cập nhật state để bao gồm defaultBirds làm tham số mặc định:

const ADD_BIRD = 'ADD_BIRD';

export function addBird(bird) {
  return {
    type: ADD_BIRD,
    bird,
  }
}

const defaultBirds = [
  {
    name: 'robin',
    views: 1,
  }
];

function birds(state=defaultBirds, action) {
  return state;
}

Bây giờ bạn có một reducer trả về state của mình, bạn có thể sử dụng hành động để áp dụng các thay đổi. Mẫu phổ biến nhất là sử dụng switch trên action.type để áp dụng các thay đổi.

Tạo một điều kiện switch để kiểm tra action.type. Nếu case là ADD_BIRD, hãy spread state hiện tại thành một mảng mới và thêm chim với view là 1:

const ADD_BIRD = 'ADD_BIRD';

export function addBird(bird) {
  return {
    type: ADD_BIRD,
    bird,
  }
}

const defaultBirds = [
  {
    name: 'robin',
    views: 1,
  }
];

function birds(state=defaultBirds, action) {
  switch (action.type) {
    case ADD_BIRD:
      return [
        ...state,
        {
          name: action.bird,
          views: 1
        }
      ];
    default:
      return state;
  }
}

Chú ý rằng bạn đang trả về state là giá trị default. Quan trọng hơn, bạn không thay đổi (mutate) state trực tiếp. Thay vào đó, bạn đang tạo một mảng mới bằng cách spread mảng cũ và thêm một giá trị mới.

Bây giờ bạn đã có một hành động, bạn có thể tạo thêm một hành động nữa để tăng một lượt xem.

Tạo một hành động được gọi là incrementBird. Giống như hành động addBird, nó sẽ lấy một con chim làm đối số và trả về một đối tượng với một type và một bird. Sự khác biệt duy nhất là type sẽ là 'INCREMENT_BIRD':

const ADD_BIRD = 'ADD_BIRD';
const INCREMENT_BIRD = 'INCREMENT_BIRD';

export function addBird(bird) {
  return {
    type: ADD_BIRD,
    bird,
  }
}

export function incrementBird(bird) {
  return {
    type: INCREMENT_BIRD,
    bird
  }
}

const defaultBirds = [
  {
    name: 'robin',
    views: 1,
  }
];

function birds(state=defaultBirds, action) {
  switch (action.type) {
    case ADD_BIRD:
      return [
        ...state,
        {
          name: action.bird,
          views: 1
        }
      ];
    default:
      return state;
  }
}

Hành động này là riêng biệt, nhưng bạn sẽ sử dụng cùng một reducer. Hãy nhớ rằng, các hành động truyền tải thay đổi bạn muốn thực hiện trên dữ liệu và reducer áp dụng những thay đổi đó để trả về state mới.

Việc tăng thêm một con chim liên quan đến nhiều hơn một chút so với việc thêm mới một con chim. Bên trong birds thêm một case mới cho INCREMENT_BIRD. Sau đó, kéo con chim bạn cần tăng ra khỏi mảng bằng cách sử dụng find() để so sánh từng name với action.bird:

const ADD_BIRD = 'ADD_BIRD';
...
function birds(state=defaultBirds, action) {
  switch (action.type) {
    case ADD_BIRD:
      return [
        ...state,
        {
          name: action.bird,
          views: 1
        }
      ];
    case INCREMENT_BIRD:
      const bird = state.find(b => action.bird === b.name);
      return state;
    default:
      return state;
  }
}

Bạn đã có con chim bạn cần thay đổi, nhưng bạn cần phải trả về state mới chứa tất cả các con chim không thay đổi cũng như con chim bạn đang cập nhật. Chọn tất cả các con chim còn lại state.filter bằng cách tất cả các con chim mà name không action.name. Sau đó, trả về một mảng mới bằng cách spread mảng birds và thêm bird vào cuối:

const ADD_BIRD = 'ADD_BIRD';
...

function birds(state=defaultBirds, action) {
  switch (action.type) {
    case ADD_BIRD:
      return [
        ...state,
        {
          name: action.bird,
          views: 1
        }
      ];
    case INCREMENT_BIRD:
      const bird = state.find(b => action.bird === b.name);
      const birds = state.filter(b => action.bird !== b.name);
      return [
        ...birds,
        bird,
      ];
    default:
      return state;
  }
}

Cuối cùng, cập nhật bird bằng cách tạo một đối tượng mới với view tăng lên 1:

const ADD_BIRD = 'ADD_BIRD';
...
function birds(state=defaultBirds, action) {
  switch (action.type) {
    case ADD_BIRD:
      return [
        ...state,
        {
          name: action.bird,
          views: 1
        }
      ];
    case INCREMENT_BIRD:
      const bird = state.find(b => action.bird === b.name);
      const birds = state.filter(b => action.bird !== b.name);
      return [
        ...birds,
        {
          ...bird,
          views: bird.views + 1
        }
      ];
    default:
      return state;
  }
}

Lưu ý rằng bạn không sử dụng reducer để sắp xếp dữ liệu. Việc sắp xếp có thể là cần thiết đối với chế độ xem vì chế độ xem sẽ thị thông tin cho người dùng. Bạn có thể có một chế độ xem sắp xếp theo tên và một chế độ xem sắp xếp theo số lượt xem, vì vậy tốt hơn nên để các component riêng lẻ xử lý việc sắp xếp. Thay vào đó, hãy giữ cho các reducer tập trung vào việc cập nhật dữ liệu và component tập trung vào việc chuyển đổi dữ liệu sang chế độ xem có thể sử dụng cho người dùng.

Reducer này cũng không hoàn hảo vì bạn có thể thêm các loài chim có cùng tên. Trong một ứng dụng production, bạn sẽ cần phải hoặc là xác thực trước khi thêm hoặc là cung cấp cho mỗi loài chim một id duy nhất để bạn có thể chọn chim thông qua id thay vì thông qua name.

Đến đây thì bạn có hai hành động hoàn chỉnh và một reducer. Bước cuối cùng là export reducer để nó có thể khởi tạo store. Trong bước đầu tiên, bạn đã tạo store bằng cách truyền một hàm mà trả về một đối tượng. Bạn sẽ làm điều tương tự trong trường hợp này. Hàm sẽ mang hai tham số là store và action và sau đó truyền slice cụ thể của store tới reducer cùng với hành động. Nó sẽ trông giống như thế này:

export function birdApp(store={}, action) {
    return {
        birds: birds(store.birds, action)
    }
}

Để đơn giản hóa mọi thứ, Redux có một hàm trợ giúp được gọi là combineReducers để kết hợp các reducer cho bạn.

Bên trong của birds.js, import combineReducers từ redux. Sau đó gọi hàm với birds và export kết quả:

import { combineReducers } from 'redux';
const ADD_BIRD = 'ADD_BIRD';
const INCREMENT_BIRD = 'INCREMENT_BIRD';

export function addBird(bird) {
  return {
    type: ADD_BIRD,
    bird,
  }
}

export function incrementBird(bird) {
  return {
    type: INCREMENT_BIRD,
    bird
  }
}

const defaultBirds = [
  {
    name: 'robin',
    views: 1,
  }
];

function birds(state=defaultBirds, action) {
  switch (action.type) {
    case ADD_BIRD:
      return [
        ...state,
        {
          name: action.bird,
          views: 1
        }
      ];
    case INCREMENT_BIRD:
      const bird = state.find(b => action.bird === b.name);
      const birds = state.filter(b => action.bird !== b.name);
      return [
        ...birds,
        {
          ...bird,
          views: bird.views + 1
        }
      ];
    default:
      return state;
  }
}

const birdApp = combineReducers({
  birds
});

export default birdApp;

Lưu file lại.

Đến đây thì tất cả các action và reducer đều đã được set up. Bước cuối cùng bây giờ là khởi tạo store của bạn sử dụng các reducer đã được kết hợp thay vì hàm placeholder.

Mở file src/index.js ra, import file birds.js vào. Sau đó khởi tạo store sử dụng birdApp:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import birdApp from './store/birds/birds';

const store = createStore(() => ({
  birds: [
    {
      name: 'robin',
      views: 1
    }
  ]
}));

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Lưu file lại.

Như vậy trong bước này, bạn đã tạo các action (hành động) và reducer. Bạn đã học cách tạo action trả về một type và cách tạo reducer sử dụng action để tạo và trả về state mới dựa trên action. Cuối cùng, bạn kết hợp các reducer thành một hàm mà bạn đã sử dụng để khởi tạo store.

Store Redux của bạn hiện đã được thiết lập xong và sẵn sàng cho các thay đổi. Trong bước tiếp theo, bạn sẽ gửi các action từ một component để cập nhật dữ liệu.

Bước 4 - Điều phối các thay đổi trong một component

Trong bước này, bạn sẽ import và gọi các action từ component của mình. Bạn sẽ sử dụng một phương thức được gọi là dispatch để gửi action và bạn sẽ gửi các hành động bên trong trình xử lý sự kiện cho form và button.

Đến cuối bước này, bạn sẽ có một ứng dụng hoạt động kết hợp store Redux và các component tùy chỉnh của bạn. Bạn sẽ có thể cập nhật store Redux theo thời gian thực và sẽ có thể hiển thị thông tin trong component của bạn khi nó thay đổi.

Bây giờ bạn có các hành động đang làm việc, bạn cần kết nối chúng với các sự kiện của mình để có thể cập nhật store. Phương thức bạn sẽ sử dụng đó là dispatch và nó sẽ gửi một hành động cụ thể đến store Redux. Khi Redux nhận được một hành động mà bạn đã gửi đi, nó sẽ truyền hành động đó cho các reducer và chúng sẽ cập nhật dữ liệu.

Mở file App.js ra, bên trong App.js bạn import Hook useDispath từ react-redux. Sau đó gọi hàm để tạo một hàm dispatch mới:

import React from 'react';
import { useDispatch, useSelector } from 'react-redux'
import './App.css';

function App() {
  ...
}

export default App;

Tiếp theo, bạn sẽ cần import các hành động của mình. Hãy nhớ rằng, các hành động là các hàm trả về một đối tượng. Đối tượng là thứ cuối cùng bạn sẽ truyền vào hàm dispatch.

Import incrementBird từ store. Sau đó, tạo một sự kiện onClick trên nút. Khi người dùng nhấp vào nút, hãy gọi incrementBird bằng bird.name và truyền kết quả cho dispatch. Để làm cho mọi thứ dễ đọc hơn, hãy gọi hàm incrementBird bên trong của dispatch:

import { useDispatch, useSelector } from 'react-redux'
import { incrementBird } from '../../store/birds/birds';
import './App.css';

function App() {
  const birds = useSelector(state => state.birds);
  const dispatch = useDispatch();

  return (
    <div className="wrapper">
      <h1>Bird List</h1>
      <form>
        <label>
          <p>
            Add Bird
          </p>
          <input type="text" />
        </label>
        <div>
          <button type="submit">Add</button>
        </div>
      </form>
      <ul>
        {birds.map(bird => (
          <li key={bird.name}>
            <h3>{bird.name}</h3>
            <div>
              Views: {bird.views}
              <button onClick={() => dispatch(incrementBird(bird.name))}><span role="img" aria-label="add"></span></button>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Lưu file lại và quay lại trang web, bạn sẽ có thể tăng số lượng robin lên:

Gia tăng một con chim

Tiếp theo, bạn cần gửi hành động addBird. Điều này sẽ thực hiện hai bước: lưu dữ liệu input vào state nội bộ và kích hoạt dispatch với onSubmit.

Sử dụng Hook useState để lưu giá trị đầu vào. Đảm bảo chuyển đổi input thành một thành phần được kiểm soát bằng cách cài đặt value trên nó. Hãy xem hướng dẫn Cách tạo form trong React để có cái nhìn chuyên sâu hơn về các thành phần được kiểm soát.

Thực hiện các thay đổi sau đối với file App.js của bạn như sau:

import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { incrementBird } from '../../store/birds/birds';
import './App.css';

function App() {
  const [birdName, setBird] = useState('');
  const birds = useSelector(state => state.birds);
  const dispatch = useDispatch();

  return (
    <div className="wrapper">
      <h1>Bird List</h1>
      <form>
        <label>
          <p>
            Add Bird
          </p>
          <input
            type="text"
            onChange={e => setBird(e.target.value)}
            value={birdName}
          />
        </label>
        <div>
          <button type="submit">Add</button>
        </div>
      </form>
      <ul>
        ...
      </ul>
    </div>
  );
}

export default App;

Tiếp theo, import addBird từ birds.js, sau đó tạo một hàm được gọi là handleSubmit. Bên trong handleSubmit, ta ngăn việc gửi form bằng event.preventDefault, sau đó gửi hành động addBird với birdName làm đối số. Sau khi thực hiện hành động, hãy gọi setBird('') để xóa đầu vào. Cuối cùng, truyền handleSubmit cho trình xử lý sự kiện onSubmit trên form:

import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { addBird, incrementBird } from '../../store/birds/birds';
import './App.css';

function App() {
  const [birdName, setBird] = useState('');
  const birds = useSelector(state => state.birds);
  const dispatch = useDispatch();

  const handleSubmit = event => {
    event.preventDefault();
    dispatch(addBird(birdName))
    setBird('');
  };

  return (
    <div className="wrapper">
      <h1>Bird List</h1>
      <form onSubmit={handleSubmit}>
        <label>
          <p>
            Add Bird
          </p>
          <input
            type="text"
            onChange={e => setBird(e.target.value)}
            value={birdName}
          />
        </label>
        <div>
          <button type="submit">Add</button>
        </div>
      </form>
      <ul>
        {birds.map(bird => (
          <li key={bird.name}>
            <h3>{bird.name}</h3>
            <div>
              Views: {bird.views}
              <button onClick={() => dispatch(incrementBird(bird.name))}><span role="img" aria-label="add"></span></button>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Lưu file lại và quay lại trang web, bạn sẽ có thể thêm được chim:

Cứu con chim mới

Bây giờ bạn đang gọi các hành động của mình và cập nhật danh sách các loài chim của bạn trong store. Lưu ý rằng khi ứng dụng của bạn được làm mới, bạn sẽ mất thông tin trước đó. Store lưu trữ tất cả trong bộ nhớ và do đó, việc làm mới trang sẽ xóa sạch dữ liệu.

Thứ tự danh sách này cũng sẽ thay đổi nếu bạn tăng view của một con chim lên trong danh sách.

Robin đi đến cuối khi sắp xếp lại

Như bạn đã thấy trong Bước 3, reducer của bạn không liên quan đến việc sắp xếp dữ liệu. Để ngăn sự thay đổi không mong muốn trong các component, bạn có thể sắp xếp dữ liệu trong component của mình. Thêm một hàm sort() vào mảng birds. Hãy nhớ rằng việc sắp xếp sẽ làm thay đổi mảng và bạn không bao giờ nên thay đổi store. Đảm bảo tạo một mảng mới bằng cách spread dữ liệu trước khi sắp xếp:

import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { addBird, incrementBird } from '../../store/birds/birds';
import './App.css';

function App() {
  const [birdName, setBird] = useState('');
  const birds = [...useSelector(state => state.birds)].sort((a, b) => {
    return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
  });
  const dispatch = useDispatch();

  const handleSubmit = event => {
    event.preventDefault();
    dispatch(addBird(birdName))
    setBird('');
  };

  return (
    <div className="wrapper">
      <h1>Bird List</h1>
      <form onSubmit={handleSubmit}>
        <label>
          <p>
            Add Bird
          </p>
          <input
            type="text"
            onChange={e => setBird(e.target.value)}
            value={birdName}
          />
        </label>
        <div>
          <button type="submit">Add</button>
        </div>
      </form>
      <ul>
        {birds.map(bird => (
          <li key={bird.name}>
            <h3>{bird.name}</h3>
            <div>
              Views: {bird.views}
              <button onClick={() => dispatch(incrementBird(bird.name))}><span role="img" aria-label="add"></span></button>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Lưu file lại và quay lại trang web, ta sẽ thấy các thành phần sẽ giữ nguyên theo thứ tự bảng chữ cái khi bạn tăng lượt view cho các con chim.

Cardinal luôn ở trên cùng

Điều quan trọng là không thử và làm quá nhiều trong store Redux của bạn. Giữ các reducer tập trung vào việc duy trì thông tin cập nhật, sau đó kéo và thao tác dữ liệu cho người dùng của bạn bên trong component.

Lưu ý: Trong bài hướng dẫn này, hãy lưu ý rằng có một lượng mã hợp lý cho mỗi hành động và reducer. May mắn thay, có một dự án được hỗ trợ chính thức gọi là Bộ công cụ Redux có thể giúp bạn giảm lượng mã soạn sẵn. Bộ công cụ Redux cung cấp một tập hợp các tiện ích phù hợp để nhanh chóng tạo các hành động và reducer, đồng thời cũng sẽ cho phép bạn tạo và định cấu hình store của mình với ít mã hơn.

Như vậy trong bước này, bạn đã gửi các hành động của mình từ một thành phần. Bạn đã học cách gọi các hành động và cách gửi kết quả đến một hàm dispatch (điều phối) và bạn đã kết nối chúng với các trình xử lý sự kiện trên các thành phần của mình để tạo một store tương tác đầy đủ. Cuối cùng, bạn đã học được cách duy trì trải nghiệm người dùng nhất quán bằng cách sắp xếp dữ liệu mà không làm thay đổi trực tiếp store.

Phần kết luận

Redux là một store đơn lẻ phổ biến. Nó có thể thuận lợi khi làm việc với các thành phần cần một nguồn thông tin chung. Tuy nhiên, không phải lúc nào nó cũng là sự lựa chọn đúng đắn trong mọi dự án. Các dự án nhỏ hơn hoặc các dự án có các thành phần biệt lập sẽ có thể sử dụng ngữ cảnh và quản lý state được tích hợp sẵn. Nhưng khi các ứng dụng của bạn ngày càng phức tạp, bạn có thể thấy rằng bộ nhớ trung tâm là rất quan trọng để duy trì tính toàn vẹn của dữ liệu. Trong những trường hợp như vậy, Redux là một công cụ tuyệt vời để tạo một kho dữ liệu thống nhất duy nhất mà bạn có thể sử dụng trên các thành phần của mình mà không tốn nhiều công sức.

» Tiếp: Cách xử lý định tuyến (route) trong ứng dụng React với React Router
« Trước: Cách gọi API web với Hook useEffect trong React
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!