Before getting to the main topic, let’s take a look at some interesting scenarios related to pop-ups 🤔.

some interesting real scenes

Case 1: Global pop-up window

image

The above picture is the login pop-up window of Nuggets. There are many opportunities to trigger the pop-up window display when not logged in, such as:

  1. Click the login button on the Header
  2. Likes and comments on the article list page or detail page
  3. Post boiling, comment boiling, and like boiling
  4. ...

Developers often define a <LoginModal /> based on a third-party component library, and then mount it under the Root component:

 function App() {
  const [visible, setVisible] = useState(false);
  const [otherLoginData, setOtherLoginData] = useState();

  // other logic ...

  return (
    <div className="app">
      <Main />
      <LoginModal
        visible={visible}
        onCancel={() => setVisible(false)}
        otherLoginData={otherLoginData}
      />
    </div>
  );
}

This creates some problems:

  1. <LoginModal /> The internal logic will be executed when the component is rendered, even if the popup is hidden
  2. extra complexity. Since there are multiple trigger timings, it is necessary to transparently transmit <Main /> setVisible and setOtherLoginData to multiple sub-components inside ---1da16fdffa204705228c6cc34e4c0243---, you can choose to pass props layer by layer. Pass it in (who knows how many layers!), you can also introduce tools for state sharing, but in any case, this is not an easy task;
  3. As there are more and more similar popup windows, the root component will maintain the state of many popup window components... God knows why displaying a popup window needs to repeatedly jump across multiple files.

Showing a popup, why is it so complicated?

The above case is from @ebay/nice-modal-react, slightly modified

In addition to the above-mentioned global pop-up scenario, there is another scenario that is also very troublesome.

Case 2: Pop-up window with branches and dependencies

image

image

A headache for users, a headache for developers

Pop-up 1 confirms pop-up pop-up 2, cancels pop-up pop-up 3, pop-up 2 and pop-up 3 also have corresponding branch logic, and it is easy for children and grandchildren. If it is implemented according to the conventional declarative pop-up component, it is very scary!

image

Not so generic solution

Get rid of the so-called React Philosophy: Data-Driven Views ( view = f(data) ) and go back to the original window.confirm and window.alert , and many problems are solved:

 let mountNode: HTMLDivElement | null = null;

LoginModal.show = (props?: LoginModalProps) => {
  if (!mountNode) {
    mountNode = document.createElement('div');
    document.body.appendChild(mountNode);
  }
  // 通过 ReactDOM.render 将组件渲染到固定的节点
  ReactDOM.render(<LoginModal {...props} visible />, mountNode);
};

LoginModal.hide = () => {
  mountNode && ReactDOM.render(<LoginModal {...props} visible={false} />, mountNode);
};

LoginModal.destroy = () => {
  // 通过 ReactDOM.unmountComponentAtNode 卸载节点
  mountNode && ReactDOM.unmountComponentAtNode(mountNode);
};

After the above code processing, you can directly display the pop-up window through LoginModal.show({otherLoginData}) ( hide and destroy similarly).

For pop-up windows with dependencies, you can make chain calls by returning Promise . The usage is as follows.

 try {
  const ret1 = await LoginModal.show({ otherLoginData });
  const ret2 = await Ohter1Modal.show();
} catch (error) {
  const ret3 = await Other2Modal.show();
}

Since the solution is to render the component to a new node through ReactDOM.render , which is separated from the original React context, it will be a little troublesome in some scenes with strong dependencies Context .

Probably the best popup practice

image

When I was browsing github one day, I found @ebay/nice-modal-react , let's see how the eBay developers make the pop-up window nice enough under React.

Create a popup

 import { Modal } from 'antd';
import NiceModal, { useModal } from '@ebay/nice-modal-react';

export default NiceModal.create(({ name }) => {
  // Use a hook to manage the modal state
  const modal = useModal();
  return (
    <Modal
      title="Hello Antd"
      onOk={() => modal.hide()}
      visible={modal.visible}
      onCancel={() => modal.hide()}
      afterClose={() => modal.remove()}
    >
      Hello {name}!
    </Modal>
  );
});

It is very similar to the original custom pop-up window component based on antd, except that the pop-up window related to display and concealment props (such as visible/hide/remove ) is obtained through useModal The outer layer encapsulates the components (higher-order components) through NiceModal.create .

Use popup

Before using the pop-up window, it needs to be introduced in the Root node <NiceModal.Provider />

 import NiceModal from '@ebay/nice-modal-react';
ReactDOM.render(
  <React.StrictMode>
    <NiceModal.Provider>
      <App />
    </NiceModal.Provider>
  </React.StrictMode>,
  document.getElementById('root'),
);

Then you can display the previously customized popup window anywhere by NiceModal.show :

 import NiceModal from '@ebay/nice-modal-react';
import MyAntdModal from './my-antd-modal'; // created by above code

function App() {
  const showAntdModal = () => {
    // Show a modal with arguments passed to the component as props
    NiceModal.show(MyAntdModal, { name: 'Nate' })
  };
  return (
    <div className="app">
      <h1>Nice Modal Examples</h1>
      <div className="demo-buttons">
        <button onClick={showAntdModal}>Antd Modal</button>
      </div>
    </div>
  );
}
The above guidelines refer to the official documents of NiceModal

After NiceModal encapsulation, the benefits are obvious:

  1. The calling process is clean and elegant
  2. The component still exists in the context (the location can be customized, the default is under Provider )
  3. show/hide return value of the method is Promise , which is convenient for chain calls (changed by useModal().resolve(val) and other methods Promise )

Simple realization idea

If you are interested in the implementation principle of NiceModal, then you can think about what is our previous pain point? Case 1 describes two major pain points:

  1. The root component renders the pop-up window and maintains the pop-up window state too NiceModal.Provider Internal unified rendering of the pop-up window
  2. Explicit and implicit related states and methods need to be shared - the corresponding state is maintained inside the library and exposed through the unified API ( show/hide/useModal )
The chain call of case 2 becomes very easy after breaking away from the declarative component, just make good use of Promise.
 const Provider: React.FC = ({ children }) => {
  // 初始弹窗信息
  const [modals, _dispatch] = useReducer(reducer, initialState);
  dispatch = _dispatch;
  return (
    <NiceModalContext.Provider value={modals}>
      {children}
      <NiceModalPlaceholder />
    </NiceModalContext.Provider>
  );
};

const NiceModalPlaceholder: React.FC = () => {
  const modals = useContext(NiceModalContext);
  // 根据弹窗信息找到需要渲染的弹窗 ID (NiceModal.show 时更新)
  const visibleModalIds = Object.keys(modals).filter((id) =>
    Boolean(modals[id])
  );

  // 找到对应创建的高阶组件(NiceModal.show 时注册)
  const toRender = visibleModalIds
    .filter((id) => MODAL_REGISTRY[id])
    .map((id) => ({
      id,
      ...MODAL_REGISTRY[id],
    }));

  return (
    <>
      {toRender.map((t) => (
        {/* 渲染 NiceModal.create 创建的高阶组件,并注入 ID */}
        <t.comp key={t.id} id={t.id} />
      ))}
    </>
  );
};

const create =
  <P extends Record<string, unknown>>(
    Comp: React.ComponentType<P>
  ): React.FC<P & NiceModalHocProps> =>
  ({ id }) => {
    const modals = useContext(NiceModalContext);
    const shouldMount = Boolean(modals[id]);

    if (!shouldMount) {
      return null;
    }
    return (
      <NiceModalIdContext.Provider value={id}>
        {/* 找到对应 ID 的参数,传入内部真实组件 */}
        <Comp {...(modals[id].args as P)} />
      </NiceModalIdContext.Provider>
    );
  };

After understanding the above three methods, show/hide/useModal and other methods are based on the dispatch function to update the pop-up data ( Context ) to trigger the view update (where show method register one more step, generate ID and bind to the corresponding higher-order component).

Recommended reading


海秋
311 声望18 粉丝

前端新手