Portal을 하위 트리로 반응

19170 단어 reactreactportal

문제



아래는 제가 최근에 접한 사용 사례입니다. 아래와 같은 표준 사이드바 셸 UI 디자인을 상상해 보십시오.



사이드바 + 이동 경로는 애플리케이션의 "쉘"이며 콘텐츠는 개별 페이지에서 렌더링됩니다.

React 구성 요소 구조는 다음과 같습니다.

<AppShell>
  <RouteOutlet />
</AppShell>


여기서 <RouteOutlet />는 URL을 기반으로 자식을 렌더링하는 React Router Switch/Route 구성입니다.

언뜻 보기에는 간단해 보일 수 있지만 까다로운 부분은 이동 경로가 렌더링되는 페이지에 따라 동적이어야 한다는 것입니다.

이를 수행하는 한 가지 방법은 React Context 을 사용하는 것입니다.

const BreadCrumbContext = React.createContext(function noop() {});

const AppShell = ({ children }) => {
  const [breadcrumbs, setBreadcrumbs] = React.useState([]);

  return (
    <BreadCrumbContext.Provider value={setBreadcrumbs}>
      <Sidebar />
      <div>
        <Breadcrumbs values={breadcrumbs} />
        {children}
      </div>
    </BreadCrumbContext.Provider>
  );
};

// custom hook to be used by page that want to add breadcrumbs
const useBreadcrumbs = (breadcrumbValues) => {
  const setBreadcrumbs = React.useContext(BreadCrumbContext);

  React.useEffect(() => {
    setBreadcrumbs(breadcrumbValues);
    return () => setBreadcrumbs([]);
  }, [breadcrumbValues, setBreadcrumbs]);
};

const MyPage = ({ customer }) => {
  useBreadcrumbs(['Customer', customer.name]);

  return <div>...Other Content</div>;
};


솔루션은 작동하지만 이러한 모든 공급자 및 사용자 지정 후크를 설정하는 것은 상당히 지루합니다.

더 간단할 수 있습니까?

해결책



React에는 상대적으로 덜 사용되는 기능Portal이 있어 자식을 DOM 계층 외부에 있는 DOM 노드로 렌더링할 수 있습니다.

그러나 공식 문서(및 온라인에서 찾을 수 있는 대부분의 문서)에서 포털의 사용 사례는 대화/도구 설명 등과 같은 사용 사례에 유용한 document.body 의 루트에 자녀를 추가하는 것입니다.

그러나 대상이 document.body가 아니라 다른 React 하위 트리라면 어떻게 될까요?

그러면 위의 문제가 해결되어 페이지에서 탐색 경로로 렌더링됩니다!

해결책은 다음과 같습니다.

// custom hook to force React to rerender, hook version of `forceUpdate` of class component
function useForceUpdate() {
  const [, dispatch] = React.useState(Object.create(null));
  return React.useCallback(() => {
    dispatch(Object.create(null));
  }, []);
}

// simple event emitter. Read https://malcolmkee.com/blog/simple-event-bus/ for a more detailed explanation.
function createEventBus() {
  const listeners = [];

  return {
    listen: (listener) => {
      listeners.push(listener);
      return () => {
        listeners.splice(listeners.indexOf(listener), 1);
      };
    },
    emit: () => listeners.forEach((l) => l()),
  };
}

// this is where the magic is
function createFillSlot() {
  // create a ref to get a reference of the target that we want to render into
  const ref = React.createRef();
  // setup the event emitter
  const eventBus = createEventBus();

  // Slot is where we want to render. It is just an empty div.
  function Slot() {
    React.useEffect(() => {
      if (ref.current) {
        // ask the event emitter to tell the whole world the slot is ready to be used
        eventBus.emit();
      }
    }, []);

    return <div ref={ref} />;
  }

  // Fill is where we render the content we want to inject to the Slot
  function Fill({ children }) {
    const forceUpdate = useForceUpdate();

    // when Slot is rendered, we will get notified by event bus, re-render
    React.useEffect(() => eventBus.listen(forceUpdate), [forceUpdate]);

    return ref.current ? ReactDOM.createPortal(children, ref.current) : null;
  }

  return {
    Slot,
    Fill,
  };
}

const Breadcrumb = createFillSlot();

// This is where we want to show the content
const Header = () => {
  return (
    <div className="p-2 flex items-center bg-white text-black shadow-lg">
      Header <Breadcrumb.Slot />
    </div>
  );
};

const Page1 = () => {
  return (
    <div>
      <h2>Page 1</h2>
      <Breadcrumb.Fill>
        Hello > <a href="#">Page 1</a>
      </Breadcrumb.Fill>
    </div>
  );
};

const Page2 = () => {
  return (
    <div>
      <h2>Page 2</h2>
      <Breadcrumb.Fill>
        Hello > <a href="#">Page 2</a>
      </Breadcrumb.Fill>
    </div>
  );
};

const App = () => {
  const [page, setPage] = React.useState('');

  return (
    <div className="flex">
      <div className="flex flex-col space-y-2 px-3 items-start">
        <button onClick={() => setPage('1')}>Show Page 1</button>
        <button onClick={() => setPage('2')}>Show Page 2</button>
      </div>
      <div className="flex-1">
        <Header />
        <div className="p-3 bg-gray-100 text-gray-600">
          {page === '1' && <Page1 />}
          {page === '2' && <Page2 />}
        </div>
      </div>
    </div>
  );
};

render(<App />);

좋은 웹페이지 즐겨찾기