USEEFFECT

2.0 Introduction to useEffect

  • componentWillUnmount, componentDidMount, componentWillUpdate와 비슷
    이 모든 것이 같은 function. react hooks로 작업할 때 이 function이 중요하다.

  • useEffect는 2개의 인자를 받는데 첫번째는 function으로서의 effect. componentDidUpdate와 기능이 비슷하다.

useEffect는 componentDidMount 역할을 해서 새로고침 시 sayHello()가 실행된다.
그렇지만 componentDidUpdate 역할도 해서 클릭 시 sayHello() 실행

useEffect(() => {
  sayHello();
});

useEffect(sayHello); // 위에 코드를 이렇게 변경 가능
  • 두번째 인자는 dependency의 deps. 만약 deps가 있다면 effect는 (deps) 리스트에 있는 값일 때만 값이 변하도록 활성화 될 것이다.

useEffect()가 deps 리스트에 있는 값이 변할때만 실행되게 한다.
componentWillUpdate와 기능이 비슷하다.

만약 component가 mount 됐을 때 실행시키고 그 이후 어떤 경우에도 실행시키고 싶지 않다면 두번째 인자에 [] 빈 dependency를 전달해 주면 된다.

  • sayHello는 useEffect로부터 function이 리턴된다. = componentWillUnmount
import React, { useEffect, useState } from 'react';
import './styles.css';

const App = () => {
  const [number, setNumber] = useState(0);
  const [aNumber, setAnumber] = useState(0);

  // component가 mount 되자마자 sayHello라는 function 실행
  const sayHello = () => console.log('hello');
  // useEffect(() => {
  //   sayHello();
  // });

  useEffect(sayHello, [number]); // 위에 코드를 이렇게 변경 가능
  // 두번째 인자에 빈배열을 만들고 여기에 값이 존재하면 그 값은 변할거고 그리고 나서 useEffect는 활성화 될 것
  // 우리는 이 예제에서 number가 변할때만 sayHello가 작동하도록 할 것. > [number]
  // 그리고 새로고침이 되면 component가 mount 되어 hello console을 볼 수 있다.
  // 위 코드에 [number]로 해 뒀기때문에 number 값이 변경될 때마다 console을 불러오는 걸 볼 수 있다. 이게 기본적인 dependency

  return (
    <div className="App">
      <div>Hi</div>
      <button onClick={() => setNumber(number + 1)}>{number}</button>
      <button onClick={() => setAnumber(aNumber + 1)}>{aNumber}</button>
    </div>
  );
};

export default App;

2.1 useTitle

  • 문서의 제목을 업데이트 시켜주는 Hooks
    보통 이걸 하기위해서 helmet을 사용하는데 우리는 functional hooks 방식으로 만들어보자
import React, { useEffect, useState } from 'react';
import './styles.css';

const useTitle = (initialTitle) => {
  const [title, setTitle] = useState(initialTitle);

  const updateTitle = () => {
    const htmlTitle = document.querySelector('title'); // <title> 태그
    htmlTitle.innerText = title;
  };
  useEffect(updateTitle, [title]);
  return setTitle;
};

const App = () => {
  const titleUpdater = useTitle('Loading...');
  // titleUpdater = setTitle() 와 같은 의미
  setTimeout(() => titleUpdater('Home'), 5000); // 5s
  return (
    <div className="App">
      <div>Hi</div>
    </div>
  );
};

export default App;

Q. titleUpdater는 함수가 아닌데 어떻게 (“Home”) 값을 넣어주는가?
A. titleUpdater라는 변수에 setTitle()의 리턴값을 줘서 App 내부 자신이 원하는 시점(위치)에서 titleUpdater(“Home”) 이런식으로 호출하면 사용할 수 있는 것

2.2 useClick

useClick을 보기 전에 reference를 알아보고 진행

  • reference : 기본적으로 우리의 component의 어떤 부부을 선택할 수 있는 방법
    = document.getElementById()와 같다.

useRef

원래 useEffect 밖에서 focus()를 했는데 mount가 너무 빨리되어서 potato.current가 극초반에 존재하지 않아 오류(undefined) 발생.
그래서 아래와 같이 useEffect 안에서 동작시키거나 setTimeout(() => potato.current?.focus(), 5000); 로 수정할 수 있다.

++ Optional Chaining(?.)

import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const App = () => {
  const potato = useRef();
  // setTimeout(() => console.log(potato.current), 5000);

  useEffect(() => {
    setTimeout(() => potato.current.focus(), 5000);
  });

  return (
    <div className="App">
      <div>Hi</div>
      <input ref={potato} placeholder="la" />
    </div>
  );
};

export default App;

useClick

import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const useClick = (onClick) => {
  const element = useRef(); // reference 생성
  useEffect(() => {
    if (typeof onClick !== 'function') {
      return;
    }
    // componentDidMount, componentDidUpdate
    if (element.current) {
      // reference 안에 element.current가 있는지 확인
      // 조건 만족 시 Click 이벤트 부여
      element.current.addEventListener('click', onClick);
    }
    return () => {
      // componentWillUnMount
      // function을 return 할 경우 useEffect를 return 받은 그 함수는 componentWillUnMount일 경우에 호출
      // component가 mount 되지 않았을때 eventListener 배치되지 않게 하기 위함
      if (element.current) {
        element.current.removeEventListener('click', onClick);
      }
    };
  }, []); // no dependency. dependency가 존재할 경우(또는 []가 없는 default 상태) 이 함수는 componentDidMount일 경우에만 호출
  // return element;
  return typeof onClick !== 'function' ? element : undefined;
  /**
   * react 16.8v부터 Hook을 조건문, 반복문, 중첩함수 내에서 호출 불가능
   * 강의 내용(typeof로 막는 부분이 원래 useClick 바로 하단에 있었음)과 같은 결과를 얻으려면
   * useEffect 내에서 이벤트 바인딩을 막고, 최종적으로 undefined를 리턴해야한다.
   * 단순히 이벤트 바인딩만 막으려면 return element를 그대로 사용해도 괜찮다.
   */
};

const App = () => {
  const sayHello = () => console.log('say hello');
  const title = useClick(sayHello);
  return (
    <div className="App">
      <h1 ref={title}>Hi</h1>
    </div>
  );
};

export default App;

2.3 useConfirm & usePreventLeave

이번에 생성할 2가지 Hook은 useEffect와 useState를 사용하지 않기 때문에 사실상 Hook은 아니다.

useConfirm

  • 사용자가 버튼을 클릭하면 (이벤트 실행하기 전에) 메시지를 보여준다.
import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const useConfirm = (message = '', onConfirm, onCancel) => {
  if (!onConfirm && typeof onConfirm !== 'function') {
    return;
  }

  // onCancel은 필수적인게 아니라서 있든 없든 상관 없지만, 있다면 function이여야한다.
  if (onCancel && typeof onCancel !== 'function') {
    return;
  }

  const confirmAction = () => {
    // confirm function이 browser에 message를 가지고 있다면.
    // ok 클릭 시 confirm(message) 값이 true가 돼서 callback 호출
    if (window.confirm(message)) {
      onConfirm();
    } else {
      onCancel();
    }
  };
  return confirmAction;
};

const App = () => {
  const deleteWorld = () => console.log('deleting the world');
  const abort = () => console.log('Aborted');

  const confirmDelete = useConfirm('are you sure', deleteWorld, abort);
  return (
    <div className="App">
      <button onClick={confirmDelete}>Delete the world</button>
    </div>
  );
};

export default App;

usePreventLeave

  • 창을 닫을 때 “아직 저장하지 않았어!”라고 알려주는 것
  • protect를 클릭할 경우 window가 beforeunload이기 때문에 창 닫을 때 “변경사항이 저장되지 않았습니다.”라는 팝업이 뜨는 것
  • 예를 들어 beforeunload는 window가 닫히기 전에 function이 실행되는 것을 허락한다.
import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const usePreventLeave = () => {
  const listener = (event) => {
    event.preventDefault();
    event.returnValue = ''; // beforeunload는 returnValue를 요구한다.
  };

  // api에 뭔가를 보냈고, 사람들이 닫지 않기를 바란다면. 이걸 보호 할 수 있게 활성화 시키는 것
  // 그런데 Api가 응답을 해서 괜찮은 상태라면 사람들이 닫는걸 신경쓰지 않아도 됌. 그때는 이 보호를 활성화 시키지 않아도 된다.
  const enablePrevent = () => window.addEventListener('beforeunload', listener); // event 가로채기
  const disablePrevent = () => window.removeEventListener('beforeunload', listener);

  return { enablePrevent, disablePrevent };
};

const App = () => {
  const { enablePrevent, disablePrevent } = usePreventLeave();
  return (
    <div className="App">
      <button onClick={enablePrevent}>protect</button>
      <button onClick={disablePrevent}>unprotect</button>
    </div>
  );
};

export default App;

2.4 useBeforeLeave

  • 탭을 닫을 때(창을 벗어나고자 할 때) 실행되는 function 마우스가 페이지를 떠났을 때 실행된다.
import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const useBeforeLeave = (onBefore) => {
  const handle = (event) => {
    // console.log(event.clientY);
    const { clientY } = event;
    // clientY 가 <= 0 일때는 위로 벗어났을 때. 아래로 벗어난거는 취급하지 않음
    if (clientY <= 0) {
      onBefore();
    }
  };

  useEffect(() => {
    if (typeof onBefore !== 'function') {
      return;
    }
    document.addEventListener('mouseleave', handle);
    return () => document.removeEventListener('mouseleave', handle);
  }, []);
};

const App = () => {
  const begForLife = () => console.log('pls dont leave');
  // useBeforeLeave는 return이 없으므로 아래와 같이 작성
  useBeforeLeave(begForLife);
  return (
    <div className="App">
      <h1>Hello</h1>
    </div>
  );
};

export default App;

2.5 useFadeIn & useNetwork

  • mix Hook & animation

useFadeIn

import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const useFadeIn = (duration = 1, delay = 0) => {
  const element = useRef();
  useEffect(() => {
    if (typeof duration !== 'number' || typeof delay !== 'number') {
      return;
    }

    if (element.current) {
      const { current } = element;
      current.style.transition = `opacity ${duration}s ease-in-out ${delay}s`;
      current.style.opacity = 1;
    }
  }, []);
  return { ref: element, style: { opacity: 0 } };
};

const App = () => {
  const fadeInH1 = useFadeIn(1, 2);
  const fadeP = useFadeIn(5, 10);
  return (
    <div className="App">
      {/* <h1 ref={el} style={{ opacity: 0 }}> */}
      <h1 {...fadeInH1}>Hello</h1> {/* 위 코드랑 같은 내용 */}
      <p {...fadeP}>lorem ipsum lalalala</p>
    </div>
  );
};

export default App;

useNetwork

  • navigator가 online 또는 offline이 되는 것을 막아준다.
  • 콘솔 Network > Offline / Online을 변경하면서 확인할 수 있다.
import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const useNetwork = (onChange) => {
  // navigator.onLine : true / false. 웹사이트 온라인 여부
  const [status, setStatus] = useState(navigator.onLine);
  const handleChange = () => {
    if (typeof onChange === 'function') {
      onChange(navigator.onLine);
    }
    setStatus(navigator.onLine);
  };
  useEffect(() => {
    window.addEventListener('online', handleChange);
    window.addEventListener('offline', handleChange);

    () => {
      window.addEventListener('online', handleChange);
      window.addEventListener('offline', handleChange);
    };
  }, []);
  return status;
};

const App = () => {
  const handleNetworkChange = (online) => {
    console.log(online ? 'we just went online' : 'we are offline');
  };
  const onLine = useNetwork(handleNetworkChange);
  return (
    <div className="App">
      <h1>{onLine ? 'Online' : 'Offline'}</h1>
    </div>
  );
};

export default App;

2.6 useScroll & useFullscreen

사용자가 스크롤해서 무언가를 지나칠 때 함수를 실행한다.

useScroll

import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const useScroll = () => {
  const [state, setState] = useState({
    x: 0,
    y: 0,
  });
  const onScroll = () => {
    // console.log("y : ", window.scrollY, "x : ", window.scrollX);
    setState({ y: window.scrollY, x: window.scrollX });
  };

  useEffect(() => {
    window.addEventListener('scroll', onScroll);
    () => window.removeEventListener('scroll', onScroll);
  }, []);
  return state;
};

const App = () => {
  const { y } = useScroll();
  return (
    <div className="App" style={{ height: '1000vh' }}>
      <h1 style={{ position: 'fixed', color: y > 100 ? 'red' : 'blue' }}>Hello</h1>
    </div>
  );
};

export default App;

useFullscreen

import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const useFullscreen = () => {
  const element = useRef();
  const triggerFull = () => {
    if (element.current) {
      element.current.requestFullscreen();
    }
  };
  const exitFull = () => {
    const chkFullScreen = document.fullscreenElement;
    if (chkFullScreen !== null) {
      document.exitFullscreen();
    }
  };
  return { element, triggerFull, exitFull };
};
// full screen을 요청할 땐 element와 함께 requestFullScreen을 사용했는데
// full screen에서 빠져나올 땐 document를 통해서 빠져나온다.

const App = () => {
  const { element, triggerFull, exitFull } = useFullscreen();
  return (
    <div className="App" style={{ height: '1000vh' }}>
      <h1>Hello</h1>
      <div ref={element}>
        <img ref={element} src="https://i.ibb.co/R6RwNxx/grape.jpg" alt="grape" width="250" />
        <button onClick={exitFull}>exit fullscreen</button>
      </div>
      <button onClick={triggerFull}>make fullscreen</button>
    </div>
  );
};

export default App;

2.7 useNotification

  • 알림. notification api 이용

https://developer.mozilla.org/ko/docs/Web/API/notification

[ Static properties ]

  • Notification.permission : read only(읽기 전용)
    • denied(거부), granted(허가), default(모든 알람이 허용되지 않음. 상태의 선택을 알 수 없어서 browser는 value가 denied인 것처럼 행동)

[ Static Method ]

  • RequestPermisson()
import React, { useEffect, useRef, useState } from 'react';
import './styles.css';

const useNotification = (title, options) => {
  // window가 아니면 브라우저에서 noticifation을 지원하지 않기 때문에 확인
  if (!('Notification' in window)) {
    return;
  }

  const fireNotif = () => {
    if (Notification.permission !== 'granted') {
      // 권한이 없으므로 권한 요청
      Notification.requestPermission().then((permission) => {
        if (permission === 'granted') {
          new Notification(title, options);
        } else {
          return;
        }
      });
    } else {
      new Notification(title, options);
    }
  };
  return fireNotif;
};

const App = () => {
  const triggerNotif = useNotification('can i steal your kimchi?', {
    body: "i love kimchi, don't you?",
  });
  return (
    <div className="App">
      <button onClick={triggerNotif}>Hello</button>
    </div>
  );
};

export default App;

2.8 useAxios

  • add Dependency > axios
  • App.js
import React, { useEffect, useRef, useState } from 'react';
import './styles.css';
import useAxios from './useAxios';

const App = () => {
  const { loading, data, error, refetch } = useAxios({
    url: 'https://yts.mx/api/v2/list_movies.json',
  });
  console.log(`Loading: ${loading}\nError: ${error}\nData: ${JSON.stringify(data)}`);
  return (
    <div className="App">
      <h1>{data && data.status}</h1>
      <h2>{loading && 'Loading'}</h2>
      <button onClick={refetch}>Refetch</button>
    </div>
  );
};

export default App;
  • useAxios.js
import defaultAxios from 'axios';
import { useEffect, useState } from 'react';
// axios : http request 만드는 것

// axios 는 약간의 customization과 configuration을 허용한다.
// ex) 디폴트 url을 설정하거나 자동으로 헤더를 설정하는 것 같은걸 허용
// 그래서 우린 axios instance를 얻을 것이다. 만약 얻지 못하면 import한 axios 를 전달
// 우리는 패키지에서 axios를 얻어서 전달할거다.
// axios는 내가 instance를 만드는걸 허용하고 나는 configuration을 할 수 있고 그것과 함께 헤더를 보낼 수 있다.
const useAxios = (opts, axiosInstance = defaultAxios) => {
  const [state, setState] = useState({
    loading: true,
    error: null,
    data: null,
  });

  const [trigger, setTrigger] = useState(0);

  const refetch = () => {
    setState({
      ...state,
      loading: true,
    });
    setTrigger(Date.now()); // random number
  };

  useEffect(() => {
    if (!opts.url) {
      // opts : options
      return;
    }

    axiosInstance(opts)
      .then((data) => {
        setState({
          ...state,
          loading: false,
          data,
        });
      })
      .catch((error) => {
        // error catch
        setState({ ...state, loading: false, error });
      });
  }, [trigger]);
  return { ...state, refetch };
};

export default useAxios;