Redux Toolkit 공식문서를 개인 학습용으로 정리한 글입니다.
Redux Toolkit은 TypeScript로 작성되었으며 API는 TypeScript 애플리케이션과의 뛰어난 통합을 가능하게하도록 설계되었습니다.
State
타입을 얻는 가장 쉬운 방법은 루트리듀서를 미리 정의하고 ReturnType
타입을 추출하는 것입니다. State
라는 이름은 일반적으로 과도하게 사용되므로 혼동을 방지하기 위해 RootState
와 같이 다른 이름을 지정하는 것이 좋습니다.
import { combineReducers } from '@reduxjs/toolkit'
const rootReducer = combineReducers({})
export type RootState = ReturnType<typeof rootReducer>
루트 리듀서를 생성하지 않는다면 다음과 같은 방식 사용
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
스토어에서 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<AppDispatch>() // Export a hook that can be reused to resolve types
export default store
dispatch
함수의 타입은 middleware
옵션에 따라 직접 유추됩니다. 따라서 올바르게 typed된 미들웨어를 추가하면 dispatch
이미 올바르게 typed되어 있습니다.
TypeScript는 스프레드 연산자를 사용하여 배열을 결합 할 때 배열 타입을 확장하는 경우가 많으므로 getDefaultMiddleware()
에서 반환된 MiddlewareArray
의 .concat(...)
및 .prepend(...)
메서드를 사용하는 것이 좋습니다.
또한 제네릭을 직접 지정할 필요가 없는 올바르게 사전 타입화된 getDefaultMiddleware
버전을 가져 오는 middleware
옵션에 대한 콜백 표기법을 사용하는 것이 좋습니다.
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
기본적으로 React Redux useDispatch
훅에는 미들웨어를 고려하는 타입이 없습니다. 디스패치 할 때 dispatch
함수에 대해 더 구체적인 타입이 필요한 경우, 반환된 dispatch
함수의 타입을 지정 하거나 useSelector
의 사용자 정의 타입 버전을 만들 수 있습니다 자세한 내용 은 React Redux 문서를 참조하십시오.
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 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
// 타입 SliceState는 슬라이스 상태에 대해 유추됩니다.
reducers: {},
})
// Or, cast the initial state as necessary
// 또는 필요에 따라 초기 상태를 캐스팅
createSlice({
name: 'test2',
initialState: { state: 'loading' } as SliceState,
reducers: {},
})
결과는 Slice<SliceState, ...>
.
액션에 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 } }
},
},
},
})
TS는 두 개의 문자열 리터럴 (slice.name
및 actionMap
의 키)을 새 리터럴로 결합 할 수 없으므로 createSlice
에서 생성된 모든 액션 생성자는 '문자열' 타입입니다. 이러한 타입은 리터럴로 거의 사용되지 않으므로 일반적으로 문제가 되지 않습니다.
type 리터럴로 필요한 대부분의 경우 slice.action.myAction.match
타입 참조는 실행 가능한 대안이어야 합니다.
const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
})
function myCustomMiddleware(action: Action) {
if (slice.actions.increment.match(action)) {
// `action` is narrowed down to the type `PayloadAction<number>` here.
// 여기서`action`은`PayloadAction <number>`타입으로 좁혀집니다.
}
}
실제로 해당 타입이 필요한 경우 불행히도 수동 캐스팅 외에 다른 방법은 없습니다.
일반적인 사용 사례에서는 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
로 알려진 payloadCreator
의 두번째 인수는, dispatch
, getState
및 extra
썽크 미들웨어뿐만 아니라 rejectWithValue
라는 유틸리티 함수의 인수를 포함하는 객체입니다.payloadCreator
안에서 이들을 사용하려면, 이러한 인수의 타입을 유추 할 수 없으므로 일부 일반 인수를 정의해야 합니다. 또한 TS는 명시적 매개 변수와 추론된 일반 매개 변수를 혼합할 수 없기 때문에 이 시점부터 Returned
및 ThunkArg
일반 매개 변수도 정의해야합니다.
이러한 인수의 유형을 정의하려면 다음 필드의 일부 또는 전체에 대한 타입 선언과 함께 객체를 세 번째 일반 인수로 전달합니다.
{dispatch?, state?, extra?, rejectValue?}
..
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
타입에 전달 혹은 액션 생성자에서 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
})