YeonjuOHYE / javascript

0 stars 0 forks source link

RTK #24

Open YeonjuOHYE opened 3 years ago

YeonjuOHYE commented 3 years ago

chapter 에서 다룰 내용

완성 코드 https://github.com/reduxjs/rtk-github-issues-example

예제 application

링크 : https://codesandbox.io/s/rtk-github-issues-example-01-plain-react-8jf6d?from-embed 프로젝트 구조 :

  • /api: fetching functions and TS types for the Github Issues API
  • /app: main component
  • /components: components that are reused in multiple places
  • /features
    • /issueDetails: components for the Issue Details page
    • /issuesList: components for the Issues List display
    • /repoSearch: components for the Repo Search form
  • /utils: various string utility functions

Redux Store Setting

🔗Add Redux Toolkit and React-Redux packages

Creating the Root Reducer

🔗Add store and root reducer with reducer HMR

Rootstate

ReturnType 을 이용한다.

import { combineReducers } from '@reduxjs/toolkit'

const rootReducer = combineReducers({})

export type RootState = ReturnType<typeof rootReducer>

export default rootReducer

※ 다른 방법은 Using configureStore with TypeScript 참고

Provider

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'

import store from './app/store'

import './index.css'

const render = () => {
  const App = require('./app/App').default

  ReactDOM.render(
    <Provider store={store}>
      <App />
    </Provider>,
    document.getElementById('root')
  )
}

render()

if (process.env.NODE_ENV === 'development' && module.hot) {
  module.hot.accept('./app/App', render)
}

Main App Display

어떤 state를 redux에 둘 것 인가

https://redux.js.org/faq/organizing-state#do-i-have-to-put-all-my-state-into-redux-should-i-ever-use-reacts-setstate

Initial State Slices

redux에 두기로한 state를 대상으로 types와 initial state을 작성한다. 여기서 부터 이들을 업데이트할 reducer를 정의 할 수 있다. 🔗Add initial state slice for UI display

State Contents Type Declarations

후에 action type이나 entire state에서 재사용을 위해 state type Declaration이 필요

Declaring Types for Slice State and Actions

createSlice 두 개의 sources 로 부터 타입을 추론

Converting the Issues Display

🔗Convert main issues display control to Redux

Converting the Issues List Page

 const [numIssues, setNumIssues] = useState<number>(-1)
  const [isLoading, setIsLoading] = useState<boolean>(false)
  const [issuesError, setIssuesError] = useState<Error | null>(null)

  const { issues, pageCount } = issuesResult

  useEffect(() => {
    async function fetchEverything() {
      async function fetchIssues() {
        const issuesResult = await getIssues(org, repo, page)
        setIssues(issuesResult)
      }

      async function fetchIssueCount() {
        const repoDetails = await getRepoDetails(org, repo)
        setNumIssues(repoDetails.open_issues_count)
      }

      try {
        await Promise.all([fetchIssues(), fetchIssueCount()])
        setIssuesError(null)
      } catch (err) {
        console.error(err)
        setIssuesError(err)
      } finally {
        setIsLoading(false)
      }
    }

    setIsLoading(true)

    fetchEverything()
  }, [org, repo, page])

useEffect에서 async function을 사용함으로써 useEffect의 return 값으로 Promise를 넘기고 이는 useEffect의 콜백인 cleanup 함수를 올바르게 사용할 수 없게 만든다.

Thinking in Thunks

What is a "Thunk"?

The Redux core (ie, createStore) 은 완벽하게 동기적이다. 따라서 비동기적 작동은 store 외부에서 발생해야 한다. 그러나 만약 비동기 로직을 store와 연동해서 작동하고 싶을 때 => Redux middleware

middleware는 store를 확장하여 다음의 기능을 제공

Redux store에 middleware 더함으로 function을 바로 store.dispath()에 pass the thunk middleware는 그 함수를 보고, 그것이 real store에 도달하지 못하도록 막고, 함수를 호출하고 그리고 dispatch와 getState를 인자로 넘김

function exampleThunkFunction(dispatch, getState) {
  // do something useful with dispatching or the store state here
}

// normally an error, but okay if the thunk middleware is added
store.dispatch(exampleThunkFunction)

////////////////using thunk action creator/////////////////

function exampleThunk() {
  return function exampleThunkFunction(dispatch, getState) {
    // do something useful with dispatching or the store state here
  }
}

// normally an error, but okay if the thunk middleware is added
store.dispatch(exampleThunk())

Why Use Thunks?

redux-saga, redux-observable은 좋은 framework이지만 기능을 다 사용하기 어려워 디폴트로 사용하기 좋다.

Writing Thunks in Redux Toolki

redux toolkit의 configureStore에서 자동으로 등록

RTK thunk function을 쓰는 어떤 syntax나 function 제공하지 않는다. 일반적으로 관련 action 파일이나 slice 파일 안에 위치한다.

Logic for Fetching Github Repo Details

Adding a Reusable Thunk Function Type

🔗Add AppThunk type

-import { configureStore } from '@reduxjs/toolkit'
+import { configureStore, Action } from '@reduxjs/toolkit'
+import { ThunkAction } from 'redux-thunk'
-import rootReducer from './rootReducer'
+import rootReducer, { RootState } from './rootReducer'
export type AppDispatch = typeof store.dispatch

+export type AppThunk = ThunkAction<void, RootState, unknown, Action<string>>

Adding the Repo Details Slice

🔗Add a slice for storing repo details

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

import { AppThunk } from 'app/store'

import { RepoDetails, getRepoDetails } from 'api/githubAPI'

interface RepoDetailsState {
  openIssuesCount: number
  error: string | null
}

const initialState = {
  openIssuesCount: -1,
  error: null
} as RepoDetailsState

const repoDetails = createSlice({
  name: 'repoDetails',
  initialState,
  reducers: {
    getRepoDetailsSuccess(state, action: PayloadAction<RepoDetails>) {
      state.openIssuesCount = action.payload.open_issues_count
      state.error = null
    },
    getRepoDetailsFailed(state, action: PayloadAction<string>) {
      state.openIssuesCount = -1
      state.error = action.payload
    }
  }
})

export const {
  getRepoDetailsSuccess,
  getRepoDetailsFailed
} = repoDetails.actions

export default repoDetails.reducer

export const fetchIssuesCount = (
  org: string,
  repo: string
): AppThunk => async dispatch => {
  try {
    const repoDetails = await getRepoDetails(org, repo)
    dispatch(getRepoDetailsSuccess(repoDetails))
  } catch (err) {
    dispatch(getRepoDetailsFailed(err.toString()))
  }
}

Async Error Handling Logic in Thunks

export const fetchIssuesCount = (
  org: string,
  repo: string
): AppThunk => async dispatch => {
  try {
    const repoDetails = await getRepoDetails(org, repo)
    dispatch(getRepoDetailsSuccess(repoDetails))
  } catch (err) {
    dispatch(getRepoDetailsFailed(err.toString()))
  }
}

위 코드의 try catch 문은 getRepoDetails와 dispatch(getRepoDetailsSuccess(repoDetails))의 에러를 모두 dispatch(getRepoDetailsFailed(err.toString()))로 처리하는 문제가 있어 다음과 같은 해결책을 생각해 볼 수 있다.

1) promise chain코드로 변경 후, success와 fail 콜백을 사용하도록

getRepoDetails(org, repo).then(
  // success callback
  repoDetails => dispatch(getRepoDetailsSuccess(repoDetails)),
  // error callback
  err => dispatch(getRepoDetailsFailed(err.toString()))
)

2) 코드의 위치 변경

 let repoDetails
  try {
    repoDetails = await getRepoDetails(org, repo)
  } catch (err) {
    dispatch(getRepoDetailsFailed(err.toString()))
    return
  }
  dispatch(getRepoDetailsSuccess(repoDetails))
}

Converting the Issue Details Page

Fetching the Issue Comments

-import React, { useState, useEffect } from 'react'
+import React, { useEffect } from 'react'
-import { useSelector, useDispatch } from 'react-redux'
+import { useSelector, useDispatch, shallowEqual } from 'react-redux'
import ReactMarkdown from 'react-markdown'
import classnames from 'classnames'

import { insertMentionLinks } from 'utils/stringUtils'
-import { getComments, Comment } from 'api/githubAPI'
import { IssueLabels } from 'components/IssueLabels'
import { RootState } from 'app/rootReducer'
import { fetchIssue } from 'features/issuesList/issuesSlice'

import { IssueMeta } from './IssueMeta'
import { IssueComments } from './IssueComments'
+import { fetchComments } from './commentsSlice'

export const IssueDetailsPage = ({
  org,
  repo,
  issueId,
  showIssuesList
}: IDProps) => {
- const [comments, setComments] = useState<Comment[]>([])
- const [commentsError] = useState<Error | null>(null)
-
  const dispatch = useDispatch()

  const issue = useSelector(
    (state: RootState) => state.issues.issuesByNumber[issueId]
  )

+ const { commentsLoading, commentsError, comments } = useSelector(
+   (state: RootState) => {
+     return {
+       commentsLoading: state.comments.loading,
+       commentsError: state.comments.error,
+       comments: state.comments.commentsByIssue[issueId]
+     }
+   },
+   shallowEqual
+ )

// omit effect
  useEffect(() => {
-   async function fetchComments() {
-     if (issue) {
-       const comments = await getComments(issue.comments_url)
-       setComments(comments)
-     }
-   }
-   fetchComments()
+   if (issue) {
+     dispatch(fetchComments(issue))
+   }
- }, [issue])
+ }, [issue, dispatch])

connect api와는 다르게 useSelector는 reference동일성을 기본으로 한다. 그래서 action이 dispatch 될 때마다 새로운 object를 반환하고 이는 컴포넌트가 rerender 되도록 한다.

이를 위한 해결방안은