본문 바로가기

React

[React] useContext

개요.

- 리액트에서 데이터의 흐름은 부모 컴포넌트에서 자식에게 인자(props)로 전달하는 맥락이 원칙이다.

- 이러한 기본적인 맥락 외에 새로운 맥락을 생성할 수 있게 해주는 방법이 Context API이다.

- Context Api는 props drilling을 회피하기 위한 것이 본래 목적이다.

- 즉, 의존성을 함수형 컴포넌트에 인자가 아닌 함수 내부에서의 호출로 주입 받는다.

 

 

1.  새 컨텍스트 생성하기

import { createContext } from 'react';

export const ThemeContext = createContext(defaultValue);

-컨텍스트 이름은 대문자로 시작한다.

-컨텍스트 자체는 어떤 정보도 포함하지 않는다.

-defaultValue는 이 context에 접근할 수 없을 때 반환되는 값이다.

 

 

2. 컨텍스트 props 내려주기

function App() {
  // ...
  return (
    <ThemeContext.Provider value={theme}>
      <Todo />
    </ThemeContext.Provider>
  );
}

// 위는 아래와 같이 생각 할 수 있다. '컴포넌트 합성(composition)'
function App() {
  // ...
  return (
    <ThemeContext.Provider children={<Todo />} value={theme} />
  );
}

-컨텍스트 객체 안에는 Provider라는 컴포넌트(ThemeContext.Provider)가 들어있다.

-이 컴포넌트에 공유하고자 하는 value를 props로 전달하고, 이 value에 접근할 다른 컴포넌트들을 합성시켜준다.

 

 

3. 컨텍스트 접근하기

function Todo() {
  return (
    <Form>
      <Button />
    </Form>
  );
}
// ...

function Button() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{backgroundColor: theme}}>제출하기</button>
  );
}

-useContext(ThemeContext)는 현재 컴포넌트에서 가장 가까운 ThemeContext.Provider에 전달된 value를 반환한다.

-useContext(ThemeContext)를 호출하는 컴포넌트(Button)는 반드시 자신 혹은 부모 컴포넌트(Todo)가 ThemeContext.Provider 컴포넌트에 wrapping되어야 한다.

- 컴포넌트 최상단에서 useContext를 호출하게 되면 이 컴포넌트는 useContext에 인자로 전달된 context를 subscribe한다.

 

 

4. 컨텍스트 value 변경하기

function App() {
  const [theme, setTheme] = useState('dark');

  return (
    <ThemeContext.Provider value={theme}>
      <Todo />
    </ThemeContext.Provider>
  );
}

-프로바이더에 전달한 value가 바뀌면 이 컨텍스트를 구독 중인 (useContext(ThemeContext)를 호출하여 해당 value를 참조하는) 모든 컴포넌트는 re-render된다.

-useContext가 반환한 값은 사실 부모 컴포넌트(App)에서 내려준 props(value:theme)와 동일하다.

-value로 state를 전달하면 setState가 실행될 때마다 프로바이더를 리턴하는 함수(App)가 재실행되면서 value 값도 바뀌게 되고, 리액트는 이를 참조하는 모든 컴포넌트를 리렌더링한다.

  -> "The returned value is always up-to-date. React automatically re-renders components that read some context if it changes."

 

 

5. Context Provider Container 컴포넌트와 useContext  Hook 만들기

-ThemeProvider 컴포넌트와 같이 context value를 처리하는 로직을 담당하는 container 컴포넌트를 만든다.

-Context를 여러 컴포넌트에서 사용된다면 커스텀 Hook으로 만들어 관리한다.

import { createContext, useContext } from 'react';

const ThemeContext = createContext(defaultValue);
const useThemeContext = () => {
  const value = useContext(ThemeContext);
  if(value === defaultValue) throw new Error('useThemeContext should be used within ThemeProvider')
  return value;
}

function ThemeProvider ({ children }) {
  const [theme, setTheme] = useState('dark');
  
  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
}

function App() {
  return (
    <ThemeProvider>
      <Todo />
    </ThemeProvider>
  );
}

function Todo() {
  return (
    <Form>
      <Button />
    </Form>
  );
}

function Button() {
  const theme = useThemeContext();
  
  return (
    <button style={{backgroundColor: theme}}>제출하기</button>
  );
}

-*참고: https://codesandbox.io/s/context-advanced-etst2w?file=/src/App.js 

 

 

6. Context  렌더링 최적화

-리액트 메모이제이션 훅 사용하기 + 역할에 따라 여러 Context 분리하기

import { createContext, useContext, useMemo, useRef, useState } from 'react';

const TodosValueContext = createContext();
const TodosActionsContext = createContext();

function TodosProvider({ children }) {
  const idRef = useRef(3);
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: '밥먹기',
      done: true
    },
    {
      id: 2,
      text: '잠자기',
      done: false
    }
  ]);

  const actions = useMemo(
    () => ({
      add(text) {
        const id = idRef.current;
        idRef.current += 1;
        setTodos((prev) => [
          ...prev,
          {
            id,
            text,
            done: false
          }
        ]);
      },
      toggle(id) {
        setTodos((prev) =>
          prev.map((item) =>
            item.id === id
              ? {
                  ...item,
                  done: !item.done
                }
              : item
          )
        );
      },
      remove(id) {
        setTodos((prev) => prev.filter((item) => item.id !== id));
      }
    }),
    []
  );

  return (
    <TodosActionsContext.Provider value={actions}>
      <TodosValueContext.Provider value={todos}>
        {children}
      </TodosValueContext.Provider>
    </TodosActionsContext.Provider>
  );
}

function useTodosValue() {
  const value = useContext(TodosValueContext);
  if (value === undefined) {
    throw new Error('useTodosValue should be used within TodosProvider');
  }
  return value;
}

function useTodosActions() {
  const value = useContext(TodosActionsContext);
  if (value === undefined) {
    throw new Error('useTodosActions should be used within TodosProvider');
  }
  return value;
}

 

 

7. useReducer를 대체하여 update 함수 만들기

-위 예시의 actions 객체처럼 업데이트 함수들이 들어있는 객체를 Provider 컴포넌트에서 바로 선언하여 context로 공유한다.

-다음과 같이 사용할 수 있다.

// 할 일 항목을 추가할 때
const { add } = useTodosActions();

const handleSubmit = () => {
  add(text);
}

// 각 항목을 보여줄 때
const { toggle, remove } = useTodosActions();

const handleToggle = () => {
  toggle(id);
};

const handleRemove = () => {
  remove(id);
};

* useReducer를 활용하여 상태 업데이트를 할 수 있게 구현하고 dispatch를 별도의 context로 넣어주는 방식도 많이 사용한다.

  -> https://kentcdodds.com/blog/how-to-use-react-context-effectively

  -useReducer의 장점: 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다. 

  ( *useReducer 사용방법: https://velog.io/@velopert/react-hooks )

*참고: https://velog.io/@velopert/react-context-tutorial

 

 

'React' 카테고리의 다른 글

[React] useReducer  (0) 2023.11.09
[React] Portal  (0) 2023.07.04
[React] Recoil  (0) 2023.03.21
[React] React-Query  (0) 2022.11.14
[React] Redux Toolkit & Middleware  (0) 2022.10.31