Stack/TypeScript

[TS / React] redux-thunk Middleware

7ingout 2022. 7. 29. 18:00

TS-REDUX-TUTORIAL

npm install redux-thunk

// 또는 yarn add redux-thunk

npm install axios

 

* github로부터 내 정보 받아오기 (server로 사용할 것임)

https://api.github.com/users/(githun닉넴) 

 

https://app.quicktype.io/

 

Instantly parse JSON in any language | quicktype

 

app.quicktype.io

JSON 포맷으로 만들어주는 사이트 ~

 

index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { applyMiddleware, createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import Thunk from 'redux-thunk';

const store = createStore(rootReducer, applyMiddleware(Thunk))

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

api/github.ts

import axios from "axios";
export const getUserProfile = async (username: string) => {
    const response = await axios.get<GithubProfile>(`https://api.github.com/users/${username}`)
    return response.data; // 데이터 값을 바로 반환
}

export interface GithubProfile {
    login:               string;
    id:                  number;
    node_id:             string;
    avatar_url:          string;
    gravatar_id:         string;
    url:                 string;
    html_url:            string;
    followers_url:       string;
    following_url:       string;
    gists_url:           string;
    starred_url:         string;
    subscriptions_url:   string;
    organizations_url:   string;
    repos_url:           string;
    events_url:          string;
    received_events_url: string;
    type:                string;
    site_admin:          boolean;
    name:                string;
    company:             string;
    blog:                string;
    location:            null;
    email:               null;
    hireable:            null;
    bio:                 string;
    twitter_username:    null;
    public_repos:        number;
    public_gists:        number;
    followers:           number;
    following:           number;
    created_at:          Date;
    updated_at:          Date;
}

 

modules/github/actions.ts

import { AxiosError } from "axios";
import { GithubProfile } from "../../api/github";
import { createAsyncAction } from "typesafe-actions";
// const { createStandardAction } = deprecated;



export const GET_USER_PROFILE = 'github/GET_USER_PROFILE';
export const GET_USER_PROFILE_SUCCESS = 'github/GET_USER_PROFILE_SUCCESS';
export const GET_USER_PROFILE_ERROR = 'github/GET_USER_PROFILE_ERROR';

// 액션 생성함수
// export const getUserProfile = createStandardAction(GET_USER_PROFILE)();
// export const getUserProfileSuccess = createStandardAction(GET_USER_PROFILE_SUCCESS)<GithubProfile>();
// export const getUserProfileError = createStandardAction(GET_USER_PROFILE_ERROR)<AxiosError>();

export const getUserProfileAsync = createAsyncAction(
    GET_USER_PROFILE, 
    GET_USER_PROFILE_SUCCESS, 
    GET_USER_PROFILE_ERROR
)<undefined, GithubProfile, AxiosError>();

 

modules/github/thunk.ts

import { getUserProfileAsync } from './actions';
import { GithubAction } from './types';
import { getUserProfile } from '../../api/github';
import { RootState } from '..';
import { ThunkAction } from 'redux-thunk';

// ThunkAction 제네릭
// 1. TReturnType: thunk 함수에서 반환하는 값의 타입을 설정
// 2. TState: 스토어의 상태에 대한 타입
// 3. TEXTRAThunkArg: redux-thunk 미들웨어의 Extran Argument의 타입을 지정
// 4. TBasicAction: dispatch 할 수 있는 액션들의 타입을 설정
export function getUserProfileThunk(username: string) : ThunkAction<void, RootState, null, GithubAction> {
    return async dispatch => {
        const { request, success, failure} = getUserProfileAsync;
        dispatch(request());
        try {
            const userProfile = await getUserProfile(username)
            dispatch(success(userProfile))
        } catch(e:any) {
            dispatch(failure(e))
        }
    };
}

 

modules/github/types.ts

import * as actions from './actions';
import { ActionType } from 'typesafe-actions'
import { GithubProfile } from '../../api/github';

export type GithubAction = ActionType<typeof actions>

export type GithubState = {
    userProfile: {
        loading: boolean;
        error: Error | null;
        data: GithubProfile | null;
    }
}

 

modules/github/reducer.ts

import { action, createReducer } from "typesafe-actions";
import { GithubState, GithubAction } from "./types";
import { GET_USER_PROFILE, GET_USER_PROFILE_SUCCESS, GET_USER_PROFILE_ERROR } from "./actions";

// 초기값 설정
const initialState: GithubState = {
    userProfile: {
        loading: false,
        error: null,
        data: null
    }
}

const github = createReducer<GithubState, GithubAction>(initialState, {
    [GET_USER_PROFILE]: state => ({
        ...state,
        userProfile: {
            loading: true,
            error: null,
            data: null
        }
    }),
    [GET_USER_PROFILE_SUCCESS]: (state, action) => ({
        ...state,
        userProfile: {
            loading: false,
            error: null,
            data: action.payload
        }
    }),
    [GET_USER_PROFILE_ERROR]: (state, action) => ({
        ...state,
        userProfile: {
            loading: false,
            error: action.payload,
            data: null
        }
    })
})

export default github;

 

modules/github/index.ts

export { default } from './reducer';
export * from './actions'
export * from './thunk'
export * from './types'

 

modules/index.ts

import { combineReducers } from "redux";
import counter from "./counter"
import todos from "./todos"
import github from "./github"

const rootReducer = combineReducers({ counter, todos, github });
export default rootReducer;

// 나중에 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야하므로
// 내보내줍니다.
export type RootState = ReturnType<typeof rootReducer>

 

components/GithubusernameForm.tsx

import React, { ChangeEvent, FormEvent, useState } from 'react';

type GithubUsernameFormprops = {
    onSubmitUsername: (username: string) => void
}
const GithubusernameForm = ({ onSubmitUsername }: GithubUsernameFormprops) => {
    const [ input, setInput ] = useState('');
    const onSubmit = (e: FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        onSubmitUsername(input);
    }
    const onChange = (e: ChangeEvent<HTMLInputElement>) => {
        setInput(e.target.value);
    }
    return (
        <form onSubmit={onSubmit}>
            <input onChange={onChange} value={input} placeholder="github 계정명을 입력하세요"/>
            <button>조회</button>
        </form>
    );
};

export default GithubusernameForm;

 

components/GithubProfileInfo.tsx

import React from 'react';

type GithubProfileInfoProps = {
    name: string;
    thumbnail: string;
    bio: string;
    blog: string;
}
const GithubProfileInfo = ({ name, thumbnail, bio, blog }: GithubProfileInfoProps) => {
    return (
        <div>
            <div>
                <img src={thumbnail} alt="img"/>
                <div>{name}</div>
            </div>
            <p>{bio}</p>
            <div>{blog !== '' && <a href={blog}>블로그</a>}</div>
        </div>
    );
};

export default GithubProfileInfo;

 

containers/GithubProfileLoader.tsx

import React from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import GithubProfileInfo from '../components/GithubProfileInfo';
import GithubusernameForm from '../components/GithubusernameForm';
import { RootState } from '../modules';
import { getUserProfileThunk } from '../modules/github';
import type {} from 'redux-thunk/extend-redux';

const GithubProfileLoader = () => {
    const { data, loading, error } = useSelector((state: RootState) => state.github.userProfile);
    const dispatch = useDispatch();

    const onSubmitUsername = (username: string) => {
        dispatch(getUserProfileThunk(username))
    }
    return (
      <>
        <GithubusernameForm onSubmitUsername={onSubmitUsername}/>
        {loading && <p>로딩중 ..</p>}
        {error && <p>에러발생 ..</p>}
        {data && <GithubProfileInfo bio={data.bio}
        blog={data.blog}
        name={data.name}
        thumbnail={data.avatar_url}/>}
      </>
    );
};

export default GithubProfileLoader;