Redux Toolkit: RTK and RTK Query – All In One

7 min read

Redux Toolkit (RTK) là một thư viện JS chính thức được phát triển bởi nhóm Redux, nhằm đơn giản hóa việc sử dụng Redux trong các ứng dụng JavaScript/TypeScript. Nó cung cấp một bộ công cụ và các hàm tiện ích giúp giảm thiểu cấu hình cần thiết và làm cho việc viết mã Redux dễ dàng và hiệu quả hơn.

Redux Toolkit Query (RTKQ) là một tiện ích bổ sung trong RTK package,là công cụ mạnh mẽ fetching và caching data trong ứng dụng.

Trong bài viết này, tôi sẽ hướng dẫn các bạn những hướng dẫn cơ bản để cài đặt và chạy 1 ứng dụng sử dụng RTK và RTKQ.

Cài đặt Redux Toolkit

Các tính năng chính của Redux Toolkit

  1. configureStore:
    • Đơn giản hóa việc tạo một Redux store với các giá trị mặc định.
    • Tự động thiết lập Redux DevTools và các middleware phổ biến như redux-thunk.
  2. createSlice:
    • Tạo ra các action creators và action types tương ứng với các reducer một cách tự động.
    • Giúp viết các reducer ngắn gọn và dễ đọc hơn.
  3. createAsyncThunk:
    • Tạo ra các action creators cho các thao tác bất đồng bộ.
    • Tích hợp sẵn với Redux để quản lý trạng thái yêu cầu và xử lý lỗi.
  4. createEntityAdapter:
    • Cung cấp các chức năng tiện ích để quản lý các tập hợp dữ liệu dạng normalized trong state.
  5. Redux DevTools và Middleware:
    • Tích hợp sẵn với Redux DevTools để dễ dàng kiểm tra và gỡ lỗi.
    • Hỗ trợ thêm các middleware như redux-thunk mặc định.

Sau đây là hướng dẫn cơ bản để bạn có thể tạo ra một App sử dụng Redux Toolkit

B1: Cài đặt NodeJs

Truy cập nodejs.org để cài đặt Nodejs

B2: Cài đặt Redux Toolkit và React-Redux packages

Mở command line trong project của bạn và chạy lệnh sau:

npm install @reduxjs/toolkit react-redux

B3: Tạo Redux store

Tạo file tên là store.js (Fact: Bạn có thể đặt tên là gì cũng được, nhưng bạn nên đặt là store để dễ dàng nhận biết).

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {},
})

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch

Đây là một store rỗng và đã có thể export để sử dụng

B4: Thêm store vào <Provider > do React cung cấp :

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { store } from './app/store'
import { Provider } from 'react-redux'

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

Đến đây là project của chúng ta đã sử dụng được store rồi.

B5: Tạo Redux State Slice

Tạo stateSlice với createSlice do Redux Toolkit cung cấp.
Mỗi Slice tối thiểu bao gồm: innitalState, reducers, name. Ngoài ra còn extraReducer,….

import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'

export interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
})

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

B6: Thêm Slice vào store

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch

B7: Sử dụng chúng trong React component của bạn

Ở đây chúng ta xây dựng component đếm, khi kích hoạt (dispatch action ) increment/decrement , thì store sẽ cập nhật state và trả về UI

import React from 'react'
import type { RootState } from '../../app/store'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'

export function Counter() {
  const count = useSelector((state: RootState) => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

Full Counter App Example

Redux Toolkit Query

Okey, giờ chúng ta đã có store và một ứng dụng đếm đơn giản. Tiếp theo, tôi muốn gọi api từ server nhưng lại không muốn viết quá nhiều lần một cách thủ công như này.

async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const json = await response.json();
    console.log(json);
  } catch (error) {
    console.error(error.message);
  }
}

Đơn giản là vì viết thủ công thì phải tự quản lý state.

Như đã giới thiệu, RTK cung cấp cho chúng ta 1 công cụ mạnh mẽ để xử lý vấn đề này.

Dưới đây là các bước để bắt đầu RTKQ

B1: Tạo API service

// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'

// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
  reducerPath: 'pokemonApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
  endpoints: (builder) => ({
    getPokemonByName: builder.query<Pokemon, string>({
      query: (name) => `pokemon/${name}`,
    }),
  }),
})

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi

B2: Thêm Service vào store của bạn

RTKQ Serivde tạo ra một reducer slice nên được bọc trong Redux root reducer và một custom middleware xử lý việc tìm nạp dữ liệu. Cả hai đều cần được thêm vào Redux store.

import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { pokemonApi } from './services/pokemon'

export const store = configureStore({
  reducer: {
    // Add the generated reducer as a specific top-level slice
    [pokemonApi.reducerPath]: pokemonApi.reducer,
  },
  // Adding the api middleware enables caching, invalidation, polling,
  // and other useful features of `rtk-query`.
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(pokemonApi.middleware),
})

// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch)

B3: Bọc <App> của bạn trong <Provider>

import * as React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'

import App from './App'
import { store } from './store'

const rootElement = document.getElementById('root')
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

B4: Sử dụng RTX Query trong component của bạn

useGetPokemonByNameQuery là một trong những hook được RTXQ tự động tạo ra.

import * as React from 'react'
import { useGetPokemonByNameQuery } from './services/pokemon'

export default function App() {
  // Using a query hook automatically fetches data and returns query values
  const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
  // Individual hooks are also accessible under the generated endpoints:
  // const { data, error, isLoading } = pokemonApi.endpoints.getPokemonByName.useQuery('bulbasaur')

  return (
    <div className="App">
      {error ? (
        <>Oh no, there was an error</>
      ) : isLoading ? (
        <>Loading...</>
      ) : data ? (
        <>
          <h3>{data.species.name}</h3>
          <img src={data.sprites.front_shiny} alt={data.species.name} />
        </>
      ) : null}
    </div>
  )
}

Ví dụ nâng cao hơn :

import './styles.css'
import { Pokemon } from './Pokemon'
import { useState } from 'react'

const pokemon = ['bulbasaur', 'pikachu', 'ditto', 'bulbasaur']

export default function App() {
  const [pollingInterval, setPollingInterval] = useState(0)

  return (
    <div className="App">
      <select
        onChange={(change) => setPollingInterval(Number(change.target.value))}
      >
        <option value={0}>Off</option>
        <option value={1000}>1s</option>
        <option value={5000}>5s</option>
      </select>
      <div>
        {pokemon.map((poke, index) => (
          <Pokemon key={index} name={poke} pollingInterval={pollingInterval} />
        ))}
      </div>
    </div>
  )
}

Giờ thì tại sao RTKQ lại tốt hơn hand-write fetching

Viết thủ công

Ưu điểm

  1. Kiểm soát hoàn toàn: Bạn có thể tùy chỉnh mọi thứ theo nhu cầu của bạn mà không bị giới hạn bởi bất kỳ thư viện nào.
  2. Học tập: Tự viết logic giúp bạn hiểu sâu hơn về cách hoạt động của việc lấy và quản lý dữ liệu trong React.

Nhược điểm

  1. Phức tạp: Quản lý state, xử lý các giai đoạn khác nhau của việc gọi API (loading, success, error), và caching thủ công có thể trở nên phức tạp và dễ mắc lỗi.
  2. Tốn thời gian: Viết và duy trì code thủ công mất nhiều thời gian hơn.
  3. Tính nhất quán: Khi ứng dụng lớn lên, việc giữ cho code lấy dữ liệu nhất quán trên toàn bộ ứng dụng có thể trở nên khó khăn.

Redux Toolkit Query

Ưu điểm

  1. Tích hợp với Redux: RTK Query tích hợp sâu với Redux, giúp dễ dàng quản lý state toàn cục và tận dụng các tính năng mạnh mẽ của Redux.
  2. Tự động hóa: RTK Query tự động hóa nhiều nhiệm vụ phổ biến như quản lý cache, refetching, và xử lý các giai đoạn khác nhau của việc gọi API.
  3. Đơn giản và gọn gàng: Code gọn gàng và dễ đọc hơn, với ít boilerplate hơn.
  4. Khả năng mở rộng: RTK Query dễ dàng mở rộng với các endpoint khác nhau và có thể quản lý nhiều API cùng một lúc.
  5. Cộng đồng và hỗ trợ: Là một phần của Redux Toolkit, nó có cộng đồng lớn và tài liệu phong phú.

Nhược điểm

  1. Học tập: Có một chút thời gian học tập ban đầu để hiểu cách RTK Query hoạt động, đặc biệt nếu bạn chưa quen với Redux.
  2. Giới hạn: Bạn có thể bị giới hạn bởi những gì thư viện cung cấp, mặc dù điều này thường không phải là vấn đề lớn vì RTK Query rất linh hoạt.

Cuối cùng thì chúc bạn thành công!

Nguồn dịch

https://redux-toolkit.js.org/tutorials/rtk-query

https://redux-toolkit.js.org/tutorials/quick-start

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *