beecomci / today_i_learned

0 stars 0 forks source link

[React-Basic-Hooks] 9. Portal #13

Open beecomci opened 3 years ago

beecomci commented 3 years ago

Portal

// child : 그리고자 하는 ReactNode
// container : 어디에 그릴지 실제 DOM 전달 
ReactDOM.createPortal(child: React.ReactNode, container: HTMLElement)

@08-portal/portal

import { useState } from 'react';
import ReactDOM from 'react-dom';

// 1번째 인자 : 내가 그리고 싶은 React Element
// 2번째 인자 : 어디에 그릴지
const Modal = () => ReactDOM.createPortal(
  <div>I am a modal</div>,
  document.body.querySelector('#app')
)

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

  return (
    <div onClick={() => setCount(count + 1)}>
      <div>Hello!</div>
      <div>count: {count}</div>
      {/* 구조상으로는 위의 영역 안에 들어가있는 것 같지만, 실제 DOM을 보면 #root 밖의 #app에 그려져 있음 */}
      {/* Virtual DOM상으로는 지금 보이는 구조대로 들어가기 때문에 Modal에서 발생한 click event도 부모 div에서 catch가 가능한 점이 장점, 내부 state를 공유해서 사용 가능 */}
      {/* React가 아니라면 Modal에 직접 click 이벤트를 또 걸어줘야 하는 불편함 */}
      <Modal />
    </div>
  )
}

export default App

활용예시: codepen

@08-portal/windowPortal

import { useState, useEffect, useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
import copyStyles from './copyStyles';

function MyWindowPortal({ children, closeWindowPortal }) {
  const containerElRef = useRef(null);

  // 외부 DOM이기 때문에 찾는 로직
  if (!containerElRef.current) {
    // STEP 1: create an empty div
    containerElRef.current = document.createElement('div');
  }

  useEffect(() => {
    const containerEl = containerElRef.current

    // STEP 3: open a new browser window
    const externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');

    // STEP 4: append the container <div> to the body of the new window
    externalWindow.document.body.appendChild(containerEl);

    externalWindow.document.title = 'A React portal window';
    copyStyles(document, externalWindow.document); // 현재 documnet와 외부 document 스타일을 맞춤 (그냥 따로 만든 로직)

    // update the state in the parent component if the user closes the new window
    // 닫히기 직전에 setShowWindowPortal로 내부 state로 닫혔음을 update
    externalWindow.addEventListener('beforeunload', closeWindowPortal);

    // store containterEl to ref
    containerElRef.current = containerEl;

    // This will fire when showWindowPortal in the parent component becomes false
    // So we tidy up by just closing the window
    return () => {
      externalWindow.close();
    }
  }, [closeWindowPortal])

  // STEP 2: draw children to containerEl
  return ReactDOM.createPortal(children, containerElRef.current);
}

function App() {
  const [count, setCount] = useState(0);
  const [showWindowPortal, setShowWindowPortal] = useState(false); // Portal을 보여줄지 말지

  // 함수를 다시 생성할 필요가 없어서 useCallback 감싸서 사용 (아래에서 dependency로 사용)
  const toggleWindowPortal = useCallback(() => {
    setShowWindowPortal(prev => !prev);
  }, [])

  const closeWindowPortal = useCallback(() => {
    setShowWindowPortal(false);
  }, [])

  useEffect(() => {
    const interval = window.setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    return () => {
      window.clearInterval(interval);
      closeWindowPortal(); // 컴포넌트가 unmount되면 Portal로 열린 Modal을 닫음
    }
  }, [closeWindowPortal])

  return (
    <div>
      <h1>Counter: {count}</h1>

      <button onClick={toggleWindowPortal}>
        {showWindowPortal ? 'Close the' : 'Open a'} Portal
      </button>

      {showWindowPortal && (
        // 위에서 정의한 closeWindowPortal을 그대로 전달해서 사용 가능
        // 내부 Element들 모두 children으로 전달됨
        <MyWindowPortal closeWindowPortal={closeWindowPortal}>
          {/* 위의 count와 같은 count 공유 사용 (외부 props, state 공유시 유용한 Portal) */}
          <h1>Counter in a portal: {count}</h1>
          <p>Even though I render in a different window, I share state!</p>

          <button onClick={() => closeWindowPortal()}>
            Close me!
          </button>
        </MyWindowPortal>
      )}
    </div>
  );
}

export default App;