#React: การจัดการ State ด้วย Redux Toolkit

การพัฒนา React Application เมื่อระบบมีขนาดใหญ่ขึ้น มักพบว่า State ไม่ได้ถูกใช้งานอยู่เพียง Component เดียว แต่ต้องแชร์ข้อมูลระหว่างหลาย Component หรือหลายหน้า เช่น ข้อมูลผู้ใช้ที่ Login, Shopping Cart, Theme, Filters หรือข้อมูลจาก API

หนึ่งในเครื่องมือยอดนิยมสำหรับจัดการ Global State คือ Redux Toolkit (RTK) ซึ่งเป็นแนวทางมาตรฐานที่ Redux แนะนำสำหรับการเขียน Redux ในปัจจุบัน ช่วยลด Boilerplate Code และทำให้การสร้าง Store, Reducer และ Action ง่ายกว่า Redux แบบเดิมอย่างมาก

บทความนี้จะทดลองสร้าง React Application ที่จัดการ State ด้วย Redux Toolkit ตั้งแต่พื้นฐานจนถึงการเรียก API แบบ Asynchronous


#1. State ใน React คืออะไร

State คือข้อมูลที่สามารถเปลี่ยนแปลงได้ระหว่างการทำงานของ Application และเมื่อ State เปลี่ยน React จะอัปเดต UI ที่เกี่ยวข้อง

ตัวอย่าง Local State:

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

useState เหมาะกับข้อมูลที่ใช้เฉพาะภายใน Component หรือ Component Tree ขนาดเล็ก

แต่เมื่อข้อมูลต้องถูกใช้งานหลายส่วน เช่น

  • Authentication
  • User Profile
  • Shopping Cart
  • Application Settings
  • Shared Filters
  • Workflow State

การใช้ Global State Management จะช่วยให้โครงสร้างระบบชัดเจนขึ้น


#2. Redux Toolkit คืออะไร

Redux Toolkit เป็น Official Toolset สำหรับพัฒนา Redux Application โดยมี API สำคัญ เช่น

  • configureStore() สำหรับสร้าง Redux Store
  • createSlice() สำหรับสร้าง State, Reducer และ Action
  • createAsyncThunk() สำหรับจัดการงาน Asynchronous
  • RTK Query สำหรับ Data Fetching และ Caching

เว็บไซต์อย่างเป็นทางการ:

https://redux-toolkit.js.org/

React Redux:

https://react-redux.js.org/


#3. Redux Toolkit Data Flow

แนวคิดพื้นฐานของ Redux เป็น One-way Data Flow

React Component
      |
      | dispatch(action)
      v
    Action
      |
      v
    Reducer
      |
      v
 Redux Store
      |
      | useSelector()
      v
React Component

Component จะไม่แก้ Global State โดยตรง แต่ส่ง Action ผ่าน dispatch() จากนั้น Reducer จะประมวลผลและปรับ State ใน Store


#4. สร้าง React Project ด้วย Vite

สร้าง Project:

npm create vite@latest redux-demo -- --template react

เข้าไปใน Project:

cd redux-demo

ติดตั้ง Dependency:

npm install

ทดลองรัน:

npm run dev

#5. ติดตั้ง Redux Toolkit

ติดตั้ง Package:

npm install @reduxjs/toolkit react-redux

Package มีหน้าที่ต่างกันดังนี้


Package หน้าที่


@reduxjs/toolkit สร้าง Store, Slice, Reducer, Async Logic และ RTK Query

#react-redux เชื่อม React Component เข้ากับ Redux Store


#6. โครงสร้าง Project

ตัวอย่างโครงสร้างแบบ Feature-based:

src/
├── app/
│   └── store.js
├── features/
│   └── counter/
│       └── counterSlice.js
├── App.jsx
└── main.jsx

เมื่อ Application ใหญ่ขึ้นสามารถเพิ่ม Feature ได้ เช่น

features/
├── auth/
├── cart/
├── products/
└── counter/

#7. สร้าง Slice ด้วย createSlice

สร้างไฟล์:

src/features/counter/counterSlice.js

เพิ่ม Code:

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  value: 0
}

const counterSlice = createSlice({
  name: 'counter',

  initialState,

  reducers: {
    increment: (state) => {
      state.value += 1
    },

    decrement: (state) => {
      state.value -= 1
    },

    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

export const {
  increment,
  decrement,
  incrementByAmount
} = counterSlice.actions

export default counterSlice.reducer

createSlice() จะสร้างทั้ง Reducer และ Action Creator ให้อัตโนมัติ


#8. ทำไม Redux Toolkit แก้ State แบบนี้ได้

ใน Redux แบบดั้งเดิมไม่ควรแก้ State โดยตรง

แต่ Redux Toolkit สามารถเขียน:

state.value += 1

ได้ เพราะ Redux Toolkit ใช้ Immer ภายใน ทำให้ Code ที่ดูเหมือน Mutation ถูกแปลงเป็น Immutable Update อย่างถูกต้อง

ดังนั้นจึงไม่จำเป็นต้องเขียนแบบนี้ใน Reducer ของ createSlice():

return {
  ...state,
  value: state.value + 1
}

#9. สร้าง Redux Store

สร้างไฟล์:

src/app/store.js

เพิ่ม:

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

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

State จะมีโครงสร้าง:

{
  counter: {
    value: 0
  }
}

#10. เชื่อม Redux Store กับ React

แก้ไข src/main.jsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'

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

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

Provider ทำหน้าที่ส่ง Redux Store ให้ Component ภายใน Application สามารถใช้งานได้


#11. อ่าน State ด้วย useSelector

React Redux มี Hook ชื่อ useSelector() สำหรับอ่านข้อมูลจาก Store

import { useSelector } from 'react-redux'

function Counter() {
  const count = useSelector(
    (state) => state.counter.value
  )

  return <h1>{count}</h1>
}

Flow:

Redux Store
     |
     v
state.counter.value
     |
     v
 useSelector()
     |
     v
 Component

เมื่อค่าที่ Component เลือกเปลี่ยน React Redux จะทำให้ Component ที่เกี่ยวข้อง Render ใหม่


#12. เปลี่ยน State ด้วย useDispatch

ใช้ useDispatch() สำหรับส่ง Action ไปยัง Redux Store

แก้ไข App.jsx

import { useDispatch, useSelector } from 'react-redux'

import {
  increment,
  decrement,
  incrementByAmount
} from './features/counter/counterSlice'

function App() {
  const count = useSelector(
    (state) => state.counter.value
  )

  const dispatch = useDispatch()

  return (
    <div>
      <h1>Redux Toolkit Counter</h1>

      <h2>Count: {count}</h2>

      <button onClick={() => dispatch(increment())}>
        +1
      </button>

      <button onClick={() => dispatch(decrement())}>
        -1
      </button>

      <button
        onClick={() => dispatch(incrementByAmount(10))}
      >
        +10
      </button>
    </div>
  )
}

export default App

#13. ทำความเข้าใจ dispatch

เมื่อเขียน:

dispatch(increment())

Redux Toolkit จะสร้าง Action ประมาณ:

{
  type: 'counter/increment'
}

ถ้าเขียน:

dispatch(incrementByAmount(10))

Action จะมี Payload:

{
  type: 'counter/incrementByAmount',
  payload: 10
}

จากนั้น Reducer ที่เกี่ยวข้องจะนำ Payload ไปปรับ State


#14. Redux Flow แบบเต็ม

ตัวอย่างเมื่อผู้ใช้กดปุ่ม +10

User
 |
 v
Button
 |
 v
dispatch(incrementByAmount(10))
 |
 v
Action
 |
 | type: counter/incrementByAmount
 | payload: 10
 |
 v
Reducer
 |
 v
state.value += 10
 |
 v
Redux Store
 |
 v
useSelector()
 |
 v
React UI

นี่คือแนวคิดหลักที่ควรเข้าใจเมื่อเริ่มใช้งาน Redux


#15. การจัดการหลาย Slice

Application จริงมักมี State หลาย Domain

ตัวอย่าง:

src/features/
├── auth/
│   └── authSlice.js
├── cart/
│   └── cartSlice.js
├── products/
│   └── productSlice.js
└── counter/
    └── counterSlice.js

สามารถนำ Reducer ทั้งหมดมาใส่ใน Store:

import { configureStore } from '@reduxjs/toolkit'

import authReducer from '../features/auth/authSlice'
import cartReducer from '../features/cart/cartSlice'
import productReducer from '../features/products/productSlice'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    auth: authReducer,
    cart: cartReducer,
    products: productReducer,
    counter: counterReducer
  }
})

Global State จะมีลักษณะ:

Redux Store
|
+-- auth
|   +-- user
|   +-- token
|
+-- cart
|   +-- items
|
+-- products
|   +-- data
|
+-- counter
    +-- value

#16. จัดการ API ด้วย createAsyncThunk

Redux Toolkit มี createAsyncThunk() สำหรับจัดการ Asynchronous Logic เช่น REST API

ตัวอย่าง:

import {
  createAsyncThunk,
  createSlice
} from '@reduxjs/toolkit'

export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async () => {
    const response = await fetch(
      'https://jsonplaceholder.typicode.com/users'
    )

    if (!response.ok) {
      throw new Error('Unable to fetch users')
    }

    return response.json()
  }
)

createAsyncThunk() จะสร้าง Action Lifecycle ให้อัตโนมัติ ได้แก่

pending
fulfilled
rejected

#17. จัดการ Async State ด้วย extraReducers

ตัวอย่าง User Slice:

const initialState = {
  users: [],
  loading: false,
  error: null
}

const userSlice = createSlice({
  name: 'users',

  initialState,

  reducers: {},

  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true
        state.error = null
      })

      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false
        state.users = action.payload
      })

      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false
        state.error = action.error.message
      })
  }
})

export default userSlice.reducer

#18. เรียก API จาก Component

import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'

import { fetchUsers } from './features/users/userSlice'

function UserList() {
  const dispatch = useDispatch()

  const {
    users,
    loading,
    error
  } = useSelector((state) => state.users)

  useEffect(() => {
    dispatch(fetchUsers())
  }, [dispatch])

  if (loading) {
    return <p>Loading...</p>
  }

  if (error) {
    return <p>Error: {error}</p>
  }

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name}
        </li>
      ))}
    </ul>
  )
}

export default UserList

#19. RTK Query

ถ้า Application มีการเรียก API จำนวนมาก Redux Toolkit มี RTK Query ซึ่งออกแบบมาสำหรับ Server State โดยเฉพาะ

RTK Query ช่วยจัดการ:

  • Data Fetching
  • Caching
  • Loading State
  • Error State
  • Cache Invalidation
  • Refetching
  • Mutation

ตัวอย่าง API Definition:

import {
  createApi,
  fetchBaseQuery
} from '@reduxjs/toolkit/query/react'

export const userApi = createApi({
  reducerPath: 'userApi',

  baseQuery: fetchBaseQuery({
    baseUrl: 'https://jsonplaceholder.typicode.com/'
  }),

  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => 'users'
    })
  })
})

export const {
  useGetUsersQuery
} = userApi

Component สามารถเรียกใช้ Hook ที่ RTK Query สร้างให้:

function UserList() {
  const {
    data: users,
    isLoading,
    error
  } = useGetUsersQuery()

  if (isLoading) {
    return <p>Loading...</p>
  }

  if (error) {
    return <p>Error</p>
  }

  return (
    <ul>
      {users?.map((user) => (
        <li key={user.id}>
          {user.name}
        </li>
      ))}
    </ul>
  )
}

จึงไม่จำเป็นต้องสร้าง loading, error และ Data Fetching Logic เองในทุก Slice


#20. useState หรือ Redux Toolkit?

ไม่ควรนำ State ทุกตัวเข้า Redux Store

สถานการณ์ เครื่องมือที่เหมาะสม


Input ภายใน Component useState Modal เปิด/ปิดเฉพาะหน้า useState State แชร์หลาย Component Redux Toolkit Authentication State Redux Toolkit Shopping Cart Redux Toolkit Complex Client Workflow Redux Toolkit Server Data/API Cache RTK Query

หลักคิดสำคัญคือใช้ Redux เมื่อ State มีความเป็น Shared/Global จริง ๆ ไม่ใช่เพียงเพราะต้องการรวม State ทุกอย่างไว้ที่เดียว


#21. Best Practices

#แยก State ตาม Feature

ควรใช้:

features/
├── auth/
├── cart/
└── products/

แทนการรวม Reducer ทั้งหมดไว้ในไฟล์เดียว

#เก็บ State เท่าที่จำเป็น

หลีกเลี่ยงการเก็บค่าที่สามารถคำนวณจาก State เดิมได้ หากคำนวณผ่าน Selector ได้

#ไม่ใส่ Non-serializable Data โดยไม่จำเป็น

Redux State ควรเน้นข้อมูลที่ Serialize ได้ เช่น Object, Array, String, Number และ Boolean

#ใช้ RTK Query สำหรับ Server State

หากข้อมูลหลักมาจาก REST API และต้องการ Cache/Refetch/Invalidation ควรพิจารณา RTK Query ก่อนเขียน createAsyncThunk() จำนวนมาก

#ใช้ Redux DevTools

configureStore() รองรับ Redux DevTools ใน Development ทำให้สามารถตรวจสอบ Action และ State Transition ได้สะดวก


#22. สรุปคำสั่งและ API สำคัญ

API หน้าที่


configureStore() สร้าง Redux Store createSlice() สร้าง Slice, Reducer และ Action Provider เชื่อม Store กับ React useSelector() อ่าน State useDispatch() Dispatch Action createAsyncThunk() จัดการ Async Logic createApi() สร้าง RTK Query API


#23. Roadmap สำหรับเรียน Redux Toolkit

แนะนำให้เรียนตามลำดับ:

React State
    |
    v
useState
    |
    v
Redux Concept
    |
    v
configureStore
    |
    v
Provider
    |
    v
createSlice
    |
    v
useSelector
    |
    v
useDispatch
    |
    v
createAsyncThunk
    |
    v
RTK Query

หากเข้าใจ Store -> Action -> Reducer -> State -> UI จะสามารถต่อยอดไปยัง Redux Toolkit ในระบบ React ขนาดใหญ่ได้ง่ายขึ้น


#สรุป

Redux Toolkit ทำให้การใช้งาน Redux ใน React ง่ายและเป็นระบบกว่า Redux แบบดั้งเดิม โดยหัวใจสำคัญคือ configureStore(), createSlice(), useSelector() และ useDispatch()

สำหรับ Client State ที่ต้องแชร์ระหว่างหลายส่วนของ Application Redux Toolkit เป็นตัวเลือกที่เหมาะสม ส่วนข้อมูลจาก Server หรือ REST API ที่ต้องมี Caching, Refetching และ Cache Invalidation ควรพิจารณา RTK Query

แนวทางสำคัญคือ ไม่จำเป็นต้องใช้ Redux กับ State ทุกตัว แต่เลือกใช้ Redux Toolkit เมื่อ State มีขอบเขตการใช้งานร่วมกันหรือมีความซับซ้อนเพียงพอ


#References