JaeYeopHan / tip-archive

📦 Archiving various development tips. If you watch this repository, you can get issues related to the newly registered development tip from the GitHub feed.
https://www.facebook.com/Jbee.dev/
245 stars 8 forks source link

Custom hooks - useFetchDispatch #59

Open JaeYeopHan opened 4 years ago

JaeYeopHan commented 4 years ago

Description

useEffect는 dependency list에 걸려있는 값이 변경될 때 실행된다. 또한 처음 렌더링 될 때도 실행된다. 즉 class component에서의 componentDidMount, componentWillUnmount 그리고 componentDidUpdate lifecycle에서 실행시킬 함수를 callback으로 넘겨받는 API이다.

데이터를 fetch하는 action을 컴포넌트의 useEffect안에서 dispatch 한다고 가정해보자. (즉 saga에서 초기 호출을 하지 않는다.) 그리고 그 데이터가 pagination이 적용되어 있어서 asyncAction.SUCCESS가 호출될 때마다 concat된다고 가정하자.

[asyncAction.SUCCESS]: (
  state: IState,
  action: Action<IResponse>,
) => {
  const { dataset } = action.payload

  return {
    ...state,
    ...action.payload,
    dataset: state.dataset.concat(dataset),
  }
},

Problem

route의 변경 또는 /detail 페이지로 진입했다가 뒤로 돌아가기를 진행할 경우, page number가 변경되지 않았음에도 불구하고 useEffect가 componentDidMount 시점에 호출되기 때문에 중복된 데이터가 redux에 concat된다. 이를 해결할 수 있는 방법은 두 가지가 있을 것 같다.

Solution 1. Save as pagination data

dataByPage: {
  [PAGE_NUMBER]: { ... }
}

서버로부터 전달되는 response를 바로 redux에 저장하지 않고 PAGE_NUMBER(호출한 page number)를 key로 저장한 후,

const dataset = Object.values(dataByPage).reduce((prev, next) => prev.concat(next), [])

이렇게 flat하게 만들어 사용하면 중복된 데이터가 저장되는 것을 피할 수 있다. 데이터가 fetch되는 것은 그대로이니 불필요한 네트워크 요청이 발생하게 된다는 단점이 존재한다. 또한 redux에 view에서는 사용하지 않는 중간 단계의 상태가 존재하고 매번 비용이 발생하기 때문에 좋지 않다.

(이 page 기준으로 저장된 상태값을 다른 곳에서 사용할 일이 없을 것이라고 판단.)

Solution 2. Custom hook

useEffectuseRef를 사용하여 custom hook을 만든다. 그리고 componentDidMount 시점에는 특정 조건 하에서만 호출되도록 분기를 태워준다.

import { useRef, useEffect } from 'react'
import { Procedure } from '../utils/eventUtils'

interface IUseFetchDispatchOption {
  componentDidUpdateCondition?: boolean
  componentDidMountCondition?: boolean
}

export const useFetchDispatch = (
  effectFunction: Procedure,
  deps: any[],
  option: IUseFetchDispatchOption,
) => {
  const didMountRef = useRef(false)
  const { componentDidUpdateCondition = true, componentDidMountCondition = true } = option

  useEffect(() => {
    if (!didMountRef.current) {
      didMountRef.current = true
      if (componentDidMountCondition) {
        effectFunction()
      }
      return
    }

    if (componentDidUpdateCondition) {
      effectFunction()
    }
  }, deps) //eslint-disable-line
}