beecomci / today_i_learned

0 stars 0 forks source link

[React-Basic-Hooks] 7. React Hooks #11

Open beecomci opened 3 years ago

beecomci commented 3 years ago

React Hooks #

소개: React Conf 2018 release: React v16.8

What are Hooks

[참고] Motivation #

props

// 순수 함수 : 입력값을 바꾸지 않고 항상 동일한 입력값에 대해 동일한 결과 반환
function sum(a, b) {
  return a + b;
}

// 입력값을 변경하기 때문에 순수 함수가 아님
function withdraw(account, amount) {
  account.total -= amount;
}

state

📌 State Hook

// useState : array로 반환
// state : state 값, setState : state를 update 할 수 있는 함수 
const [state, setState] = useState(initialState);
// update 할 newState를 전달해서 state update
setState(newState);

React가 화면을 update하는 3가지 방법

@06-hooks/state

import { useState} from 'react';

function Count() {
  // initialState : 0 
  const [count, setCount] = useState(0); 

  const increase = () => {
    setCount(count + 1);
  }

  return (
    <div className="set-state">
      <div>Current Count: {count}</div>
      <p>
        <button onClick={increase}>increase</button>
      </p>
    </div>
  )
}

Functional updates

// 값이 아니라 함수 전달 
setState((previousState) => nextState)

@06-hooks/functional-state-update

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

// 1. 만약 중간에 로직이 복잡해서 여러번 setState()를 호출하게 되는 경우 (억지로 만든 예시)

// 2. 컴포넌트가 render가 될 때, state 값을 매번 가져오는데 
// setCount에서의 count는 컴포넌트 내의 count state를 바라봐서 마지막 state으로만 override 됨
const increase = () => {
  setCount(count + 1);
  setCount(count + 1);
};

// 3. 함수가 체이닝으로 이어지기 때문에 컴포넌트 내의 state 값을 가져오는게 아니라 setCount가 알고 있는 이전 값을 가져올 수 있음
const increaseFunctional = () => {
  setCount(prevCount => { prevCount + 1 });
  setCount(prevCount => { prevCount + 1 });
};
// 클래스 컴포넌트의 setState 메서드와는 다르게, useState는 갱신 객체(update objects)를 자동으로 합치지는 않음
// 함수 업데이터 폼을 객체 전개 연산자와 결합함으로써 이 동작을 복제 가능
const [state, setState] = useState({});

setState(prevState => {
  // Object.assign would also work
  return {...prevState, ...updatedValues};
});
// Component를 맨 처음 render 할 때 1번만 실행됨
// 그 다음 setState가 호출되면 Component가 매번 호출되지만, 넘긴 함수는 memoization이 되어서 다시 실행 X 
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

@06-hooks/lazy-initial-state

function someExpensiveComputation(props) {
  console.log('expensive!!!!!!!!!!!');
  const initialState = 0;
  return initialState;
}

function LazyInitialState(props) {
  // AS-IS
  const initialState = someExpensiveComputation(props);
  const [state, setState] = useState(initialState);

  // TO-BE
  const [state, setState] = useState(() => someExpensiveComputation(props));

  return (
    <div>
      Current Count: {state}
      <p>
        <button onClick={() => setState(state + 1)}>
          UPDATE
        </button>
      </p>
    </div>
  );
}

⚡️ Effect hook

Effect?

Side effect (함수형 컴포넌트가 render외에 하는 다른 일)

React's purely functional world => imperative world (명령형)

useEffect

// didUpdate : 컴포넌트가 업데이트 된 다음에 실행되는 함수
// dependency : 이 값이 변경이 될 때만 effect 실행해달라 (최적화 연관), 없으면 비교대상이 없으니 항상 didUpdate (지양)
useEffect(didUpdate, [dependency]);

@06-hooks/effect

unction Example() {
  console.log('1. component render start!');

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

  // render 된 다음에 useEffect로 넘긴 didUpdate 함수가 실행됨
  // state가 변경되면서 전체적으로 useEffect와 render가 매번 실행됨
  // dependency를 count로 전달해서 count state가 변경될 때만 실행되도록
  // useEffect는 if, for 문 등에 들어가면 안되고 항상 상위에 위치해야하는 React Rule
  // - 안지키면 React Hook은 항상 반드시 같은 순서로 실행이 보장되어야 한다 라는 오류가 뜸
  useEffect(() => {
    // if (count < 10) 일때 useEffect를 감싸는 구조 X, useEffect 안에서 조건문 처리 해야함
    console.log('3. effect start!');
    if (count > 10) return undefined;

    // 직접 dom을 제어하는 부분이라 useEffect에서 처리
    document.title = `You clicked ${count} times`;
    console.log('title updated');

    // 초기화 함수
    // useEffect 실행하기 전에 이전 컴포넌트에서 등록된 useEffect의 clear 함수가 실행됨
    return () => {
      console.log('4. effect clear!');
      document.title = 'clear!';
    }
  }, [count]);

  // React Component는 Virtaul DOM만을 제어하고 실제 DOM을 제어하는 건 side effect로 판단되어 useEffect에 처리
  console.log('2. Virtual DOM render!');
  return (
    <div>
      <button onClick={() => setToggleCounter((prev) => !prev)}>
        Toggle counter
      </button>
      {toggleCounter && (
        <>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </>
      )}
    </div>
  );
}

@06-hooks/clock

const Clock = () => {
  const [time, setTime] = useState(() => new Date().toLocaleTimeString());

  useEffect(() => {
    const timer = setTimeout(() => setTime(new Date().toLocaleTimeString()), 1000);

    // clearTimeout 해주지 않으면 Clock 컴포넌트가 빠지는 경우(뒤로가기) 1초 뒤에 실행이 될 때 이미 unmount된 컴포넌트에 state를 접근하는 경우 에러 발생
    return(() => {clearTimeout(timer)});
  }, [time]);

  render(
    <div>
    <h1>current time: {time}</h1>
  </div>
  )
};

✌️ Rules of Hooks

  1. Only Call Hooks at the Top Level
    • if, for 문 등에 포함되면 안됨, Hook이 실행이 안될수도 있음
    • Hook에 등록한 useState, useEffect가 함수형 컴포넌트 외에 Hook만을 관리하는 별도 메모리에 배열로 저장되는 구조
    • 매번 render가 다시 되더라도 등록한 Hook이 유지됨
    • Hook은 예외처리가 되면 안되고 top level에서 호출되어야 배열 안에서 순서가 꼬이지 않음
  2. Only Call Hooks from React Functions

[필수] eslint-plugin-react-hooks 추가하여 lint 도움 받기

[참고] lint 도움을 받기 위해 custom hook 들은 use prefix를 붙인다.

💡 Building Your Own Hooks

custom hooks

이런 경우 사용

@06-hooks/fetch/data-fetch

import { useState, useEffect } from 'react';
import fetch from './fetch';
import mock from './mockData';

const usersApi = 'https://jsonplaceholder.typicode.com/users';

// Custom Hook
// 배열을 반환하는 fetch일 경우 이 Hook 재사용 가능
const useArrayFetch = () => {
  const [data, setData] = useState([]);
  const hasData = data.length > 0;

  useEffect(() => {
    // useEffect는 async를 지원하지 않기 때문에 아래처럼 즉시 실행하는 형태로 사용
    (async () => {
      // 비동기 처리인 fetch는 useEffect에서 주로 처리
      const fetchedData = await fetch(usersApi);

      setData(fetchedData);
    })();
  }, []); // 빈 배열 넘겨서 컴포넌트가 mount될 때 1번만 호출됨

  return { data, hasData };
};

const Users = () => {
  // 이 Users 컴포넌트는 View render만 집중, 보통 Custom Hook은 다른 파일에 따로 보관
  const { data, hasData } = useArrayFetch();

  return (
    <div>
      {/* 사용자 친화적 UX */}
      {!hasData && (<div> Loading... </div>)}
      {hasData && data.map(user => {
        const { id, name, email } = user;
        return (
          <p key={id}>{name} : <i>{email}</i></p>
        )
      })}
    </div>
  )
}

Hooks API #

Basic Hooks

const [state, setState] = useState(initialState);
setState(newState); // 컴포넌트가 re-render
// sideEffect가 발생하는 경우 사용
// react의 pure function에서 넘어가서 imperative 영역으로 넘어가는 경우 사용 (fetch, timer, logging, dom 제거 etc)
// class 컴포넌트의 didUpdate의 기능과 비슷 
// dependency를 넣으면 각각 이전 값과 비교해서(값들은 값 비교, array나 object같은 reference를 가진 객체는 reference 비교, Object.is 비교) 바뀐 경우만  update
useEffect(didUpdate, [dependency]);
const value = useContext(MyContext);

Additional Hooks

const [state, dispatch] = useReducer(reducer, initialArg, init);
// 함수를 저장
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
// 값(함수의 반환값) 자체를 저장
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const refContainer = useRef(initialValue);

const value = refContainer.current
useImperativeHandle(ref, createHandle, [deps])
useLayoutEffect(didUpdate, [dependency]);
useDebugValue(value);

Hooks Summary

When to useMemo and useCallback #

📝 props vs. state

React Component가 render 되는 때/시점

dom을 실제로 update하는 건 react이기 때문에 react가 언제 dom을 update하는지 그 시점을 잘 알고 있으면 좋음

props와 state 구별

#cheatsheet

cheatsheet