Posts Context
Post
Cancel

Context

https://ko.legacy.reactjs.org/docs/context.html

Summary

React의 context를 이용하면 컴포넌트 트리 전체에 데이터를 제공할 수 있으며, props를 일일이 넘겨주지 않아도 됩니다. context를 사용하면 컴포넌트 트리 안에서 전역적인 데이터를 공유할 수 있습니다. context는 변하는 데이터를 여러 하위 컴포넌트에 효율적으로 전달하는 방법을 제공합니다.

Facts

  • 🌍 context는 React 컴포넌트 트리 안에서 전역적으로 데이터를 공유하는 방법입니다.
  • 📦 Context 객체는 React.createContext(defaultValue)를 통해 생성됩니다. defaultValue는 Provider가 값을 제공하지 않을 때 사용되는 값입니다.
  • 🔄 Provider 컴포넌트는 context를 구독하는 컴포넌트에게 context의 변화를 알리는 역할을 합니다. value prop을 통해 값을 전달합니다.
    • Provider 하위에 Provider가 있는 경우 하위 Provider의 값이 우선시 됨.
    • context를 구독하는 모든 컴포넌트는 Provider의 value prop이 변경될때마다 리렌더링됨. context 값이 변경되었는지 여부는  Object.is와 같은 알고리즘 사용
  • 👨‍💼 Class 컴포넌트에서 context를 이용하려면 Class.contextType을 지정하고 this.context를 사용하여 값을 읽을 수 있습니다.
  • 🔍 Context.Consumer는 context 변화를 구독하는 컴포넌트로 함수 컴포넌트 안에서 context를 구독할 수 있게 합니다.
    ```jsx
{value => /* context 값을 이용한 렌더링 */}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- 🔮 context 값을 변화시키려면 context를 관리하는 컴포넌트에서 state와 업데이트 메서드를 제공하여 하위 컴포넌트가 값을 업데이트할 수 있도록 합니다.  
- 🔄 여러 개의 context를 함께 사용할 때는 Context.Consumer를 중첩하여 사용하거나, render prop 컴포넌트를 활용할 수 있습니다.  
  
### 언제 context를 써야 할까  
- context는 주로 컴포넌트 트리 전체에 걸쳐서 변하는 데이터를 공유해야 할 때 사용됩니다.  
- context를 사용하면 컴포넌트를 재사용하기가 어려워질 수 있으므로, 신중하게 사용해야 합니다.  
- 컴포넌트 합성을 통해 props를 넘기는 것이 더 간단한 경우도 있습니다.  
- 복잡한 컴포넌트 트리에서 context는 중간 레벨에 있는 컴포넌트들에게 데이터를 전달할 때 유용합니다.  
- context를 사용하기 전에 컴포넌트 합성과 역전 제어 패턴을 고려해보세요.  
  
### API  
- React.createContext: Context 객체를 생성하며 defaultValue를 설정할 수 있습니다.  
- Context.Provider: context의 변화를 알리고 값을 하위 컴포넌트에 전달하는 역할을 합니다.  
- Class.contextType: Class 컴포넌트에서 context 값을 읽어올 수 있도록 해주는 프로퍼티입니다.  
- Context.Consumer: context 값을 구독하는 컴포넌트를 정의할 때 사용됩니다.  
- Context.displayName: 개발자 도구에서 context를 표시할 때 사용되는 이름입니다.  
  
### 주의사항  
- 예전 버전의 context API가 존재했으나, 새로운 API로 옮기는 것이 권장되며 예전 API는 삭제될 예정입니다.  
- context 값을 업데이트할 때 참조(reference)를 확인하여 불필요한 렌더링을 방지하세요. 부모의 state로 값을 끌어올려 사용할 수도 있습니다.
```js

// BAD
class App extends React.Component {
  render() {
    return (
      <MyContext.Provider value=>        
	      <Toolbar />
      </MyContext.Provider>
    );
  }
}

// GOOD
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},    
    };
  }

  render() {
    return (
      <MyContext.Provider value={this.state.value}>        
	      <Toolbar />
      </MyContext.Provider>
    );
  }
}

useContext

https://react.dev/reference/react/useContext

함수형 컴포넌트에서 context를 구독하는 방법

1
2
3
4
5
6
7
import { useContext } from 'react';  

  
function MyComponent() {  
	const theme = useContext(ThemeContext);  
	
	// ...
  • Parameter
    • SomeContext : createContext로 만든 Context.
  • Returns
    • SomeContext.Provider의 value props에 넣은 context value를 반환함.
    • Provider가 없으면 defaultValue에 넣은 값이 반환됨.
    • 이 반환되는 값은 항상 최신값이며, 변경시 리렌더링된다.
  • Caveats
    • 같은 컴포넌트에서 반환된 Provider의 영향을 받지 않는다. Provider는 항상 useContext()가 사용된 컴포넌트보다 상위에 있어야함
    • Object.is로 비교해서 값이 달라졌으면 리렌더링한다.
    • memo를 통해 리렌더링을 무시하는 것은 children이 새로운 context value를 받는 걸 막지 못한다.
    • 빌드시스템이 중복 모듈을 만들면 context가 꺠질 수 있다. context를 통해 무언가를 넘기는 것은 context를 제공하는 SomeContext와 구독할때 사용하는 SomeContext가 정확하게 같은 객체일때만 동작한다. (===으로 같아야함)

context updating example

  • Provider에서 업데이트 ```js import { createContext, useContext, useState } from ‘react’;

const ThemeContext = createContext(null);

export default function MyApp() { const [theme, setTheme] = useState(‘light’); return ( <ThemeContext.Provider value={theme}> <Form /> </ThemeContext.Provider> ) }

function Form({ children }) { return ( ); }

function Panel({ title, children }) { const theme = useContext(ThemeContext); const className = ‘panel-‘ + theme; return ( <section className={className}> <h1>{title}</h1> {children} </section> ) }

function Button({ children }) { const theme = useContext(ThemeContext); const className = ‘button-‘ + theme; return ( <button className={className}> {children} </button> ); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- `value`에 업데이트 함수 포함 

```js
import { createContext, useContext, useState } from 'react';

const CurrentUserContext = createContext(null);

export default function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <CurrentUserContext.Provider
      value=
    >
      <Form />
    </CurrentUserContext.Provider>
  );
}

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <LoginButton />
    </Panel>
  );
}

function Panel({ title, children }) {
  return (
    <section className="panel">
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function LoginButton() {
  const {
    currentUser,
    setCurrentUser
  } = useContext(CurrentUserContext);

  if (currentUser !== null) {
    return <p>You logged in as {currentUser.name}.</p>;
  }

  return (
    <Button onClick={() => {
      setCurrentUser({ name: 'Advika' })
    }}>Log in as Advika</Button>
  );
}


function Button({ children, onClick }) {
  return (
    <button className="button" onClick={onClick}>
      {children}
    </button>
  );
}

Optimizing re-renders when passing objects and functions

다음과 같이 value에 객체를 넣으면 MyApp이 리렌더링 될 때마다 새로운 객체가 만들어져, 이 context를 구독하고 있는 컴포넌트들도 리렌더링 되게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyApp() {  
	const [currentUser, setCurrentUser] = useState(null);  
	
	function login(response) {
		storeCredentials(response.credentials);  
		setCurrentUser(response.user);  
	}  

	return (  
		<AuthContext.Provider value=>  
			<Page />  
		</AuthContext.Provider>  
	);  
}

이를 최적화하기 위해서는 다음과같이 useCallbackuseMemo를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useCallback, useMemo } from 'react';  

function MyApp() {  
	const [currentUser, setCurrentUser] = useState(null);  

	const login = useCallback((response) => {  
		storeCredentials(response.credentials);  
		setCurrentUser(response.user);  
	}, []);  

	const contextValue = useMemo(() => ({  
		currentUser,  
		login  
	}), [currentUser, login]);  

	return (  
		<AuthContext.Provider value={contextValue}>  
			<Page />  
		</AuthContext.Provider>  
	);  
}

Context refactoring

context를 컴포넌트로 분리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);
const CurrentUserContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <MyProviders theme={theme} setTheme={setTheme}>
      <WelcomePanel />
      <label>
        <input
          type="checkbox"
          checked={theme === 'dark'}
          onChange={(e) => {
            setTheme(e.target.checked ? 'dark' : 'light')
          }}
        />
        Use dark mode
      </label>
    </MyProviders>
  );
}

function MyProviders({ children, theme, setTheme }) {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <ThemeContext.Provider value={theme}>
      <CurrentUserContext.Provider
        value=
      >
        {children}
      </CurrentUserContext.Provider>
    </ThemeContext.Provider>
  );
}

function WelcomePanel({ children }) {
  const {currentUser} = useContext(CurrentUserContext);
  return (
    <Panel title="Welcome">
      {currentUser !== null ?
        <Greeting /> :
        <LoginForm />
      }
    </Panel>
  );
}

context와 reducer 결합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// TasksContext.js
import { createContext, useContext, useReducer } from 'react';

const TasksContext = createContext(null);

const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const initialTasks = [
  { id: 0, text: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// AddTask.js
import { useState, useContext } from 'react';
import { useTasksDispatch } from './TasksContext.js';

export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useTasksDispatch();
  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        }); 
      }}>Add</button>
    </>
  );
}

let nextId = 3;

#review

This post is licensed under CC BY 4.0 by the author.