TypeScript での使用
- TypeScript で各 Redux Toolkit API を使用する方法の詳細
はじめに
Redux Toolkit は TypeScript で記述されており、その API は TypeScript アプリケーションとの優れた統合を可能にするように設計されています。
このページでは、Redux Toolkit に含まれるさまざまな API のそれぞれと、TypeScript で正しく型付けする方法について詳しく説明します。
Redux Toolkit と React Redux をセットアップして TypeScript で動作させる方法の簡単な概要については、TypeScript クイックスタートチュートリアルページを参照してください.
このページで説明されていない型に関する問題が発生した場合は、議論のためにissue をオープンしてください。
configureStore
configureStore
の基本的な使い方は、TypeScript クイックスタートチュートリアルページに示されています。ここでは、役立つ可能性のある追加の詳細を示します。
State
型の取得
State
型を取得する最も簡単な方法は、ルートリデューサーを事前に定義し、その ReturnType
を抽出することです。型名 State
は通常使い古されているため、混乱を防ぐために、RootState
のように異なる名前を付けることをお勧めします。
import { combineReducers } from '@reduxjs/toolkit'
const rootReducer = combineReducers({})
export type RootState = ReturnType<typeof rootReducer>
または、自分で rootReducer
を作成するのではなく、スライスリデューサーを直接 configureStore()
に渡すことを選択した場合は、ルートリデューサーを正しく推論するために、型をわずかに変更する必要があります
import { configureStore } from '@reduxjs/toolkit'
// ...
const store = configureStore({
reducer: {
one: oneSlice.reducer,
two: twoSlice.reducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export default store
リデューサーを直接 configureStore()
に渡し、ルートリデューサーを明示的に定義しない場合、rootReducer
への参照はありません。代わりに、State
型を取得するために store.getState
を参照できます。
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './rootReducer'
const store = configureStore({
reducer: rootReducer,
})
export type RootState = ReturnType<typeof store.getState>
Dispatch
型の取得
ストアから Dispatch
型を取得する場合は、ストアを作成した後に抽出できます。型名 Dispatch
は通常使い古されているため、混乱を防ぐために、AppDispatch
のように異なる名前を付けることをお勧めします。また、以下に示す useAppDispatch
のようなフックをエクスポートし、useDispatch
を呼び出す場所で使用すると、より便利になる場合があります。
import { configureStore } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'
import rootReducer from './rootReducer'
const store = configureStore({
reducer: rootReducer,
})
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = useDispatch.withTypes<AppDispatch>() // Export a hook that can be reused to resolve types
export default store
Dispatch
型の正しい型付け
dispatch
関数の型は、middleware
オプションから直接推論されます。したがって、正しく型付けされたミドルウェアを追加すると、dispatch
はすでに正しく型付けされているはずです。
TypeScript は、スプレッド演算子を使用して配列を結合するときに配列型を拡大することが多いため、getDefaultMiddleware()
によって返される Tuple
の .concat(...)
メソッドと .prepend(...)
メソッドを使用することをお勧めします。
import { configureStore } from '@reduxjs/toolkit'
import additionalMiddleware from 'additional-middleware'
import logger from 'redux-logger'
// @ts-ignore
import untypedMiddleware from 'untyped-middleware'
import rootReducer from './rootReducer'
export type RootState = ReturnType<typeof rootReducer>
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.prepend(
// correctly typed middlewares can just be used
additionalMiddleware,
// you can also type middlewares manually
untypedMiddleware as Middleware<
(action: Action<'specialAction'>) => number,
RootState
>,
)
// prepend and concat calls can be chained
.concat(logger),
})
export type AppDispatch = typeof store.dispatch
export default store
getDefaultMiddleware
を使用しない Tuple
の使用
getDefaultMiddleware
の使用を完全にスキップする場合は、middleware
配列を型安全に作成するために Tuple
を使用する必要があります。このクラスはデフォルトの JavaScript Array
型を拡張したものですが、.concat(...)
の型と追加の .prepend(...)
メソッドの型が変更されています。
例
import { configureStore, Tuple } from '@reduxjs/toolkit'
configureStore({
reducer: rootReducer,
middleware: () => new Tuple(additionalMiddleware, logger),
})
抽出した Dispatch
型を React Redux で使用する
デフォルトでは、React Redux の useDispatch
フックには、ミドルウェアを考慮に入れる型は含まれていません。ディスパッチ時に dispatch
関数のより具体的な型が必要な場合は、返される dispatch
関数の型を指定するか、カスタム型の useSelector
バージョンを作成できます。詳細については、React Redux のドキュメントを参照してください。
createAction
ほとんどの場合、action.type
をリテラルで定義する必要はないため、以下を使用できます
createAction<number>('test')
これにより、作成されたアクションは PayloadActionCreator<number, string>
型になります。
一部のセットアップでは、action.type
のリテラル型が必要になります。残念ながら、TypeScript の型定義では、手動で定義された型パラメーターと推論された型パラメーターを混在させることはできないため、ジェネリック定義と実際の JavaScript コードの両方で type
を指定する必要があります
createAction<number, 'test'>('test')
重複なしでこれを記述する別の方法を探している場合は、準備コールバックを使用すると、両方の型パラメーターを引数から推論できるため、アクション型を指定する必要がなくなります。
function withPayloadType<T>() {
return (t: T) => ({ payload: t })
}
createAction('test', withPayloadType<string>())
リテラル型付き action.type
の代わりに
たとえば、case
ステートメントでペイロードを正しく型付けするために、判別ユニオンの判別子として action.type
を使用している場合は、この代替案に興味があるかもしれません
作成されたアクションクリエイターには、型述語として機能する match
メソッドがあります
const increment = createAction<number>('increment')
function test(action: Action) {
if (increment.match(action)) {
// action.payload inferred correctly here
action.payload
}
}
この match
メソッドは、redux-observable
や RxJS の filter
メソッドと組み合わせて使用する場合にも非常に便利です。
createReducer
型安全なリデューサー引数オブジェクトの構築
createReducer
の 2 番目のパラメーターは、ActionReducerMapBuilder
インスタンスを受け取るコールバックです
const increment = createAction<number, 'increment'>('increment')
const decrement = createAction<number, 'decrement'>('decrement')
createReducer(0, (builder) =>
builder
.addCase(increment, (state, action) => {
// action is inferred correctly here
})
.addCase(decrement, (state, action: PayloadAction<string>) => {
// this would error out
}),
)
builder.addMatcher
の型付け
builder.addMatcher
への最初の matcher
引数として、型述語関数を使用する必要があります。その結果、2 番目の reducer
引数の action
引数は TypeScript によって推論できます
function isNumberValueAction(action: UnknownAction): action is PayloadAction<{ value: number }> {
return typeof action.payload.value === 'number'
}
createReducer({ value: 0 }, builder =>
builder.addMatcher(isNumberValueAction, (state, action) => {
state.value += action.payload.value
})
})
createSlice
createSlice
はアクションとリデューサーを作成するため、ここでは型安全性を心配する必要はありません。アクション型はインラインで指定できます
const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
})
// now available:
slice.actions.increment(2)
// also available:
slice.caseReducers.increment(0, { type: 'increment', payload: 5 })
ケースリデューサーが多すぎてインラインで定義すると煩雑になる場合や、スライス間でケースリデューサーを再利用したい場合は、createSlice
呼び出しの外で定義し、CaseReducer
として型付けすることもできます
type State = number
const increment: CaseReducer<State, PayloadAction<number>> = (state, action) =>
state + action.payload
createSlice({
name: 'test',
initialState: 0,
reducers: {
increment,
},
})
初期状態型の定義
SliceState
型をジェネリックとして createSlice
に渡すのは良い考えではないことに気付いたかもしれません。これは、ほとんどの場合、createSlice
への後続のジェネリックパラメーターは推論する必要があり、TypeScript は同じ「ジェネリックブロック」内でジェネリック型の明示的な宣言と推論を混在させることができないためです。
標準的なアプローチは、状態のインターフェイスまたは型を宣言し、その型を使用する初期状態値を作成し、初期状態値を createSlice
に渡すことです。また、initialState: myInitialState satisfies SliceState as SliceState
という構造を使用することもできます。
type SliceState = { state: 'loading' } | { state: 'finished'; data: string }
// First approach: define the initial state using that type
const initialState: SliceState = { state: 'loading' }
createSlice({
name: 'test1',
initialState, // type SliceState is inferred for the state of the slice
reducers: {},
})
// Or, cast the initial state as necessary
createSlice({
name: 'test2',
initialState: { state: 'loading' } satisfies SliceState as SliceState,
reducers: {},
})
これにより、Slice<SliceState, ...>
が生成されます。
prepare
コールバックを使用したアクション内容の定義
アクションに meta
プロパティまたは error
プロパティを追加したり、アクションの payload
をカスタマイズしたりする場合は、prepare
表記を使用する必要があります。
TypeScript でこの表記を使用すると、次のようになります
const blogSlice = createSlice({
name: 'blogData',
initialState,
reducers: {
receivedAll: {
reducer(
state,
action: PayloadAction<Page[], string, { currentPage: number }>,
) {
state.all = action.payload
state.meta = action.meta
},
prepare(payload: Page[], currentPage: number) {
return { payload, meta: { currentPage } }
},
},
},
})
スライス用に生成されたアクション型
createSlice
は、スライスからの name
フィールドとリデューサー関数のフィールド名を組み合わせた 'test/increment'
のようなアクション型文字列を生成します。これは、TS の文字列リテラル分析のおかげで、正確な値として強く型付けされます。
また、slice.action.myAction.match
型述語を使用することもできます。これにより、アクションオブジェクトが正確な型に絞り込まれます
const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
})
type incrementType = typeof slice.actions.increment.type
// type incrementType = 'test/increment'
function myCustomMiddleware(action: Action) {
if (slice.actions.increment.match(action)) {
// `action` is narrowed down to the type `PayloadAction<number>` here.
}
}
実際にその型が必要な場合は、残念ながら手動でキャストするしか方法はありません。
extraReducers
を使用した型安全性
アクションの type
文字列をリデューサー関数にマップするリデューサールックアップテーブルは、完全に正しく型付けするのが簡単ではありません。これは、createReducer
と createSlice
の extraReducers
引数の両方に影響します。したがって、createReducer
と同様に、リデューサーオブジェクト引数を定義するには「ビルダーコールバック」アプローチを使用する必要があります。
これは、スライスリデューサーが他のスライスによって生成されたアクションタイプ、またはcreateAction
への特定の呼び出し(createAsyncThunk
によって生成されるアクションなど)によって生成されたアクションタイプを処理する必要がある場合に特に役立ちます。
const fetchUserById = createAsyncThunk(
'users/fetchById',
// if you type your function argument here
async (userId: number) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`)
return (await response.json()) as Returned
},
)
interface UsersState {
entities: User[]
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}
const initialState = {
entities: [],
loading: 'idle',
} satisfies UsersState as UsersState
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// fill in primary logic here
},
extraReducers: (builder) => {
builder.addCase(fetchUserById.pending, (state, action) => {
// both `state` and `action` are now correctly typed
// based on the slice state and the `pending` action creator
})
},
})
createReducer
のbuilder
と同様に、このbuilder
もaddMatcher
(builder.matcher
の型付けを参照)とaddDefaultCase
を受け入れます。
すべてのフィールドがオプションのペイロード
PayloadAction<Partial<User>>
やPayloadAction<{value?: string}>
のように、すべてのフィールドがオプションであるペイロード型を提供しようとすると、TSはアクション型を正しく推論できない場合があります。
これを回避するには、少なくとも1つのフィールドが渡されるように、カスタムのAtLeastOne
ユーティリティ型を使用することができます。
type AtLeastOne<T extends Record<string, any>> = keyof T extends infer K
? K extends string
? Pick<T, K & keyof T> & Partial<T>
: never
: never
// Use this type instead of `Partial<MyPayloadType>`
type AtLeastOneUserField = AtLeastOne<User>
createSlice
内での非同期サンクの型付け
2.0以降、createSlice
では、コールバック構文を使用してreducers
内でサンクを定義できます。
create.asyncThunk
メソッドの型付けは、createAsyncThunk
と同じように機能しますが、1つの重要な違いがあります。
state
やdispatch
の型は、循環型を引き起こすため、ThunkApiConfig
の一部として提供できません。
代わりに、必要なときにgetState() as RootState
のように型をアサートする必要があります。また、循環型推論サイクルを中断するために、ペイロード関数の明示的な戻り値の型を含めることもできます。
create.asyncThunk<Todo, string, { rejectValue: { error: string } }>(
// may need to include an explicit return type
async (id: string, thunkApi): Promise<Todo> => {
// Cast types for `getState` and `dispatch` manually
const state = thunkApi.getState() as RootState
const dispatch = thunkApi.dispatch as AppDispatch
try {
const todo = await fetchTodo()
return todo
} catch (e) {
throw thunkApi.rejectWithValue({
error: 'Oh no!',
})
}
},
)
一般的なサンクAPI構成オプションについては、withTypes
ヘルパーが提供されています。
reducers: (create) => {
const createAThunk = create.asyncThunk.withTypes<{
rejectValue: { error: string }
}>()
return {
fetchTodo: createAThunk<Todo, string>(async (id, thunkApi) => {
throw thunkApi.rejectWithValue({
error: 'Oh no!',
})
}),
fetchTodos: createAThunk<Todo[], string>(async (id, thunkApi) => {
throw thunkApi.rejectWithValue({
error: 'Oh no, not again!',
})
}),
}
}
createSlice
のラップ
リデューサーロジックを再利用する必要がある場合、追加の共通動作でリデューサー関数をラップする「高階リデューサー」を作成するのが一般的です。これはcreateSlice
でも行うことができますが、createSlice
の型の複雑さのため、SliceCaseReducers
型とValidateSliceCaseReducers
型を非常に特定の方法で使用する必要があります。
そのような「汎用的な」ラップされたcreateSlice
呼び出しの例を次に示します。
interface GenericState<T> {
data?: T
status: 'loading' | 'finished' | 'error'
}
const createGenericSlice = <
T,
Reducers extends SliceCaseReducers<GenericState<T>>,
>({
name = '',
initialState,
reducers,
}: {
name: string
initialState: GenericState<T>
reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>
}) => {
return createSlice({
name,
initialState,
reducers: {
start(state) {
state.status = 'loading'
},
/**
* If you want to write to values of the state that depend on the generic
* (in this case: `state.data`, which is T), you might need to specify the
* State type manually here, as it defaults to `Draft<GenericState<T>>`,
* which can sometimes be problematic with yet-unresolved generics.
* This is a general problem when working with immer's Draft type and generics.
*/
success(state: GenericState<T>, action: PayloadAction<T>) {
state.data = action.payload
state.status = 'finished'
},
...reducers,
},
})
}
const wrappedSlice = createGenericSlice({
name: 'test',
initialState: { status: 'loading' } as GenericState<string>,
reducers: {
magic(state) {
state.status = 'finished'
state.data = 'hocus pocus'
},
},
})
createAsyncThunk
基本的なcreateAsyncThunk
の型
最も一般的なユースケースでは、createAsyncThunk
の呼び出し自体に対して明示的に型を宣言する必要はありません。
任意の関数の引数と同様に、payloadCreator
引数の最初の引数の型を指定するだけで、結果のサンクは入力パラメーターと同じ型を受け入れます。payloadCreator
の戻り値の型も、生成されるすべてのアクションタイプに反映されます。
interface MyData {
// ...
}
const fetchUserById = createAsyncThunk(
'users/fetchById',
// Declare the type your function argument here:
async (userId: number) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`)
// Inferred return type: Promise<MyData>
return (await response.json()) as MyData
},
)
// the parameter of `fetchUserById` is automatically inferred to `number` here
// and dispatching the resulting thunkAction will return a Promise of a correctly
// typed "fulfilled" or "rejected" action.
const lastReturnedAction = await store.dispatch(fetchUserById(3))
thunkApi
オブジェクトの型付け
thunkApi
として知られるpayloadCreator
の2番目の引数は、サンクミドルウェアからのdispatch
、getState
、およびextra
引数への参照と、rejectWithValue
というユーティリティ関数を含むオブジェクトです。payloadCreator
内でこれらを使用する場合は、これらの引数の型を推論できないため、いくつかのジェネリック引数を定義する必要があります。また、TSは明示的なジェネリックパラメーターと推論されたジェネリックパラメーターを混在させることができないため、この時点から、Returned
とThunkArg
ジェネリックパラメーターも定義する必要があります。
thunkApi
型のマニュアル定義
これらの引数の型を定義するには、3番目のジェネリック引数としてオブジェクトを渡し、これらのフィールドの一部またはすべてに対して型宣言を行います。
type AsyncThunkConfig = {
/** return type for `thunkApi.getState` */
state?: unknown
/** type for `thunkApi.dispatch` */
dispatch?: Dispatch
/** type of the `extra` argument for the thunk middleware, which will be passed in as `thunkApi.extra` */
extra?: unknown
/** type to be passed into `rejectWithValue`'s first argument that will end up on `rejectedAction.payload` */
rejectValue?: unknown
/** return type of the `serializeError` option callback */
serializedErrorType?: unknown
/** type to be returned from the `getPendingMeta` option callback & merged into `pendingAction.meta` */
pendingMeta?: unknown
/** type to be passed into the second argument of `fulfillWithValue` to finally be merged into `fulfilledAction.meta` */
fulfilledMeta?: unknown
/** type to be passed into the second argument of `rejectWithValue` to finally be merged into `rejectedAction.meta` */
rejectedMeta?: unknown
}
const fetchUserById = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
number,
{
// Optional fields for defining thunkApi field types
dispatch: AppDispatch
state: State
extra: {
jwt: string
}
}
>('users/fetchById', async (userId, thunkApi) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`, {
headers: {
Authorization: `Bearer ${thunkApi.extra.jwt}`,
},
})
return (await response.json()) as MyData
})
通常、成功するか、予想されるエラー形式になることがわかっているリクエストを実行する場合は、rejectValue
に型を渡し、アクションクリエーターでreturn rejectWithValue(knownPayload)
を返すことができます。これにより、リデューサー内だけでなく、createAsyncThunk
アクションをディスパッチした後のコンポーネントでもエラーペイロードを参照できます。
interface MyKnownError {
errorMessage: string
// ...
}
interface UserAttributes {
id: string
first_name: string
last_name: string
email: string
}
const updateUser = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
UserAttributes,
// Types for ThunkAPI
{
extra: {
jwt: string
}
rejectValue: MyKnownError
}
>('users/update', async (user, thunkApi) => {
const { id, ...userData } = user
const response = await fetch(`https://reqres.in/api/users/${id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${thunkApi.extra.jwt}`,
},
body: JSON.stringify(userData),
})
if (response.status === 400) {
// Return the known error for future handling
return thunkApi.rejectWithValue((await response.json()) as MyKnownError)
}
return (await response.json()) as MyData
})
state
、dispatch
、extra
、およびrejectValue
のこの表記法は、最初は一般的ではないように見えるかもしれませんが、実際に必要なこれらの型のみを提供できます。たとえば、payloadCreator
内でgetState
にアクセスしない場合は、state
の型を提供する必要はありません。rejectValue
についても同じことが言えます。潜在的なエラーペイロードにアクセスする必要がない場合は、無視できます。
さらに、createAction
によって提供されるaction.payload
とmatch
に対するチェックを、定義された型の既知のプロパティにアクセスしたい場合の型ガードとして利用できます。例
- リデューサー内
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: {},
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
state.entities[payload.id] = payload
})
builder.addCase(updateUser.rejected, (state, action) => {
if (action.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
state.error = action.payload.errorMessage
} else {
state.error = action.error
}
})
},
})
- コンポーネント内
const handleUpdateUser = async (userData) => {
const resultAction = await dispatch(updateUser(userData))
if (updateUser.fulfilled.match(resultAction)) {
const user = resultAction.payload
showToast('success', `Updated ${user.name}`)
} else {
if (resultAction.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
// Note: this would also be a good place to do any handling that relies on the `rejectedWithValue` payload, such as setting field errors
showToast('error', `Update failed: ${resultAction.payload.errorMessage}`)
} else {
showToast('error', `Update failed: ${resultAction.error.message}`)
}
}
}
事前型付けされたcreateAsyncThunk
の定義
RTK 1.9以降では、state
、dispatch
、およびextra
の型を組み込むことができる、事前型付けされたバージョンのcreateAsyncThunk
を定義できます。これにより、これらの型を一度設定するだけで、createAsyncThunk
を呼び出すたびに繰り返す必要がなくなります。
これを行うには、createAsyncThunk.withTypes<>()
を呼び出し、上記にリストされているAsyncThunkConfig
型のフィールド名と型を含むオブジェクトを渡します。これは次のようになります。
const createAppAsyncThunk = createAsyncThunk.withTypes<{
state: RootState
dispatch: AppDispatch
rejectValue: string
extra: { s: string; n: number }
}>()
元の代わりに、事前型付けされたcreateAppAsyncThunk
をインポートして使用すると、型が自動的に使用されます。
createEntityAdapter
createEntityAdapter
の型付けには、エンティティ型を単一のジェネリック引数として指定するだけです。
createEntityAdapter
ドキュメントの例は、TypeScriptでは次のようになります。
interface Book {
bookId: number
title: string
// ...
}
const booksAdapter = createEntityAdapter<Book>({
selectId: (book) => book.bookId,
sortComparer: (a, b) => a.title.localeCompare(b.title),
})
const booksSlice = createSlice({
name: 'books',
initialState: booksAdapter.getInitialState(),
reducers: {
bookAdded: booksAdapter.addOne,
booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
booksAdapter.setAll(state, action.payload.books)
},
},
})
normalizr
でのcreateEntityAdapter
の使用
normalizr
のようなライブラリを使用する場合、正規化されたデータは次の形状になります。
{
result: 1,
entities: {
1: { id: 1, other: 'property' },
2: { id: 2, other: 'property' }
}
}
addMany
、upsertMany
、およびsetAll
の各メソッドでは、追加の変換ステップなしで、これのentities
部分を直接渡すことができます。ただし、normalizr
のTS型付けでは、複数のデータ型が結果に含まれる可能性があることを正しく反映していないため、その型構造を自分で指定する必要があります。
その例を次に示します。
type Author = { id: number; name: string }
type Article = { id: number; title: string }
type Comment = { id: number; commenter: number }
export const fetchArticle = createAsyncThunk(
'articles/fetchArticle',
async (id: number) => {
const data = await fakeAPI.articles.show(id)
// Normalize the data so reducers can responded to a predictable payload.
// Note: at the time of writing, normalizr does not automatically infer the result,
// so we explicitly declare the shape of the returned normalized data as a generic arg.
const normalized = normalize<
any,
{
articles: { [key: string]: Article }
users: { [key: string]: Author }
comments: { [key: string]: Comment }
}
>(data, articleEntity)
return normalized.entities
},
)
export const slice = createSlice({
name: 'articles',
initialState: articlesAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// The type signature on action.payload matches what we passed into the generic for `normalize`, allowing us to access specific properties on `payload.articles` if desired
articlesAdapter.upsertMany(state, action.payload.articles)
})
},
})