React

[React] 디자인 패턴 - Custom Hook Pattern

presentKey 2023. 3. 5. 20:26

참고한 사이트


✨ Custom Hook Pattern

장점: Custom Hook은 states, handlers를 노출하여 많은 제어 권한을 가질 수 있습니다.

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

단점: 로직과 UI가 분리되어 있어서, 올바르게 사용하기 위해선 컴포넌트가 어떻게 동작하는지 잘 이해해야합니다.

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

 

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


🚩 구현

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

 

src/patterns/custom-hooks/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(custom hook), 자식 컴포넌트 구현

 

src/patterns/custom-hooks/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/custom-hooks/useCounter.jsx

import { useState } from 'react';

export default function useCounter(initialNum) {
  const [num, setNum] = useState(initialNum);

  const incrementNum = () => setNum((prev) => prev + 1);
  const decrementNum = () => setNum((prev) => prev - 1);
  return { num, incrementNum, decrementNum };
}

 

src/patterns/custom-hooks/components/Count.jsx

src/patterns/custom-hooks/components/Decrement.jsx

src/patterns/custom-hooks/components/Increment.jsx

src/patterns/custom-hooks/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, disabled }) {
  return (
    <button
      type="button"
      style={{ padding: '4px' }}
      onClick={onClick}
      disabled={disabled}
    >
      -
    </button>
  );
}

 

import React from 'react';

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

 

import React from 'react';

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

 

3. Cart 컴포넌트에서 custom logic 사용, Board 컴포넌트에서 일반 logic 사용

  • Cart 컴포넌트
    • count의 초기 num: 0
    • 0 일때, 감소 버튼 disabled
    • 10 일때, 증가 버튼 disabled
  • Board 컴포넌트
    • count의 초기 num: 5
    • 증감범위 제한 없음

src/patterns/custom-hooks/Cart.jsx

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

export default function Cart() {
  const { num, incrementNum, decrementNum } = useCounter(0);
  const MAX_COUNT = 10;

  const handleClickIncrement = () => {
    // Put your custom logic
    if (num < MAX_COUNT) {
      incrementNum();
    }
  };

  return (
    <>
      <h1>Cart Component</h1>
      <Counter value={num}>
        <Counter.Decrement onClick={decrementNum} disabled={num === 0} />
        <Counter.Label>Custom increment Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment
          onClick={handleClickIncrement}
          disabled={num === MAX_COUNT}
        />
      </Counter>
    </>
  );
}

 

src/patterns/custom-hooks/Board.jsx

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

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

  return (
    <>
      <h1>Board Component</h1>
      <Counter value={num}>
        <Counter.Decrement onClick={decrementNum} />
        <Counter.Label>Basic Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment onClick={incrementNum} />
      </Counter>
    </>
  );
}