참고한 사이트
✨ Control Props Pattern
장점: 모든 상태들을 제어 및 소유하고 있는 특정 컴포넌트가 있기 때문에, custom logic을 삽입할 수 있습니다.
단점: 구현이 복잡해 질 수 있습니다.

🚩 구현
1. context API를 통해서, Provider와 useCounterContext 정의
src/patterns/control-props/useCounterContext.jsx
import { createContext, useContext } from 'react';
const CounterContext = createContext(null);
export function CounterProvider({ children, value }) {
return (
<CounterContext.Provider value={value}>{children}</CounterContext.Provider>
);
}
// 자식 컴포넌트에서 state와 handler 공유
export function useCounterContext() {
return useContext(CounterContext);
}
2. 제어 컴포넌트(Counter)에서 외부 state를 사용할 지, 내부 state를 사용할 지 제어하는 로직 작성
src/patterns/control-props/Counter.jsx
import { useState } from 'react';
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 = null, onChangeCounter }) {
const [num, setNum] = useState(0);
// value와 onChangeCounter가 전달되면, inConstrolled = true (외부 state)
const isControlled = value !== null && !!onChangeCounter;
// 전달받은 value를 사용할 지, 내부 num을 사용할 지 결정
const getCount = () => (isControlled ? value : num);
const handleCountChange = (newValue) =>
isControlled ? onChangeCounter(newValue) : setNum(newValue);
const incrementNum = () => handleCountChange(getCount() + 1);
const decrementNum = () => handleCountChange(getCount() - 1);
return (
<CounterProvider value={{ num: getCount(), incrementNum, decrementNum }}>
{children}
</CounterProvider>
);
}
Counter.Decrement = Decrement;
Counter.Increment = Increment;
Counter.Label = Label;
Counter.Count = Count;
src/patterns/control-props/components/Count.jsx
src/patterns/control-props/components/Decrement.jsx
src/patterns/control-props/components/Increment.jsx
src/patterns/control-props/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';
import { useCounterContext } from '../useCounterContext';
export default function Decrement() {
const { decrementNum } = useCounterContext();
return (
<button type="button" style={{ padding: '4px' }} onClick={decrementNum}>
-
</button>
);
}
import React from 'react';
import { useCounterContext } from '../useCounterContext';
export default function Increment() {
const { incrementNum } = useCounterContext();
return (
<button type="button" style={{ padding: '4px' }} onClick={incrementNum}>
+
</button>
);
}
import React from 'react';
export default function Label({ children }) {
return <span style={{ padding: '4px' }}>{children}</span>;
}
3. custom logic 삽입 및 Counter 컴포넌트 사용

- 외부 state - Counter
- count의 초기 num: 500
- 증감범위는 500 ~ 505
- 내부 state - Counter
- count의 초기 num: 0
- 증감범위 제한 없음
src/patterns/control-props/Cart.jsx
import React, { useState } from 'react';
import Counter from './Counter';
export default function Cart() {
const [num, setNum] = useState(500);
const handleChangeCounter = (newNum) => {
if (newNum >= 500 && newNum <= 505) {
setNum(newNum);
}
};
return (
<>
<Counter value={num} onChangeCounter={handleChangeCounter}>
<Counter.Decrement />
<Counter.Label>외부 state - Counter</Counter.Label>
<Counter.Count />
<Counter.Increment />
</Counter>
<hr />
<Counter>
<Counter.Decrement />
<Counter.Increment />
<Counter.Label>내부 state - Counter</Counter.Label>
<Counter.Count />
</Counter>
</>
);
}
참고한 사이트
✨ Control Props Pattern
장점: 모든 상태들을 제어 및 소유하고 있는 특정 컴포넌트가 있기 때문에, custom logic을 삽입할 수 있습니다.
단점: 구현이 복잡해 질 수 있습니다.

🚩 구현
1. context API를 통해서, Provider와 useCounterContext 정의
src/patterns/control-props/useCounterContext.jsx
import { createContext, useContext } from 'react';
const CounterContext = createContext(null);
export function CounterProvider({ children, value }) {
return (
<CounterContext.Provider value={value}>{children}</CounterContext.Provider>
);
}
// 자식 컴포넌트에서 state와 handler 공유
export function useCounterContext() {
return useContext(CounterContext);
}
2. 제어 컴포넌트(Counter)에서 외부 state를 사용할 지, 내부 state를 사용할 지 제어하는 로직 작성
src/patterns/control-props/Counter.jsx
import { useState } from 'react';
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 = null, onChangeCounter }) {
const [num, setNum] = useState(0);
// value와 onChangeCounter가 전달되면, inConstrolled = true (외부 state)
const isControlled = value !== null && !!onChangeCounter;
// 전달받은 value를 사용할 지, 내부 num을 사용할 지 결정
const getCount = () => (isControlled ? value : num);
const handleCountChange = (newValue) =>
isControlled ? onChangeCounter(newValue) : setNum(newValue);
const incrementNum = () => handleCountChange(getCount() + 1);
const decrementNum = () => handleCountChange(getCount() - 1);
return (
<CounterProvider value={{ num: getCount(), incrementNum, decrementNum }}>
{children}
</CounterProvider>
);
}
Counter.Decrement = Decrement;
Counter.Increment = Increment;
Counter.Label = Label;
Counter.Count = Count;
src/patterns/control-props/components/Count.jsx
src/patterns/control-props/components/Decrement.jsx
src/patterns/control-props/components/Increment.jsx
src/patterns/control-props/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';
import { useCounterContext } from '../useCounterContext';
export default function Decrement() {
const { decrementNum } = useCounterContext();
return (
<button type="button" style={{ padding: '4px' }} onClick={decrementNum}>
-
</button>
);
}
import React from 'react';
import { useCounterContext } from '../useCounterContext';
export default function Increment() {
const { incrementNum } = useCounterContext();
return (
<button type="button" style={{ padding: '4px' }} onClick={incrementNum}>
+
</button>
);
}
import React from 'react';
export default function Label({ children }) {
return <span style={{ padding: '4px' }}>{children}</span>;
}
3. custom logic 삽입 및 Counter 컴포넌트 사용

- 외부 state - Counter
- count의 초기 num: 500
- 증감범위는 500 ~ 505
- 내부 state - Counter
- count의 초기 num: 0
- 증감범위 제한 없음
src/patterns/control-props/Cart.jsx
import React, { useState } from 'react';
import Counter from './Counter';
export default function Cart() {
const [num, setNum] = useState(500);
const handleChangeCounter = (newNum) => {
if (newNum >= 500 && newNum <= 505) {
setNum(newNum);
}
};
return (
<>
<Counter value={num} onChangeCounter={handleChangeCounter}>
<Counter.Decrement />
<Counter.Label>외부 state - Counter</Counter.Label>
<Counter.Count />
<Counter.Increment />
</Counter>
<hr />
<Counter>
<Counter.Decrement />
<Counter.Increment />
<Counter.Label>내부 state - Counter</Counter.Label>
<Counter.Count />
</Counter>
</>
);
}