メインコンテンツへスキップ

createAsyncThunk

概要

Reduxアクションタイプ文字列と、Promiseを返す必要のあるコールバック関数を受け取る関数です。渡されたアクションタイプのプレフィックスに基づいて、Promiseライフサイクルアクションタイプを生成し、Promiseコールバックを実行し、返されたPromiseに基づいてライフサイクルアクションをディスパッチするサンクアクションクリエーターを返します。

これは、非同期リクエストライフサイクルを処理するための標準的な推奨アプローチを抽象化したものです。

どのデータをフェッチしているか、読み込み状態をどのように追跡したいか、返されたデータをどのように処理する必要があるかを知らないため、Reducer関数は生成しません。 これらのアクションを処理する独自のReducerロジックを、アプリケーションに適した読み込み状態と処理ロジックで記述する必要があります。

ヒント

Redux ToolkitのRTK QueryデータフェッチAPIは、Reduxアプリケーション向けの専用データフェッチおよびキャッシュソリューションであり、データフェッチを管理するためのサンクまたはReducerを記述する必要性を排除できます。ぜひお試しいただき、アプリケーションのデータフェッチコードを簡素化できるかどうかをご確認ください。

使用例

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

// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
)

interface UsersState {
entities: User[]
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}

const initialState = {
entities: [],
loading: 'idle',
} satisfies UserState as UsersState

// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})

// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))

パラメータ

createAsyncThunkは、文字列アクションのtype値、payloadCreatorコールバック、およびoptionsオブジェクトの3つのパラメータを受け取ります。

type

非同期リクエストのライフサイクルを表す追加のReduxアクションタイプ定数を生成するために使用される文字列

たとえば、'users/requestStatus'type引数は、次のアクションタイプを生成します

  • pending: 'users/requestStatus/pending'
  • fulfilled: 'users/requestStatus/fulfilled'
  • rejected: 'users/requestStatus/rejected'

payloadCreator

非同期ロジックの結果を含むPromiseを返す必要のあるコールバック関数。 同期的に値を返すこともできます。 エラーが発生した場合、`Error`インスタンスを含むrejected Promise、または記述的なエラーメッセージなどのプレーンな値、あるいは`thunkAPI.rejectWithValue`関数によって返される`RejectWithValue`引数を持つresolved Promiseを返す必要があります。

payloadCreator関数は、適切な結果を計算するために必要なロジックを自由に含めることができます。 これには、標準的なAJAXデータフェッチリクエスト、結果が最終的な値に結合された複数のAJAXコール、React Native AsyncStorageとの相互作用などが含まれます。

payloadCreator関数は、2つの引数で呼び出されます

  • arg: サンクアクションクリエーターがディスパッチされたときに渡された最初のパラメーターを含む単一の値。 リクエストの一部として必要なアイテムIDなどの値を渡すのに役立ちます。 複数の値を渡す必要がある場合は、サンクをディスパッチするときに、`dispatch(fetchUsers({status: 'active', sortBy: 'name'}))`のようにオブジェクトにまとめて渡します。
  • thunkAPI: 通常はReduxサンク関数に渡されるすべてのパラメーターと、追加のオプションを含むオブジェクト
    • dispatch: Reduxストアのdispatchメソッド
    • getState: ReduxストアのgetStateメソッド
    • extra: サンクミドルウェアにセットアップ時に渡された「追加引数」(存在する場合)
    • requestId: このリクエストシーケンスを識別するために自動的に生成された一意の文字列ID値
    • signal: アプリケーションロジックの別の部分がこのリクエストをキャンセルする必要があるとマークしているかどうかを確認するために使用できるAbortController.signalオブジェクト
    • rejectWithValue(value, [meta]): rejectWithValueは、定義されたペイロードとメタを持つrejectedレスポンスを返すために、アクションクリエーターで`return`(または`throw`)できるユーティリティ関数です。 指定した値を渡し、rejectedアクションのペイロードで返します。 `meta`も渡すと、既存の`rejectedAction.meta`とマージされます。
    • fulfillWithValue(value, meta): fulfillWithValueは、`fulfilledAction.meta`に追加できる機能を持ちながら、値で`fulfill`するために、アクションクリエーターで`return`できるユーティリティ関数です。

payloadCreator関数のロジックは、結果を計算するために必要に応じてこれらの値を使用できます。

オプション

次のオプションフィールドを持つオブジェクト

  • condition(arg, { getState, extra } ): boolean | Promise<boolean>: 必要に応じて、ペイロードクリエーターの実行とすべてのアクションのディスパッチをスキップするために使用できるコールバック。 詳細な説明については、実行前のキャンセルを参照してください。
  • dispatchConditionRejection: condition()falseを返した場合、デフォルトの動作ではアクションはまったくディスパッチされません。 サンクがキャンセルされたときに「rejected」アクションをディスパッチしたい場合は、このフラグをtrueに設定します。
  • idGenerator(arg): string: リクエストシーケンスのrequestIdを生成するときに使用する関数。 デフォルトではnanoidを使用しますが、独自のID生成ロジックを実装できます。
  • serializeError(error: unknown) => any 内部`miniSerializeError`メソッドを独自のシリアライズロジックに置き換えます。
  • getPendingMeta({ arg, requestId }, { getState, extra }): any: `pendingAction.meta`フィールドにマージされるオブジェクトを作成する関数。

戻り値

createAsyncThunkは、標準のReduxサンクアクションクリエーターを返します。 サンクアクションクリエーター関数には、`pending`、`fulfilled`、`rejected`の場合のプレーンアクションクリエーターがネストされたフィールドとしてアタッチされます。

上記の`fetchUserById`の例を使用すると、`createAsyncThunk`は4つの関数を生成します

  • fetchUserById、記述した非同期ペイロードコールバックを開始するサンクアクションクリエーター
    • fetchUserById.pending、`'users/fetchByIdStatus/pending'`アクションをディスパッチするアクションクリエーター
    • fetchUserById.fulfilled、`'users/fetchByIdStatus/fulfilled'`アクションをディスパッチするアクションクリエーター
    • fetchUserById.rejected、`'users/fetchByIdStatus/rejected'`アクションをディスパッチするアクションクリエーター

ディスパッチされると、サンクは

  • pendingアクションをディスパッチします
  • payloadCreatorコールバックを呼び出し、返されたPromiseが解決されるのを待ちます
  • Promiseが解決されると
    • Promiseが正常に解決された場合、Promiseの値を`action.payload`として`fulfilled`アクションをディスパッチします
    • Promiseが`rejectWithValue(value)`戻り値で解決された場合、`action.payload`に渡された値と`action.error.message`として'Rejected'を使用して`rejected`アクションをディスパッチします
    • Promiseが失敗し、`rejectWithValue`で処理されなかった場合、エラー値のシリアライズされたバージョンを`action.error`として`rejected`アクションをディスパッチします
  • 最終的にディスパッチされたアクション(`fulfilled`または`rejected`アクションオブジェクト)を含むfulfilled Promiseを返します

Promiseライフサイクルアクション

createAsyncThunkは、createActionを使用して3つのReduxアクションクリエーターを生成します:pendingfulfilled、およびrejected。 各ライフサイクルアクションクリエーターは、返されたサンクアクションクリエーターにアタッチされるため、Reducerロジックはアクションタイプを参照し、ディスパッチされたときにアクションに応答できます。 各アクションオブジェクトには、`action.meta`の下に現在の一意の`requestId`と`arg`値が含まれます。

アクションクリエーターには、次のシグネチャがあります

interface SerializedError {
name?: string
message?: string
code?: string
stack?: string
}

interface PendingAction<ThunkArg> {
type: string
payload: undefined
meta: {
requestId: string
arg: ThunkArg
}
}

interface FulfilledAction<ThunkArg, PromiseResult> {
type: string
payload: PromiseResult
meta: {
requestId: string
arg: ThunkArg
}
}

interface RejectedAction<ThunkArg> {
type: string
payload: undefined
error: SerializedError | any
meta: {
requestId: string
arg: ThunkArg
aborted: boolean
condition: boolean
}
}

interface RejectedWithValueAction<ThunkArg, RejectedValue> {
type: string
payload: RejectedValue
error: { message: 'Rejected' }
meta: {
requestId: string
arg: ThunkArg
aborted: boolean
}
}

type Pending = <ThunkArg>(
requestId: string,
arg: ThunkArg,
) => PendingAction<ThunkArg>

type Fulfilled = <ThunkArg, PromiseResult>(
payload: PromiseResult,
requestId: string,
arg: ThunkArg,
) => FulfilledAction<ThunkArg, PromiseResult>

type Rejected = <ThunkArg>(
requestId: string,
arg: ThunkArg,
) => RejectedAction<ThunkArg>

type RejectedWithValue = <ThunkArg, RejectedValue>(
requestId: string,
arg: ThunkArg,
) => RejectedWithValueAction<ThunkArg, RejectedValue>

Reducerでこれらのアクションを処理するには、 "builder callback"表記法を使用して、`createReducer`または`createSlice`でアクションクリエーターを参照します。

const reducer1 = createReducer(initialState, (builder) => {
builder.addCase(fetchUserById.fulfilled, (state, action) => {})
})

const reducer2 = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUserById.fulfilled, (state, action) => {})
},
})

さらに、fulfilledアクションとrejectedアクションの両方に一致させるための`settled`マッチャーがアタッチされています。 概念的には、これは`finally`ブロックに似ています。

`settled`はアクションクリエーターではなくマッチャーであるため、`addCase`ではなく`addMatcher`を使用してください。

const reducer1 = createReducer(initialState, (builder) => {
builder.addMatcher(fetchUserById.settled, (state, action) => {})
})

const reducer2 = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addMatcher(fetchUserById.settled, (state, action) => {})
},
})

サンク結果の処理

Result アクションのアンラップ

Thunk はディスパッチされると値を返す場合があります。一般的なユースケースは、Thunk から Promise を返し、コンポーネントから Thunk をディスパッチし、Promise が解決するのを待ってから追加の作業を行うことです。

const onClick = () => {
dispatch(fetchUserById(userId)).then(() => {
// do additional work
})
}

createAsyncThunk によって生成された Thunk は、**常に解決された Promise を返します**。Promise 内には、状況に応じて fulfilled アクションオブジェクトまたは rejected アクションオブジェクトが含まれます。

呼び出し元のロジックは、これらのアクションを元の Promise の内容であるかのように扱いたい場合があります。ディスパッチされた Thunk によって返された Promise には unwrap プロパティがあり、これを呼び出すことで、fulfilled アクションの payload を抽出したり、rejected アクションから rejectWithValue によって作成された error または利用可能な場合は payload をスローしたりできます。

// in the component

const onClick = () => {
dispatch(fetchUserById(userId))
.unwrap()
.then((originalPromiseResult) => {
// handle result here
})
.catch((rejectedValueOrSerializedError) => {
// handle error here
})
}

または、async/await 構文を使用します。

// in the component

const onClick = async () => {
try {
const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap()
// handle result here
} catch (rejectedValueOrSerializedError) {
// handle error here
}
}

ほとんどの場合、添付の .unwrap() プロパティを使用することをお勧めしますが、Redux Toolkit は同様の目的で使用できる unwrapResult 関数もエクスポートしています。

import { unwrapResult } from '@reduxjs/toolkit'

// in the component
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then((originalPromiseResult) => {
// handle result here
})
.catch((rejectedValueOrSerializedError) => {
// handle result here
})
}

または、async/await 構文を使用します。

import { unwrapResult } from '@reduxjs/toolkit'

// in the component
const onClick = async () => {
try {
const resultAction = await dispatch(fetchUserById(userId))
const originalPromiseResult = unwrapResult(resultAction)
// handle result here
} catch (rejectedValueOrSerializedError) {
// handle error here
}
}

ディスパッチ後のエラーの確認

これは、**Thunk での失敗したリクエストまたはエラーは、**決して**_拒否された_ Promise を返さない**ことを意味することに注意してください。この時点では、失敗は処理されない例外ではなく、処理されたエラーであると想定しています。これは、dispatch の結果を使用しないユーザーに対して、キャッチされない Promise の拒否を防ぎたいという事実によるものです。

コンポーネントがリクエストが失敗したかどうかを知る必要がある場合は、.unwrap または unwrapResult を使用し、再スローされたエラーを適切に処理します。

Thunk エラーの処理

payloadCreator が拒否された Promise(async 関数でスローされたエラーなど)を返すと、Thunk は action.error としてエラーの自動的にシリアル化されたバージョンを含む rejected アクションをディスパッチします。ただし、シリアル化可能性を確保するために、SerializedError インターフェースと一致しないものはすべて削除されています。

export interface SerializedError {
name?: string
message?: string
stack?: string
code?: string
}

rejected アクションの内容をカスタマイズする必要がある場合は、エラーを自分でキャッチし、thunkAPI.rejectWithValue ユーティリティを使用して新しい値を**返す**必要があります。 return rejectWithValue(errorPayload) を実行すると、rejected アクションはその値を action.payload として使用します。

API レスポンスが「成功」したが、Reducer が知っておくべき何らかの追加のエラー詳細が含まれている場合にも、rejectWithValue アプローチを使用する必要があります。これは、API からフィールドレベルの検証エラーが予想される場合に特に一般的です。

const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
// Use `err.response.data` as `action.payload` for a `rejected` action,
// by explicitly returning it using the `rejectWithValue()` utility
return rejectWithValue(err.response.data)
}
},
)

キャンセル

実行前のキャンセル

ペイロードクリエーターが呼び出される前に Thunk をキャンセルする必要がある場合は、ペイロードクリエーターの後にオプションとして condition コールバックを提供できます。コールバックは Thunk 引数と {getState, extra} をパラメーターとするオブジェクトを受け取り、それらを使用して続行するかどうかを決定します。実行をキャンセルする必要がある場合、condition コールバックはリテラル false 値、または false に解決される Promise を返す必要があります。Promise が返された場合、Thunk は pending アクションをディスパッチする前に Promise が完了するのを待ちます。そうでない場合は、同期的にディスパッチを続行します。

const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
{
condition: (userId, { getState, extra }) => {
const { users } = getState()
const fetchStatus = users.requests[userId]
if (fetchStatus === 'fulfilled' || fetchStatus === 'loading') {
// Already fetched or in progress, don't need to re-fetch
return false
}
},
},
)

condition()false を返した場合、デフォルトの動作ではアクションはまったくディスパッチされません。Thunk がキャンセルされたときに「rejected」アクションをディスパッチする必要がある場合は、{condition, dispatchConditionRejection: true} を渡します。

実行中のキャンセル

実行中の Thunk が完了する前にキャンセルする場合は、dispatch(fetchUserById(userId)) によって返された Promise の abort メソッドを使用できます。

実際の例は次のようになります。

// file: store.ts noEmit
import { configureStore } from '@reduxjs/toolkit'
import type { Reducer } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'

declare const reducer: Reducer<{}>
const store = configureStore({ reducer })
export const useAppDispatch = () => useDispatch<typeof store.dispatch>()

// file: slice.ts noEmit
import { createAsyncThunk } from '@reduxjs/toolkit'
export const fetchUserById = createAsyncThunk(
'fetchUserById',
(userId: string) => {
/* ... */
},
)

// file: MyComponent.ts
import { fetchUserById } from './slice'
import { useAppDispatch } from './store'
import React from 'react'

function MyComponent(props: { userId: string }) {
const dispatch = useAppDispatch()
React.useEffect(() => {
// Dispatching the thunk returns a promise
const promise = dispatch(fetchUserById(props.userId))
return () => {
// `createAsyncThunk` attaches an `abort()` method to the promise
promise.abort()
}
}, [props.userId])
}

Thunk がこの方法でキャンセルされた後、error プロパティに AbortError を持つ "thunkName/rejected" アクションがディスパッチ(および返却)されます。Thunk はそれ以上のアクションをディスパッチしません。

さらに、payloadCreatorthunkAPI.signal 経由で渡される AbortSignal を使用して、コストのかかる非同期アクションを実際にキャンセルできます。

最新のブラウザの fetch api には、すでに AbortSignal のサポートが組み込まれています。

import { createAsyncThunk } from '@reduxjs/toolkit'

const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId: string, thunkAPI) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`, {
signal: thunkAPI.signal,
})
return await response.json()
},
)

キャンセルの状態の確認

シグナル値の読み取り

signal.aborted プロパティを使用して、Thunk が中止されたかどうかを定期的に確認し、その場合はコストのかかる長時間実行の作業を停止できます。

import { createAsyncThunk } from '@reduxjs/toolkit'

const readStream = createAsyncThunk(
'readStream',
async (stream: ReadableStream, { signal }) => {
const reader = stream.getReader()

let done = false
let result = ''

while (!done) {
if (signal.aborted) {
throw new Error('stop the work, this has been aborted!')
}
const read = await reader.read()
result += read.value
done = read.done
}
return result
},
)

中止イベントのリッスン

また、signal.addEventListener('abort', callback) を呼び出して、promise.abort() が呼び出されたときに Thunk 内のロジックに通知させることもできます。これは、たとえば axios CancelToken と組み合わせて使用できます。

import { createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'

const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId: string, { signal }) => {
const source = axios.CancelToken.source()
signal.addEventListener('abort', () => {
source.cancel()
})
const response = await axios.get(`https://reqres.in/api/users/${userId}`, {
cancelToken: source.token,
})
return response.data
},
)

Promise の拒否がエラーまたはキャンセルによるものだったかどうかの確認

Thunk キャンセルに関する動作を調査するには、ディスパッチされたアクションの meta オブジェクトのさまざまなプロパティを調べることができます。Thunk がキャンセルされた場合、Promise の結果は rejected アクションになります(そのアクションが実際にストアにディスパッチされたかどうかに関係なく)。

  • 実行前にキャンセルされた場合、meta.condition は true になります。
  • 実行中に中止された場合、meta.aborted は true になります。
  • これらのどちらも true でない場合、Thunk はキャンセルされておらず、Promise の拒否または rejectWithValue によって単に拒否されました。
  • Thunk が拒否されなかった場合、meta.abortedmeta.condition はどちらも undefined になります。

そのため、Thunk が実行前にキャンセルされたことをテストする場合、次のようにすることができます。

import { createAsyncThunk } from '@reduxjs/toolkit'

test('this thunk should always be skipped', async () => {
const thunk = createAsyncThunk(
'users/fetchById',
async () => throw new Error('This promise should never be entered'),
{
condition: () => false,
}
)
const result = await thunk()(dispatch, getState, null)

expect(result.meta.condition).toBe(true)
expect(result.meta.aborted).toBe(false)
})

  • 読み込み状態を伴い、一度に1つのリクエストのみで、IDでユーザーをリクエストする
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI, User } from './userAPI'

const fetchUserById = createAsyncThunk<
User,
string,
{
state: { users: { loading: string; currentRequestId: string } }
}
>('users/fetchByIdStatus', async (userId: string, { getState, requestId }) => {
const { currentRequestId, loading } = getState().users
if (loading !== 'pending' || requestId !== currentRequestId) {
return
}
const response = await userAPI.fetchById(userId)
return response.data
})

const usersSlice = createSlice({
name: 'users',
initialState: {
entities: [],
loading: 'idle',
currentRequestId: undefined,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state, action) => {
if (state.loading === 'idle') {
state.loading = 'pending'
state.currentRequestId = action.meta.requestId
}
})
.addCase(fetchUserById.fulfilled, (state, action) => {
const { requestId } = action.meta
if (
state.loading === 'pending' &&
state.currentRequestId === requestId
) {
state.loading = 'idle'
state.entities.push(action.payload)
state.currentRequestId = undefined
}
})
.addCase(fetchUserById.rejected, (state, action) => {
const { requestId } = action.meta
if (
state.loading === 'pending' &&
state.currentRequestId === requestId
) {
state.loading = 'idle'
state.error = action.error
state.currentRequestId = undefined
}
})
},
})

const UsersComponent = () => {
const { entities, loading, error } = useSelector((state) => state.users)
const dispatch = useDispatch()

const fetchOneUser = async (userId) => {
try {
const user = await dispatch(fetchUserById(userId)).unwrap()
showToast('success', `Fetched ${user.name}`)
} catch (err) {
showToast('error', `Fetch failed: ${err.message}`)
}
}

// render UI here
}
  • rejectWithValue を使用して、コンポーネントでカスタムの拒否されたペイロードにアクセスする

    注:これは、userAPI が検証固有のエラーのみをスローすると仮定した、人為的な例です。

// file: store.ts noEmit
import { configureStore } from '@reduxjs/toolkit'
import type { Reducer } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'
import usersReducer from './user/slice'

const store = configureStore({ reducer: { users: usersReducer } })
export const useAppDispatch = () => useDispatch<typeof store.dispatch>()
export type RootState = ReturnType<typeof store.getState>

// file: user/userAPI.ts noEmit

export declare const userAPI: {
updateById<Response>(id: string, fields: {}): { data: Response }
}

// file: user/slice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
import type { AxiosError } from 'axios'

// Sample types that will be used
export interface User {
id: string
first_name: string
last_name: string
email: string
}

interface ValidationErrors {
errorMessage: string
field_errors: Record<string, string>
}

interface UpdateUserResponse {
user: User
success: boolean
}

export const updateUser = createAsyncThunk<
User,
{ id: string } & Partial<User>,
{
rejectValue: ValidationErrors
}
>('users/update', async (userData, { rejectWithValue }) => {
try {
const { id, ...fields } = userData
const response = await userAPI.updateById<UpdateUserResponse>(id, fields)
return response.data.user
} catch (err) {
let error: AxiosError<ValidationErrors> = err // cast the error for access
if (!error.response) {
throw err
}
// We got validation errors, let's return those so we can reference in our component and set form errors
return rejectWithValue(error.response.data)
}
})

interface UsersState {
error: string | null | undefined
entities: Record<string, User>
}

const initialState = {
entities: {},
error: null,
} satisfies UsersState as UsersState

const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
// The `builder` callback form is used here because it provides correctly typed reducers from the action creators
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
state.entities[payload.id] = payload
})
builder.addCase(updateUser.rejected, (state, action) => {
if (action.payload) {
// Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, the payload will be available here.
state.error = action.payload.errorMessage
} else {
state.error = action.error.message
}
})
},
})

export default usersSlice.reducer

// file: externalModules.d.ts noEmit

declare module 'some-toast-library' {
export function showToast(type: string, message: string)
}

// file: user/UsersComponent.ts

import React from 'react'
import { useAppDispatch } from '../store'
import type { RootState } from '../store'
import { useSelector } from 'react-redux'
import { updateUser } from './slice'
import type { User } from './slice'
import type { FormikHelpers } from 'formik'
import { showToast } from 'some-toast-library'

interface FormValues extends Omit<User, 'id'> {}

const UsersComponent = (props: { id: string }) => {
const { entities, error } = useSelector((state: RootState) => state.users)
const dispatch = useAppDispatch()

// This is an example of an onSubmit handler using Formik meant to demonstrate accessing the payload of the rejected action
const handleUpdateUser = async (
values: FormValues,
formikHelpers: FormikHelpers<FormValues>,
) => {
const resultAction = await dispatch(updateUser({ id: props.id, ...values }))
if (updateUser.fulfilled.match(resultAction)) {
// user will have a type signature of User as we passed that as the Returned parameter in createAsyncThunk
const user = resultAction.payload
showToast('success', `Updated ${user.first_name} ${user.last_name}`)
} else {
if (resultAction.payload) {
// Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, those types will be available here.
formikHelpers.setErrors(resultAction.payload.field_errors)
} else {
showToast('error', `Update failed: ${resultAction.error}`)
}
}
}

// render UI here
}