Hooks & Context

 

Hooks NEW in React 16.8

컴포넌트 간 통신

Context API

Lead Software Engineer @ProtoPie

Microsoft MVP

TypeScript Korea User Group Organizer

Marktube (Youtube)

Mark Lee

이 웅재

Basic Hooks

useState

useEffect

useContext (Context API 애서 다룹니다.)

npx create-react-app react-hooks-example
import React from 'react';

export default class Example1 extends React.Component {
  state = {
    count: 0,
  };

  render() {
    const { count } = this.state;
    return (
      <div>
        <p>You clicked {count} times</p>
        <button onClick={this.click}>Click me</button>
      </div>
    );
  }

  click = () => {
    this.setState({ count: this.state.count + 1 });
  };
}

this.setState

import React, { useState } from 'react';

const Example2 = () => {
  const [count, setCount] = useState(0);

  function click() {
    setCount(count + 1);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={click}>Click me</button>
    </div>
  );
};

export default Example2;

const [count, setCount] = useState(0);

const [스테이트 값, 스테이트 변경 함수] = useState(스테이트 초기값);

import React, { useState } from 'react';

const Example3 = () => {
  const [state, setState] = useState({ count: 0 });

  function click() {
    setState({ count: state.count + 1 });
  }

  return (
    <div>
      <p>You clicked {state.count} times</p>
      <button onClick={click}>Click me</button>
    </div>
  );
};

export default Example3;

const [state, setState] = useState({count: 0});

const [스테이트 값, 스테이트 변경 함수] = useState(스테이트 초기값);

Functional Component

= Stateless Component

= Stateless Functional Component

Functional Component

!= Stateless Component

 

because state hook

  • 컴포넌트 사이에서 상태와 관련된 로직을 재사용하기 어렵습니다.

    • ​컨테이너 방식 말고, 상태와 관련된 로직

  • 복잡한 컴포넌트들은 이해하기 어렵습니다.

  • Class 는 사람과 기계를 혼동시킵니다.

    • ​컴파일 단계에서 코드를 최적화하기 어렵게 만든다.

  • ​this.state 는 로직에서 레퍼런스를 공유하기 때문에 문제가 발생할 수 있다.

    • ​좋은 것일까 ?

  • useState

    • state 를 대체 할 수 있다.

  • useEffect

    • 라이프 사이클 훅을 대체 할 수 있다.

      • componentDidMount

      • componentDidUpdate

      • componentWillUnmount

import React from 'react';

export default class Example4 extends React.Component {
  state = { count: 0 };
  componentDidMount() {
    console.log('componentDidMount', this.state.count);
  }

  componentDidUpdate() {
    console.log('componentDidUpdate', this.state.count);
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <p>You clicked {count} times</p>
        <button onClick={this.click}>Click me</button>
      </div>
    );
  }

  click = () => {
    this.setState({ count: this.state.count + 1 });
  };
}

componentDidMount, componentDidUpdate

import React, { useState, useEffect } from 'react';

const Example5 = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('componentDidMount & componentDidUpdate', count);
  });

  function click() {
    setCount(count + 1);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={click}>Click me</button>
    </div>
  );
};

export default Example5;

useEffect(() => {});

import React from 'react';

export default class Example6 extends React.Component {
  state = {
    time: new Date(),
  };
  _timer = null;

  componentDidMount() {
    this._timer = setInterval(() => {
      this.setState({ time: new Date() });
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this._timer);
  }

  render() {
    const { time } = this.state;
    return <div>{time.toISOString()}</div>;
  }
}

componentWillUnmount

import React, { useState, useEffect } from 'react';

const Example7 = () => {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);

  return <div>{time.toISOString()}</div>;
};

export default Example7;

useEffect(() => {return () => {}, [])

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  
  function click() {
    setCount(count + 1);
  }
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={click}>Click me</button>
    </div>
  );
}

useEffect 자세히 알아보기

첫 번째 랜더링 (1)

from 리액트

"컴포넌트야,

state가 0 일 때의 UI를 보여줘."

첫 번째 랜더링 (2)

from 컴포넌트

  • "여기 랜더링 결과물로 <p>You clicked 0 times</p> 가 있어."
  • "그리고 모든 처리가 끝나고 이 이펙트를 실행하는 것을 잊지 마."
    () => { document.title = 'You clicked 0 times' }

첫 번째 랜더링 (3)

from 리액트

"좋아. UI를 업데이트 하겠어.
이봐 브라우저, 나 DOM에 뭘 좀 추가하려고 해."

첫 번째 랜더링 (4)

from 브라우저

"좋아, 화면에 그려줄게."

첫 번째 랜더링 (5)

리액트

좋아, 이제 컴포넌트 컴포넌트가 준 이펙트를 실행할거야.

() => { document.title = 'You clicked 0 times' } 를 실행.

클릭 후, 랜더링 (1)

from 컴포넌트

"이봐 리액트, 내 상태를 1 로 변경해줘."

클릭 후, 랜더링 (2)

from 리액트

"상태가 1 일때의 UI를 줘."

클릭 후, 랜더링 (3)

from 컴포넌트

  • "여기 랜더링 결과물로 <p>You clicked 1 times</p> 가 있어."

  • "그리고 모든 처리가 끝나고 이 이펙트를 실행하는 것을 잊지 마."
    () => { document.title = 'You clicked 1 times' }.

클릭 후, 랜더링 (4)

from 리액트

"좋아. UI를 업데이트 하겠어.
이봐 브라우저, 나 DOM에 뭘 좀 추가하려고 해."

 

클릭 후, 랜더링 (5)

from 브라우저

"좋아, 화면에 그려줄게."

클릭 후, 랜더링 (6)

리액트

좋아, 이제 컴포넌트 컴포넌트가 준 이펙트를 실행할거야.

() => { document.title = 'You clicked 1 times' } 를 실행.

Custom Hooks

useSomething

// hooks/useWindowWidth.js

import { useState, useEffect } from 'react';

export default function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', onResize);
    return () => {
      window.removeEventLister('resize', onResize);
    };
  }, []);

  return width;
}

나만의 훅 만들기

useHasMounted vs withHasMounted

// hocs/withHasMounted.js

import React from 'react';

export default function withHasMounted(Component) {
  class WrapperComponent extends React.Component {
    state = {
      hasMounted: false,
    };
    componentDidMount() {
      this.setState({
        hasMounted: true,
      });
    }
    render() {
      const { hasMounted } = this.state;
      return <Component {...this.props} hasMounted={hasMounted} />;
    }
  }

  WrapperComponent.displayName = `withHasMounted(${Component.name})`;

  return WrapperComponent;
}

withHasMounted (HOC)

// hooks/useHasMounted.js

import { useState, useEffect } from 'react';

export default function useHasMounted() {
  const [hasMounted, setHasMounted] = useState(false);
  useEffect(() => {
    setHasMounted(true);
  }, []);

  return hasMounted;
}

useHasMounted (Custom Hook)

Additional Hooks

useReducer

useCallback, useMemo

useRef, useImperativeHandle,

useLayoutEffect

useDebugValue

useReducer

  • 다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우

  • 다음 state가 이전 state에 의존적인 경우

  • Redux 를 안다면 쉽게 사용 가능

import React, { useReducer, useEffect } from 'react';

const Example8 = ({ count }) => {
  const [state, dispatch] = useReducer(reducer, { count });
  useEffect(() => {
    setTimeout(() => {
      dispatch({ type: 'PLUS' });
    }, 2000);
  }, []);

  function click() {
    dispatch({ type: 'PLUS' });
  }

  return (
    <div>
      <p>You clicked {state.count} times</p>
      <button onClick={click}>Click me</button>
    </div>
  );
};

export default Example8;

const [state, dispatch] = useReducer(reducer, initialState, init?)

const reducer = (state, action) => {
  if (action.type === 'PLUS') {
    return {
      count: state.count + 1,
    };
  }
  return state;
};
import React, { useState } from 'react';

function sum(persons) {
  console.log('sum...');
  return persons.map(person => person.age).reduce((l, r) => l + r, 0);
}

const Example9 = () => {
  const [value, setValue] = useState('');
  const [persons] = useState([{ name: 'Mark', age: 38 }, { name: 'Hanna', age: 27 }]);

  function change(e) {
    setValue(e.target.value);
  }

  const count = sum(persons);

  return (
    <div>
      <input value={value} onChange={change} />
      <p>{count}</p>
    </div>
  );
};

export default Example9;

실제로 변하지 않더라도, 매번 실행되는 어떤 값

import React, { useState, useMemo } from 'react';

function sum(persons) {
  console.log('sum...');
  return persons.map(person => person.age).reduce((l, r) => l + r, 0);
}

const Example9 = () => {
  const [value, setValue] = useState('');
  const [persons] = useState([{ name: 'Mark', age: 38 }, { name: 'Hanna', age: 27 }]);

  function change(e) {
    setValue(e.target.value);
  }

  const count = useMemo(() => sum(persons), [persons]);

  return (
    <div>
      <input value={value} onChange={change} />
      <p>{count}</p>
    </div>
  );
};

export default Example9;

const 디펜던시 변경 없으면 고정 = useMemo(함수, 디펜던시)

import React, { useState } from 'react';

const Example10 = () => {
  const [value, setValue] = useState('');
  const [persons, setPersons] = useState([
    { id: 0, name: 'Mark', age: 38 },
    { id: 1, name: 'Hanna', age: 27 },
  ]);

  function change(e) {
    setValue(e.target.value);
  }

  function click(id) {
    setPersons(
      persons => persons.map(person =>
        person.id === id
          ? { ...person, age: person.age + 1 }
          : { ...person },
      )
    );
  }

  return (
    <div>
      <input value={value} onChange={change} />
      {persons.map(person => (
        <Person {...person} key={person.id} click={click} />
      ))}
    </div>
  );
};

export default Example10;

함수가 매번 새로 생성되는 경우, 최적화의 어려움 (feat. React.memo)

const Person = React.memo(({ id, name, age, click }) => {
  console.log('Person...');
  function onClick() {
    click(id);
  }
  return (
    <div>
      {name}, {age} <button onClick={onClick}>+</button>
    </div>
  );
});
import React, { useState } from 'react';

const Example10 = () => {
  const [value, setValue] = useState('');
  const [persons, setPersons] = useState([
    { id: 0, name: 'Mark', age: 38 },
    { id: 1, name: 'Hanna', age: 27 },
  ]);

  function change(e) {
    setValue(e.target.value);
  }

  const click = useCallback(id => {
    setPersons(persons => {
      return persons.map(person =>
        person.id === id
          ? {
              ...person,
              age: person.age + 1,
            }
          : {
              ...person,
            },
      );
    });
  }, []);

  return (
    <div>
      <input value={value} onChange={change} />
      {persons.map(person => (
        <Person {...person} key={person.id} click={click} />
      ))}
    </div>
  );
};

export default Example10;

useCallback

const Person = React.memo(({ id, name, age, click }) => {
  console.log('Person...');
  function onClick() {
    click(id);
  }
  return (
    <div>
      {name}, {age} <button onClick={onClick}>+</button>
    </div>
  );
});
import React, { useRef, useEffect, useState } from 'react';

const Example11 = () => {
  const [count, setCount] = useState(0);
  const inputCreateRef = React.createRef();
  const inputUseRef = useRef();
  console.log(inputCreateRef.current);
  console.log(inputUseRef.current);
  useEffect(() => {
    setTimeout(() => {
      setCount(count => count + 1);
    }, 1000);
  });
  return (
    <div>
      <p>{count}</p>
      <input ref={inputCreateRef} />
      <input ref={inputUseRef} />
    </div>
  );
};

export default Example11;

createRef vs useRef

컴포넌트 간 통신

npx create-react-app component-communication

하위 컴포넌트를 변경하기

A 의 button 를 클릭하여 E 를 변경하려면

  1. <A /> 컴포넌트에서 button 에 onClick 이벤트를 만들고,

  2. button 을 클릭하면, <A /> 의 state 를 변경하여, <B /> 로 내려주는 props 를 변경

  3. <B /> 의 props 가 변경되면, <C /> 의 props 에 전달

  4. <C /> 의 props 가 변경되면, <D /> 의 props 로 전달

  5. <D /> 의 props 가 변경되면, <E /> 의 props 로 전달

// A 컴포넌트
<div>
    <B />
    <button>클릭</button>
</div>
// B 컴포넌트
<div>
    <C />
</div>
// C 컴포넌트
<div>
    <D />
</div>
// D 컴포넌트
<div>
    <E />
</div>
// E 컴포넌트
<div>
    {props.value}
</div>
import React from "react";

class A extends React.Component {
  state = {
    value: "아직 안바뀜"
  };

  render() {
    console.log("A render");
    return (
      <div>
        <B {...this.state} />
        <button onClick={this._click}>E 의 값을 바꾸기</button>
      </div>
    );
  }

  _click = () => {
    this.setState({
      value: "E 의 값을 변경"
    });
  };
}

export default A;
const B = props => (
  <div>
    <p>여긴 B</p>
    <C {...props} />
  </div>
);

const C = props => (
  <div>
    <p>여긴 C</p>
    <D {...props} />
  </div>
);

const D = props => (
  <div>
    <p>여긴 D</p>
    <E {...props} />
  </div>
);

const E = props => (
  <div>
    <p>여긴 E</p>
    <h3>{props.value}</h3>
  </div>
);

A 의 button 를 클릭하여 E 를 변경하려면

상위 컴포넌트를 변경하기

  1. <A /> 에 함수를 만들고, 그 함수 안에 state 를 변경하도록 구현, 그 변경으로 인해 p 안의 내용을 변경.

  2. 만들어진 함수를 props 에 넣어서, <B /> 로 전달

  3. <B /> 의 props 의 함수를 <C /> 의 props 로 전달

  4. <C /> 의 props 의 함수를 <D /> 의 props 로 전달

  5. <D /> 의 Props 의 함수를 <E /> 의 props 로 전달, <E /> 에서 클릭하면 props 로 받은 함수를 실행

E 의 button 를 클릭하여 A 의 p 를 변경하려면

// A 컴포넌트
<div>
    <B />
    <p>{state.value}</p>
</div>
// B 컴포넌트
<div>
    <C />
</div>
// C 컴포넌트
<div>
    <D />
</div>
// D 컴포넌트
<div>
    <E />
</div>
// E 컴포넌트
<div>
    <button>클릭</button>
</div>
import React from "react";

class A extends React.Component {
  state = {
    value: "아직 안바뀜"
  };

  render() {
    console.log("A render");
    return (
      <div>
        <h3>{this.state.value}</h3>
        <B change={this.change} />
      </div>
    );
  }

  change = () => {
    this.setState({
      value: "A 의 값을 변경"
    });
  };
}

export default A;

E 의 button 를 클릭하여 A 의 p 를 변경하려면

const B = props => (
  <div>
    <p>여긴 B</p>
    <C {...props} />
  </div>
);

const C = props => (
  <div>
    <p>여긴 C</p>
    <D {...props} />
  </div>
);

const D = props => (
  <div>
    <p>여긴 D</p>
    <E {...props} />
  </div>
);

const E = props => {
  function click() {
    props.change();
  }
  return (
    <div>
      <p>여긴 E</p>
      <button onClick={click}>클릭</button>
    </div>
  );
};

Component - Communication

Context API

useContext

npx create-react-app react-context-example

하위 컴포넌트 전체에 데이터를 공유하는 법

  • 데이터를 Set 하는 놈

    • 가장 상위 컴포넌트 => 프로바이더

  • 데이터를 Get 하는 놈

    • 모든 하위 컴포넌트에서 접근 가능

      • ​컨슈머로 하는 방법

      • 클래스 컴포넌트의 this.context 로 하는 방법

      • 펑셔널 컴포넌트의 useContext 로 하는 방법

데이터를 Set 하기

  1. 일단 컨텍스트를 생성한다.

  2. 컨텍스트.프로바이더 를 사용한다.

  3. ​value 를 사용

import React from 'react';

const PersonContext = React.createContext();

export default PersonContext;
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import PersonContext from './contexts/PersonContext';

const persons = [
  { id: 0, name: 'Mark', age: 38 },
  { id: 1, name: 'Hanna', age: 27 },
];

ReactDOM.render(
  <PersonContext.Provider value={persons}>
    <App />
  </PersonContext.Provider>,
  document.getElementById('root'),
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

데이터를 Get 하기 (1) - Consumer

  1. 컨텍스트를 가져온다.

  2. 컨텍스트.컨슈머를 사용한다.

  3. ​value 를 사용

import React from 'react';
import PersonContext from '../contexts/PersonContext';

const Example1 = () => (
  <PersonContext.Consumer>
    {value => <ul>{JSON.stringify(value)}</ul>}
  </PersonContext.Consumer>
);

export default Example1;
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Example1 from './components/Example1';

export default function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <Example1 />
      </header>
    </div>
  );
}

데이터를 Get 하기 (2) - class

  1. static contextType 에 컨텍스트를 설정한다.

  2. this.context => value 이다.

import React from 'react';
import PersonContext from '../contexts/PersonContext';

export default class Example2 extends React.Component {
  static contextType = PersonContext;

  render() {
    return <ul>{JSON.stringify(this.context)}</ul>;
  }
}

// Example2.contextType = PersonContext;
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Example1 from './components/Example1';
import Example2 from './components/Example2';

export default function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <Example1 />
        <Example2 />
      </header>
    </div>
  );
}

데이터를 Get 하기 (3) - functional

  1. useContext 로 컨텍스트를 인자로 호출한다.

  2. useContext 의 리턴이 value 이다.

import React, { useContext } from 'react';
import PersonContext from '../contexts/PersonContext';

const Example3 = () => {
  const value = useContext(PersonContext);

  return <ul>{JSON.stringify(value)}</ul>;
};

export default Example3;
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Example1 from './components/Example1';
import Example2 from './components/Example2';
import Example3 from './components/Example3';

export default function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <Example1 />
        <Example2 />
        <Example3 />
      </header>
    </div>
  );
}

7. Hooks & Context

By Woongjae Lee

7. Hooks & Context

Fast Campus Frontend Developer School 17th

  • 1,510