reduxtoolkit+typescript) findNPM (createAsyncThunk의 generic)

김명성·2022년 7월 7일

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에 타입스크립트 적용이 어려워 하루종일 작업했다.

0개의 댓글