React

[React] 디자인 패턴 - State Reducer Pattern

presentKey 2023. 3. 6. 22:19

참고 사이트


✨ State Reducer Pattern

장점: reducer는 모든 내부 action들에 접근하여 오버라이드할 수 있습니다. 

단점: reducer의 액션이 바뀔 수 있기 때문에, 컴포넌트 내부 로직에 대한 깊은 이해가 필요합니다.

 

https://javascript.plainenglish.io/5-advanced-react-patterns-a6b7624267a6


🚩 구현

1. context API를 통해서, Provider와 useCounterContext 정의

 

src/patterns/state-reducer/useCounterContext.jsx

import { createContext, useContext } from 'react';

const CounterContext = createContext(null);

export function CounterProvider({ children, value }) {
  return (
    <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
  );
}

export function useCounterContext() {
  return useContext(CounterContext);
}

 

2. Counter 컴포넌트와 useCounter, 자식 컴포넌트 구현

 

src/patterns/state-reducer/Counter.jsx

import Count from './components/Count';
import Decrement from './components/Decrement';
import Increment from './components/Increment';
import Label from './components/Label';
import { CounterProvider } from './useCounterContext';

export default function Counter({ children, value: num }) {
  return <CounterProvider value={{ num }}>{children}</CounterProvider>;
}

Counter.Decrement = Decrement;
Counter.Increment = Increment;
Counter.Label = Label;
Counter.Count = Count;

 

src/patterns/state-reducer/useCounter.jsx

import { useReducer } from 'react';

const internalReducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return {
        num: state.num + 1,
      };
    case 'decrement':
      return {
        num: state.num - 1,
      };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
};

export default function useCounter({ initialNum }, reducer = internalReducer) {
  const [{ num }, dispatch] = useReducer(reducer, {
    num: initialNum,
  });

  const incrementNum = () => {
    dispatch({ type: 'increment' });
  };

  const decrementNum = () => {
    dispatch({ type: 'decrement' });
  };

  return {
    num,
    incrementNum,
    decrementNum,
  };
}

useCounter.reducer = internalReducer;
useCounter.types = {
  increment: 'increment',
  decrement: 'decrement',
};

 

src/patterns/state-reducer/components/Count.jsx

src/patterns/state-reducer/components/Decrement.jsx

src/patterns/state-reducer/components/Increment.jsx

src/patterns/state-reducer/components/Label.jsx

import React from 'react';
import { useCounterContext } from '../useCounterContext';

export default function Count() {
  const { num } = useCounterContext();

  return <strong style={{ padding: '4px' }}>{num}</strong>;
}

 

import React from 'react';

export default function Decrement({ onClick }) {
  return (
    <button type="button" style={{ padding: '4px' }} onClick={onClick}>
      -
    </button>
  );
}

 

import React from 'react';

export default function Increment({ onClick }) {
  return (
    <button type="button" style={{ padding: '4px' }} onClick={onClick}>
      +
    </button>
  );
}

 

import React from 'react';

export default function Label({ children }) {
  return <span style={{ padding: '4px' }}>{children}</span>;
}

 

3. Cart 컴포넌트에서 action 오버라이드, Board 컴포넌트는 일반 action 사용

  • Cart 컴포넌트
    • 감소 버튼 클릭 시, -2 감소
  • Board 컴포넌트
    • 증가 버튼 +1, 감소 버튼 -1

src/patterns/state-reducer/Cart.jsx

import React from 'react';
import Counter from './Counter';
import useCounter from './useCounter';

export default function Cart() {
  const reducer = (state, action) => {
    switch (action.type) {
      case 'decrement':
        return {
          num: state.num - 2, // The decrement delta was changed for 2 (Default is 1)
        };
      default:
        return useCounter.reducer(state, action);
    }
  };

  const { num, incrementNum, decrementNum } = useCounter(
    { initialNum: 0 },
    reducer
  );

  return (
    <>
      <h2> Cart Component (-2 +1)</h2>
      <Counter value={num}>
        <Counter.Decrement onClick={decrementNum} />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment onClick={incrementNum} />
      </Counter>
    </>
  );
}

 

src/patterns/state-reducer/Board.jsx

import React from 'react';
import Counter from './Counter';
import useCounter from './useCounter';

export default function Board() {
  const { num, incrementNum, decrementNum } = useCounter({ initialNum: 0 });

  return (
    <>
      <h2> Board Component (+- 1)</h2>
      <Counter value={num}>
        <Counter.Decrement onClick={decrementNum} />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment onClick={incrementNum} />
      </Counter>
    </>
  );
}