리덕스 툴킷은 타입스크립트로 만들어져 있어서 TS definition이 내장되어있다.
React-Redux는 별도의 패키지인 @types/react-redux
에 타입 정의를 가지고 있다.
React-Redux는 v7.2.3 이후 @types/react-redux
에 의존성을 가지며 자동으로 설지된다.
yarn add @reduxjs/toolkit react-redux
yarn add -D redux-logger @types/redux-logger
redux-logger
패키지는 개발자도구 콘솔에 redux state 변화를 로그로 남겨주는 패키지이다.
redux devtools extension을 사용하면 굳이 사용하지 않아도 될 것 같다.
리덕스 툴킷은 configureStore
를 사용하여 기본적인 설정을 제공한다.
import { configureStore } from '@reduxjs/toolkit'
import loginSlice from '../slices/loginSlice'
import joinSlice from '../slices/joinSlice'
export const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
users: usersReducer,
loginData: loginSlice.reducer
joinData: joinSlice.reducer
}
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
위 설정을 통해 store는 자체적으로 RootState
와 AppDispatch
의 타입을 추론한다.
slice 구성에 따라 다르겠지만 아래와 같이 타입이 추론될 것이다.
{ posts: PostsState, comments: CommentsState, users: UsersState }
여기서, configureStore
가 아닌 combineReducers
를 사용한다면 아래와 같이 사용한다
import { combineReducers } from '@reduxjs/toolkit'
const rootReducer = combineReducers({})
export type RootState = ReturnType<typeof rootReducer>
State 타입을 얻는 가장 쉬운방법은 root reducer를 정의하는 것이고 그것의 ReturnType을 추출하는 것이다.
State는 보통 흔히 사용되기 때문에 혼동방지를 위해 RootState와 같은 이름을 추천한다.
RootState
와 AppDispatch
타입을 각 컴포넌트에서 import해서 사용할 수도 있지만, 애플리케이션에서 사용할 타입이 지정된 useDispatch
나 useSelector
을 만들어서 사용하는 것이 더 좋다.
useSelector
는 매번 (state: RootState)를 입력할 필요가 없음useDispatch
의 경우, 기본 Dispatch
타입은 thunk와 같은 미들웨어를 포함하지 않는다. thunks를 올바르게 dispatch하기 위해, thunk 미들웨어 타입을 포함하는 store로부터 커스터마이징된 특정 AppDispatch
타입을 사용할 필요가 있고 그것을 useDispatch
로 사용해야한다.import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch: () => AppDispatch = useDispatch
useAppSelector와 useAppDispatch는 기존 useSelector, useDispatch를 추상화한 것이다. 이렇게 사용하면 각 컴포넌트에서 useSelector, useDispatch를 매번 설정하지 않고 앱 전역에서 사용할 수 있다.
초기 상태값에 대한 type 정의를 포함한 상태 slice 파일을 만든다. 만든 초기 상태값을 createSlice
에 포함시키면 각 reducer 케이스 별로 state의 type을 알아서 추론한다.
생성되는 action들은 리덕스 툴킷의 PayloadAction<T>
타입을 사용해야한다.
여기서 사용된 제네릭 타입은 action.payload
인자로 사용된다.
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'
// slice 상태 타입정의
interface CounterState {
value: number;
}
// slice 상태타입을 사용하는 초기상태 정의
const initialState: CounterState = {
value: 0,
}
export const counterSlice = createSlice({
name: 'counter',
// 'createSlice'는 initialState를 통해 state type을 추론한다.
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
// action.payload 사용을 위해 PayloadAction<T> 타입 사용
incrementByAmout: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } counterSlice.actions
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface JoinState {
email: string;
name: string;
password: string;
password_check: string;
}
const initialState: JoinState = {
email: '',
name: '',
password: '',
password_check: '',
}
const joinSlice = createSlice({
name: 'joinData',
initialState,
reducers: {
setJoinData: (state, action: PayloadAction<JoinState>) {
return { ...joinData, ...action.payload }
}
}
})
export const { setJoinData } = joinSlice.actions;
export default joinSlice;
import React, { useState } from 'react'
import { useAppSelector, useAppDispatch } from 'app/hooks'
import { decrement, increment } from './counterSlice'
export functoin Counter() {
const count = useAppSelector((state) => state.counter.value)
const dispatch = useAppDispatch()
// ...
}
import { useAppDispatch, useAppSelector } from '../../sample/store/config';
import { setJoinData } from '../../sample/store/slices/joinSlice';
const JoinForm = () => {
const joinData = useAppSelector((state) => state.joinData);
const dispatch = useAppDispatch();
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
dispatch(setJoinData({ ...joinData, [name]: value }));
}
}
// ...