redux 대신 useReducer 후크로 복잡한 UI 상태를 관리하는 방법

가장 인기 있는 프론트엔드 라이브러리인 React를 사용하여 UI를 빌드하는 사람이라면 UI 업데이트를 트리거하는 props와 state에 대해 분명히 들었을 것입니다. 상태는 로컬 상태(동기) 또는 네트워크 상태(비동기)일 수 있습니다.

React에서 상태를 관리하는 것은 항상 redux , mobx , recoil 와 같은 많은 라이브러리와 함께 문제가 되었고 목록은 계속됩니다. 어떻게 활용할 수 있는지 설명하겠습니다. 앱에 추가 종속성을 추가하지 않고 앱의 번들 크기를 줄입니다.

React를 꽤 오랫동안 사용하고 있다면(적어도 React 16.3부터) 복잡한 UI 상태를 관리하기 위해 가장 인기 있는 라이브러리 Redux 중 하나에 대해 들어봤을 것입니다. redux-thunk 및 redux-saga 라이브러리.

redux에서 미들웨어로 사용하고 redux 기능을 확장할 수 있는 라이브러리가 많이 있습니다. redux를 처음부터 설정하는 경우 작업을 시작하기 전에 상용구 코드를 설정해야 합니다. 최신 버전의 redux는 일부 상용구를 줄이기 위해 후크 기반 API를 제공하지만 여전히 작업, 감속기, 미들웨어 등에 대해 알아야 합니다.

최신 React 또는 React 16.8 이상을 사용하고 있다면 이미 react에 도입된 가장 인기 있는 기능 중 하나인 hooks 을 사용 중일 수 있습니다. 후크를 사용하면 클래스를 작성하지 않고 구성 요소를 작성하고 반응 앱의 상태를 쉽게 관리할 수 있습니다.

이 게시물에서는 useEffect, useMemo, useRef, useState와 같은 다른 후크의 도움으로 useReducer 후크를 사용하여 redux를 사용하지 않고 복잡한 UI 상태를 관리하는 방법을 설명합니다. 이 게시물은 후크의 기본 사항과 사용 방법을 모두 알고 있다고 가정합니다. 전에 사용하지 않았다면 시작하기 위한 공식 문서를 읽는 것이 좋습니다.

관심 분야에 따라 라이브러리를 추가, 삭제 및 관리할 수 있는 간단한 책 라이브러리 CRUD 앱을 구축한다고 가정해 보겠습니다. 저는 이 예제를 보여주기 위해 redux와 함께 널리 사용되는 React UI 패턴container, and presentational components pattern 중 하나를 사용하고 있습니다. 이것은 이미 사용 중인 모든 패턴에 맞을 수 있습니다.
books-container.js
import React, {useReducer, useMemo, useEffect, useRef} from 'react'
import _ from 'lodash'
import BooksLayout from './books-layout'

// Extract this to utils file, can be reused in many places
// Same as that of redux's bindActionCreators method
const bindActionCreators = (reducerMap, dispatch) =>
  _.reduce(
    reducerMap,
    (result, val, type) => ({
      ...result,
      [type]: payload => dispatch({type, payload}),
    }),
    {}
  )

// Initial state of the app
const initialState = {
  books: {}, 
  // To keep track of progress of a API call and to show the 
  // progress in the UI
  bookReadState: null
  bookDeleteState: null
  bookUpdateState: null
}

const reducerMap = {
  setBooks: (state, books) => ({
    ...state,
    books,
  }),
  updateBook: (state, book) => ({
    ...state,
    books: // merge state.books with updated book details
  },
  deleteBook: (state, book) => ({
    ...state,
    books: // update the state.books with deleted book
  }),
  setBookReadState: (state, bookReadState) => ({
    ...state, bookReadState
  }),
  setBookUpdateState: (state, bookUpdateState) => ({
    ...state, bookUpdateState
  }),
  setBookDeleteState: (state, bookDeleteState) => ({
    ...state, bookDeleteState
  }),
}

const useService = ({id, actions}) => {
  // abortController can be used to abort the one or more request
  // when required, can also be used to abort when multiple requests are made
  // within a short period, so that you don't make multiple requests
  const abortController = useRef(new global.AbortController())

  actions = useMemo(
    () => ({
      ...actions,
      readBooks: async () => {
        try {
          const data = await readBooks({
            fetchCallback: actions.setBookReadState})
          actions.setBooks(data)
        } catch(error) {
          // error handling
        }
      },
      updateBook: async book => {
        try { 
          const data = await updateBook({book, 
            fetchCallback: actions.setBookUpdateState})
          actions.updateBook(data)
        } catch(error) {
          // error handling
        }
      },
      deleteBook: async id => {
        try {
          const data = await deleteBook({id, 
            fetchCallback: actions.setDeleteReadState})
          actions.deleteBook(data)
        } catch {
          // error handling
        }
      },
    }),
    [actions]
  )

  useEffect(() => {
    const controller = abortController.current
    // Invoke the actions required for the initial app to load in the useEffect.
    // Here I'm reading the books on first render
    actions.readBooks()

    return () => {
      controller.current.abort()
    }
  }, [actions])

  return {actions}
}

const reducer = (state, {type, payload}) => reducerMap[type](state, payload)

const BooksContainer = props => {
  const [state, dispatch] = useReducer(reducer, initialState)
  const actions = useMemo(() => bindActionCreators(reducerMap, dispatch), [])
  const service = useService({...props, state, actions})

  return (
    <BooksLayout
      {...state}
      {...service}
      {...props}
    />
  )
}

export default BooksContainer
books-layout.js
import React from 'react'

const BooksLayout = ({books, actions, bookReadState, ...props}) => {
  return (
    <>
    {bookReadState === 'loading' ? <div>Loading...</div> : 
      {books.map(book => (
          // UI Logic to display an each book
          // button to click to delete 
          // call actions.deleteBook(id)
          )
        )
      }
    }
    </>
  )
}

export default BooksLayout

위의 예에서 볼 수 있듯이 컨테이너에서 앱의 상태를 제어할 수 있으며 redux에서 해야 하는 것처럼 상태를 각 구성 요소에 별도로 연결하는 것에 대해 걱정할 필요가 없습니다.

위의 예에서는 데모 목적으로 모든 코드를 단일 파일에 보관했으며 코드의 일부가 완전하지 않았습니다. 필요에 따라 네트워크 호출, 비즈니스 로직 및 UI 로직에 대한 추상화로 코드를 교체하십시오. DRY(Don't Repeat Yourself) 원칙에서 제안하는 것처럼 앱 전체에서 더 많은 재사용성에 대한 요구 사항에 따라 논리를 분리하여 이 코드를 개선할 수 있습니다.

Redux는 글로벌 스토어가 있는 복잡한 앱에 적합하고 확장성이 뛰어납니다. 이 기사에서는 redux 대신 useReducer를 활용하여 더 적은 코드로 전역 상태 관리를 달성하고 앱에 새 패키지를 추가하는 것에 대해 걱정할 필요가 없으며 번들 크기를 줄일 수 있는 방법을 설명하려고 합니다. 크게.

댓글을 남겨주시고 저를 팔로우 하시면 더 많은 기사를 보실 수 있습니다.

좋은 웹페이지 즐겨찾기