React

[React] 디자인 패턴 - Props Getters Pattern

presentKey 2023. 3. 6. 02:43

참고한 사이트


✨ Props Getters Pattern

장점: props 오버로딩을 통해서 부모 -> 자식, 자식 -> 부모 양방향 커뮤니케이션이 가능해집니다.

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

단점: props를 재정의 하기위해선, 노출된 props과 내부 로직을 제대로 이해하고 있어야 합니다.

 

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


🚩 구현

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

 

src/patterns/props-getters/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/props-getters/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/props-getters/useCounter.jsx

import { useState } from 'react';

// Function which concat all functions togeter
const callFnsInSequence =
  (...fns) =>
  (...args) =>
    fns.forEach((fn) => fn && fn(...args));

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

  const incrementNum = () => setNum((prev) => prev + 1);
  const decrementNum = () => setNum((prev) => prev - 1);

  // props getter for 'Counter'
  const getCounterProps = ({ ...otherProps } = {}) => ({
    value: num,
    'aria-valuemax': max,
    'aria-valuenow': num,
    ...otherProps,
  });

  // props getter for 'Increment'
  const getIncrementProps = ({ onClick, ...otherProps } = {}) => ({
    onClick: callFnsInSequence(incrementNum, onClick),
    disabled: num === max,
    ...otherProps,
  });

  // props getter for 'Decrement'
  const getDecrementProps = ({ onClick, ...otherProps } = {}) => ({
    onClick: callFnsInSequence(decrementNum, onClick),
    disabled: num === 0,
    ...otherProps,
  });

  return {
    num,
    incrementNum,
    decrementNum,
    getCounterProps,
    getIncrementProps,
    getDecrementProps,
  };
}

🔔 [JavaScript 문법] { ... } = { }, 기본 값이 할당된 Destructured parameter

 

 

src/patterns/props-getters/components/Count.jsx

src/patterns/props-getters/components/Decrement.jsx

src/patterns/props-getters/components/Increment.jsx

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

 

import React from 'react';

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

 

import React from 'react';

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

 

3. Cart 컴포넌트에서 노출된 props 사용 및 props 재정의

  • 노출된 props 이용
  • {...getIncrementProps({ onClick: handleBtn1Click })} 
    • 1을 더하고 alert
  • {...getIncrementProps({ disabled: num >= MAX_COUNT - 2 })} 
    • 8 까지만 1을 더하고, disabled

src/patterns/props-getters/Cart.jsx

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

const MAX_COUNT = 10;

export default function Cart() {
  const { num, getCounterProps, getIncrementProps, getDecrementProps } =
    useCounter({ initialNum: 0, max: MAX_COUNT });

  const handleBtn1Clicked = () => alert('btn1 clicked');

  return (
    <>
      <Counter {...getCounterProps()}>
        <Counter.Decrement {...getDecrementProps()} />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment {...getIncrementProps()} />
      </Counter>
      <br />
      <button
        type="button"
        {...getIncrementProps({ onClick: handleBtn1Clicked })}
      >
        1을 더하고 alert
      </button>
      <br />
      <button {...getIncrementProps({ disabled: num >= MAX_COUNT - 2 })}>
        count: 8 까지만 1을 더하고, disabled
      </button>
    </>
  );
}