Thuật ngữ “thunk” ở trong lập trình có nghĩa là đoạn code gây ra một số tác vụ trì hoãn.

Trong Redux, thunk là một hàm cho phép bổ sung các logic tách biệt với UI layer. Các logic này có thể là các side effect (chẳng hạn như fetch data), dispatch nhiều action hoặc truy cập đến state của store.

Để sử dụng thunk, ta cần thêm “redux-thunk” middleware vào store. Nếu sử dụng Redux Toolkit thì không cần.

Writing Thunks

Một thunk function sẽ nhận vào hai đối số là phương thức dispatchgetState.

Ví dụ:

const thunkFunction = (dispatch, getState) => {
	// logic here that can dispatch actions or read state
}

Logic ở trong thunk có thể là đồng bộ hoặc bất đồng bộ và ta có thể gọi sử dụng dispatch hoặc getState bất cứ lúc nào.

Chúng ta cũng có thể dùng một hàm để tạo ra thunk. Ta gọi hàm đó là thunk action creator, ví dụ:

// fetchTodoById is the "thunk action creator"
export function fetchTodoById(todoId) {
    // fetchTodoByIdThunk is the "thunk function"
    return async function fetchTodoByIdThunk(dispatch, getState) {
        const response = await client.get(`/fakeApi/todo/${todoId}`)
        dispatch(todosLoaded(response.todos))
    }
}

Thunk action creator và thunk cũng có thể là arrow function:

export const fetchTodoById = (todoId) => async (dispatch) => {
    const response = await client.get(`/fakeApi/todo/${todoId}`)
    dispatch(todosLoaded(response.todos))
}

Thunk sẽ được dispatch thông qua thunk action creator, tương tự như khi ta dispatch action bằng cách truyền vào action creator:

function TodoComponent({ todoId }) {
    const dispatch = useDispatch()
 
    const onFetchClicked = () => {
        dispatch(fetchTodoById(todoId))
    }
}

Minh họa quá trình gọi API khi sử dụng thunk:

Tip

Thunk action creator và thunk thường được viết ở trong file chứa slice.

Thunk Usage Patterns

Dispatching Actions

Chúng ta có thể dùng thunk để dispatch nhiều action cùng một lúc hoặc thậm chí là dispatch các thunk khác.

Ví dụ bên dưới dispatch một action và một thunk:

function complexSynchronousThunk(someValue) {
    return (dispatch, getState) => {
        dispatch(someBasicActionCreator(someValue))
        dispatch(someThunkActionCreator())
    }
}

Accessing State

Không giống như các component, thunk có quyền truy cập đến phương thức getState và có thể gọi sử dụng phương thức này bất cứ lúc nào. Tính năng này hữu ích khi ta cần xử lý một số logic dựa trên state hiện tại, ví dụ:

const MAX_TODOS = 5
 
function addTodosIfAllowed(todoText) {
    return (dispatch, getState) => {
        const state = getState()
 
        // Could also check `state.todos.length < MAX_TODOS`
        if (selectCanAddNewTodo(state, MAX_TODOS)) {
            dispatch(todoAdded(todoText))
        }
    }
}

Do state được cập nhật ngay lập tức nên ta có thể gọi getState sau khi dispatch để lấy ra state mới:

function checkStateAfterDispatch() {
    return (dispatch, getState) => {
        const firstState = getState()
        dispatch(firstAction())
 
        const secondState = getState()
 
        if (secondState.someField != firstState.someField) {
            dispatch(secondAction())
        }
    }
}

Trong trường hợp ta cần lấy ra state của nhiều slice khác nhau, ta cũng có thể sử dụng thunk:

function crossSliceActionThunk() {
    return (dispatch, getState) => {
        const state = getState()
        // Read both slices out of state
        const { a, b } = state
 
        // Include data from both slices in the action
        dispatch(actionThatNeedsMoreData(a, b))
    }
}

Async Logic and Side Effects

Khi thực hiện gửi request ở trong thunk, ta cần dispatch các action trước và sau khi request được xử lý nhằm kiểm soát loading state của request đó.

Thường thì ta sẽ dispatch “pending” action trước khi gửi request. Nếu request thành công thì ta sẽ dispatch “fulfilled” action. Còn nếu request thất bại thì ta sẽ dispatch “rejected” action.

Ví dụ:

function fetchData(someValue) {
    return (dispatch, getState) => {
        dispatch(requestStarted())
 
        myAjaxLib.post("/someEndpoint", { data: someValue }).then(
            (response) => dispatch(requestSucceeded(response.data)),
            (error) => dispatch(requestFailed(error.message))
        )
    }
}

Using createAsyncThunk

Thay vì tạo ra 3 action cho 3 trạng thái của request một cách thủ công và dispatch chúng ở trong thunk, ta có thể sử dụng phương thức createAsyncThunk của Redux Toolkit để tự động sinh ra các action cũng như là lược bỏ các dispatch ở trong thunk function.

import { createAsyncThunk } from "@reduxjs/toolkit"

Phương thức này nhận vào hai đối số:

  • Một action type string dùng để tạo các action type tương ứng với 3 trạng thái của request (pending, fulfilledrejected)
  • Một “payload creation callback” chứa các đoạn code bất đồng bộ và trả về một Promise.

Ví dụ:

export const fetchSneakers = createAsyncThunk("sneakers/fetchSneakers", async () => {
    const response = await axios.get(URL)
    return response.data
})

Giá trị trả về của createAsyncThunk là một async thunk.

Payload creation callback cũng nhận vào hai đối số:

  • arg: là đối số truyền vào async thunk khi dispatch.
  • thunkAPI: là một đối tượng gồm nhiều thuộc tính. Trong số đó có phương thức rejectWithValue giúp trả về một rejected response.

Ví dụ:

export const fetchSneakerById = createAsyncThunk("sneakers/fetchSneakerById", async (sneakerId, thunkAPI) => {
	try {
	    const response = await axios.get(`URL/${sneakerId}`)
	    return response.data
	} catch(error) {
		return thunkAPI.rejectWithValue("Something went wrong: ", error.message)
	}
})

Seealso

createAsyncThunk

Thêm vào các reducer cho từng trạng thái của request khi tạo slice như sau:

const sneakersSlice = createSlice({
    name: "sneakers",
    initialState: {
        sneakers: [],
        isLoading: "false",
    },
    reducers: {
        // omit reducer cases
    },
    extraReducers: {
        [fetchSneakers.pending]: (state) => {
            state.isLoading = "true"
        },
        [fetchSneakers.fulfilled]: (state, action) => {
            state.sneakers = action.payload
            state.isLoading = "false"
        },
        [fetchSneakers.rejected]: (state, action) => {
	        console.error(action.payload)
            state.isLoading = "false"
        }
    }
})

Lưu ý là ta cần phải export slice reducer để thêm vào store.

export default sneakersSlice.reducer

Dispatch async thunk ở trong component như sau:

<button onClick={() => dispatch(fetchSneakers())}>
	Fetch sneakers
</button>

Cũng có thể dispatch async thunk thông qua store:

import { getCartItems } from "../features/cart/cartSlice"
 
export const store = configureStore({
	// ...
})
 
store.dispatch(getCartItems())
list
from [[Writing Logic with Thunks]]
sort file.ctime asc

Resources