import React from 'react';
import { SomethingContext } from 'some-context-package';
function YourComponent() {
const something = React.useContext(SomethingContext);
}
더 나은 유저 경험을 제공하기 위해, 다음과 같이 customhook을 이용하자.
import React from 'react';
import { useSomething } from 'some-context-package';
function YourComponent() {
const something = useSomething();
}
...
function useCountState() {
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCountState must be used within a CountProvider')
}
return context
}
function useCountDispatch() {
const context = React.useContext(CountDispatchContext)
if (context === undefined) {
throw new Error('useCountDispatch must be used within a CountProvider')
}
return context
}
export {CountProvider, useCountState, useCountDispatch}
useCountState와 useCountDispatch 커스텀 훅은 가장 가까운 CountProvider에게서 제공된 context value를 useContext를 사용한다. 그러나, 만약 값이 없다면 error message를 던질거다. 그러나 만약 값이 없다면, CountProvider 내부에서 렌더된 컴포넌트아니라고 도움이 되는 error message를 던지자. 도움이 될만한 에러 메시지를 제공하자.(#FailFast)
Typescript
TS를 이용할 때 defaultValue를 스킵하는 이슈를 피할 수 있는 방법을 알아보자.
// src/count-context.tsx
import * as React from 'react'
type Action = {type: 'increment'} | {type: 'decrement'}
type Dispatch = (action: Action) => void
type State = {count: number}
type CountProviderProps = {children: React.ReactNode}
<!-- ------------------------------ 1 ------------------------------- -->
const CountStateContext = React.createContext<State | undefined>(undefined)
const CountDispatchContext = React.createContext<Dispatch | undefined>(
undefined,
)
function countReducer(state: State, action: Action) {
switch (action.type) {
case 'increment': {
return {count: state.count + 1}
}
case 'decrement': {
return {count: state.count - 1}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}: CountProviderProps) {
<!-- ------------------------------ 2 ------------------------------- -->
const [state, dispatch] = React.useReducer(countReducer, {count: 0})
return (
<CountStateContext.Provider value={state}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
function useCountState() {
<!-- ------------------------------ 3 ------------------------------- -->
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCountState must be used within a CountProvider')
}
return context
}
function useCountDispatch() {
const context = React.useContext(CountDispatchContext)
if (context === undefined) {
throw new Error('useCountDispatch must be used within a CountProvider')
}
return context
}
export {CountProvider, useCountState, useCountDispatch}
context는 undefined로 생성될 수 있지만(1), 초기화할 때 값을 넣어줘서(2) 문제를 해결하자.
What about async actions?
비동기 요청을 해야하는 상황이고 해당 요청 과정에서 여러개를 dispatch해야 하는 경우 어떻게 될까? 물론 컴포넌트에서 구현할 수 있지만, 그렇게 하면 모든 컴포넌트에 수동으로 모두 작업해줘야한다.
// user-profile.js
import { useUserState, useUserDispatch, updateUser } from './user-context';
function UserSettings() {
const { user, status, error } = useUserState();
const userDispatch = useUserDispatch();
function handleSubmit(event) {
event.preventDefault();
updateUser(userDispatch, user, formState);
}
// more code...
}
state and dispatch separation is annoying
const state = useCountState();
const dispatch = useCountDispatch();
// verbose하다면 아래와 같이 바꾸자.
function useCount() {
return [useCountState(), useCountDispatch()];
}
conclusion
CountContext를 export하지말자. 컨텍스트 값을 제공하는 것과 consume할 수 있는 것을 딱 하나씩 expose 하자. 사람들이 상황에 맞는 방식으로 컨텍스트 값을 사용하고, consumer에게 유용한 유틸리티를 제공할 수 있다.