
createAsyncThunk의 제네릭 예시
// 공식문서
const updateUser = createAsyncThunk<
// Return type of the payload creator
// 1번째로는 응답 받는 payload의 타입 기재
MyData,
// First argument to the payload creator
// callback function의 첫번째 매개변수의 타입
UserAttributes,
// Types for ThunkAPI
// 2번째 매개변수 ThunkApi의 타입
{
extra: {
jwt: string
}
rejectValue: MyKnownError
}
>
// RepositorySlice . npm api를 관리하는 toolkit의 slice
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export interface RepositoriesState {
loading: boolean;
error: string | undefined;
data: NpmPackage[];
}
const initialState: RepositoriesState = {
loading: false,
error: undefined,
data: [],
}
interface NpmPackage {
package:{
name: string,
version:string,
description:string,
links:{
npm:string,
homepage:string
}
}
}
// createAsyncThunk
// action type string을 받아 Callback 함수를 실행하고 그 결과를 Promise로 담아 Thunk action creator로 반환하는 콜백 함수이다.
// 어떤 데이터를 가져와서, 어떤 데이터를 반환해야 하는지 모르기 때문에 Reducer 함수를 내부에서 생성할 수 없다.
// 작성한 CreateAsyncThunk는 createSlice의 extraReducers로 등록하여 사용한다.
// createAsyncThunk와 createSlice를 사용하여 Toolkit만으로 비동기 처리를 쉽게 할 수 있으며
// saga의 어느정도 기능까지 구현이 가능하다.
// createAsyncThunk는 액션타입의 문자열, 프로미스를 반환하는 비동기 함수, 추가옵션을 받는다.
// 입력받은 액션 타입 문자열을 기반으로 프로미스 라이프사이클 액션 타입을 생성하고, thunk action creator를 반환한다.
// thunk action creator는 프로미스 콜백을 실행하고, 프로미스를 기반으로 라이프사이클액션을 디스패치한다.
export const searchRepositories = createAsyncThunk<NpmPackage[],string,{rejectValue: Error}>(
"repositories/search",
async (term:string) => {
const { data } = await axios.get("https://registry.npmjs.org/-/v1/search",{
params: {
text: term
}
});
return data.objects.map((result: NpmPackage) => result)
})
const repositoriesSlice = createSlice({
name: "repositories",
initialState,
reducers: {},
// slice.actions에서 생성되지 않은 action을 사용할 수 있게 해주는 extraReducers
// builder의 addCase를 통해 작성한 CreateAsyncThunk를 등록한다.
// 3가지 작업유형 fulfilled, rejected, pending을 생성한다.
extraReducers: (builder) => {
builder.addCase(searchRepositories.fulfilled, (state,action) => {
state.loading = false;
state.data = [...action.payload]
})
builder.addCase(searchRepositories.rejected, (state, action) => {
let error;
if (action.payload) {
error = action.payload.message;
} else {
error = action.error.message;
}
return { loading: false, error, data: [] };
});
builder.addCase(searchRepositories.pending, () => {
return { loading: true, error: undefined, data: [] };
});
},
});
export const repositoriesReducer = repositoriesSlice.reducer;
import { useSelector, TypedUseSelectorHook, useDispatch } from "react-redux";
import {AppDispatch, RootState} from '../store'
// useSelector를 바로 사용 할 수 없다..
// 공식문서에 나와있다.....
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// RootState = ReturnType<typeof store.getState>
export const useAppDispatch = () => useDispatch<AppDispatch>();
// store.ts
// 모든 slice를 관리하는 곳.
import { configureStore } from "@reduxjs/toolkit";
import { repositoriesReducer } from "./slices/RepositorySlice";
export const store = configureStore({
reducer: {
repositories: repositoriesReducer
}
})
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
실제 사용. 먼저 최상위 script에 store를 뿌려주고,
import React from 'react';
import { Provider } from 'react-redux';
import { store } from '../store/store'
import RepositoriesList from './RepositoriesList'
const App = () => {
return (
<Provider store={store}>
<div className='absolute top-20 left-1/2 -translate-x-1/2 -translate-y-10'>
<h1 className="text-3xl font-bold">Search For a NPM Package</h1>
<RepositoriesList/>
</div>
</Provider>
);
};
export default App;
import React, { ChangeEvent, FormEvent, useState } from 'react';
import LoadingSpinner from '../shared/UI/LoadingSpinner';
import { useAppDispatch, useAppSelector } from '../store/hooks/useActions'
import {searchRepositories} from '../store/slices/RepositorySlice'
import RepositoriesItem from './RepositoriesItem';
interface childProps {
children: React.ReactNode
}
const RepositoriesList = () => {
const dispatch = useAppDispatch();
const [inputVal, setInputVal] = useState('');
// custom hook을 작성하지 않으면 이부분에서 끝도 없는 미궁의 에러가 나타남...
const {data,error,loading} = useAppSelector((state) => state.repositories);
const onSubmit = (e:FormEvent<HTMLFormElement>) => {
e.preventDefault()
dispatch(searchRepositories(inputVal))
}
const onChange = (e:ChangeEvent<HTMLInputElement>) => {
setInputVal(e.target.value)
}
return (
<div className='p-4'>
<form className='flex justify-between items-center' onSubmit={onSubmit}>
<input type="text" value={inputVal} onChange={onChange} className='py-2 px-4 outline-2 outline-rose-400 font-bold ' />
<button className='border py-2 px-4 border-slate-600 font-bold flex items-center'>Search{loading && <LoadingSpinner />}</button>
</form>
{error && <h3>{error}</h3>}
<RepositoriesItem data={data} error={error} loading={loading} />
</div>
);
};
export default RepositoriesList;
리스트의 아이템 부분.. 프레젠테이션의 역할만 담당한다.
import React from 'react';
import { RepositoriesState } from '../store/slices/RepositorySlice';
const RepositoriesItem = ({error,loading,data}:RepositoriesState) => {
return (
<>
{!error && !loading && data.map(item =>
<ul key={item.package.name} className='border border-slate-800 my-4 px-4 leading-10 '>
<li className='text-xl font-bold border-b py-2 border-yellow-300'><a href={item.package.links.npm}><span className=''>{item.package.name}</span></a></li>
<li><span className='font-bold'>CurrentVersion:</span> <span>{item.package.version} version.</span></li>
<li><span>{item.package.description}</span></li>
</ul>
)}
</>
)
};
export default RepositoriesItem;
몇시간이면 될 줄 알았는데 toolkit에 타입스크립트 적용이 어려워 하루종일 작업했다.