Modal in

React and Redux

—— 刘旸 @外居乐

噩梦

问题

  • 管理显示逻辑很麻烦
  • 数量太多有性能问题
  • 逻辑与上下文相关

基础的 Modal

import React from 'react';
import Modal from 'react-modal';
import ReactDOM from 'react-dom';

Modal.setAppElement('#app');

const App = () => (
  <div>
    <Modal
      isOpen
      onAfterOpen={...}
      onRequestClose={...}
      style={...}
      contentLabel="Opened Modal"
    >
      Opened Modal
    </Modal>
    <Modal
      isOpen={false}
      onAfterOpen={...}
      onRequestClose={...}
      style={...}
      contentLabel="Closed Modal"
    >
      Closed Modal
    </Modal>
  </div>
)

ReactDOM.render(<App />, document.getElementById('app'));
<html lang="en">
  <head>...</head>
  <body class="ReactModal__Body--open">
    <div id="app" aria-hidden="true">...</div>
    <div class="ReactModalPortal">
      <div class="Overlay">
        <div aria-label="Opened Modal" tabindex="-1">
          Opened Modal
        </div>
      </div>
    </div>
    <div class="ReactModalPortal"></div>
  </body>
</html>
  • Append 到 Root 节点的外面
  • 只有 Open 的时候才会有 children

业务场景下的 Modal

  • 只在局部使用
  • 在一个页面中复用
  • 在多个页面中复用

只在局部使用的 Modal

  • Modal 和打开的按钮一起写在使用的组件中
  • 状态放在该组件的 state 中
import React from 'react';
import Modal from 'react-modal';
import { compose, withState, pure } from 'recompose';

export default compose(
  pure,
  withState('isOpen', 'setIsOpen', false),
)(({ isOpen, setIsOpen }) => (
  <div>
    <Modal
      isOpen={isOpen}
      onRequestClose={() => setIsOpen(false)}
    >
      Modal
    </Modal>
    <button onClick={() => setIsOpen(true)}>Open Modal</button>
  </div>
));

在页面中会复用的 Modal

  • 例如:修改列表中某一项的 Modal
  • Modal 写在父组件中
  • 状态放在父组件的 state 中
  • 修改状态的方法传递给弹出 Modal 的子组件
import React from 'react';
import Modal from 'react-modal';
import { compose, withState, pure } from 'recompose';

const Item = ({ openModal }) => (
  <div>
    <button onClick={openModal}>Open Modal</button>
  </div>
);

export default compose(
  pure,
  withState('isOpen', 'setIsOpen', false),
  withState('currentItem', 'setCurrentItem', {}),
)(({ isOpen, setIsOpen, items, setCurrentItem, currentItem }) => (
  <div>
    <Modal
      isOpen={isOpen}
      onRequestClose={() => setIsOpen(false)}
    >
      Selected Item: {currentItem.id}
    </Modal>
    {items.map(item => (
      <Item 
        key={item.id}
        openModal={() => {
          setIsOpen(true);
          setCurrentItem(item);
        }}
      />
    ))}
  </div>
));

在多页面中会复用的 Modal

  • 例如:Login / Register Modal
  • Modal 写在 Layout 组件中
  • 状态放在 store 中
  • 修改状态的 action 传递给弹出 Modal 的子组件

复杂业务场景下的 Modal

Confirmation

  • 页面中可能同时存在多个 Confirmation
  • 选择确定或取消有后续操作
  • 后续操作有可能是异步的,需要 Loading
  • 我只想调用一个方法就把 Modal 打开
  • 我希望可以自动 Loading,完成以后关闭

实现方案 —— Antd

  • 提供一个 static 方法来打开 Modal
  • 使用 ReactDOM APIDOM API 控制 DOM
  • 确认的时候检查 OK callback,如果是 promise 的话就显示 Loading,resolve 以后才关闭

实现方案 —— Redux

  • Store 中存储 confirmations 列表
  • callback 存放在哪里?
  • 如何处理 loading ?
import { OPEN_CONFIRMATION, CONFIRMATION_OK, CONFIRMATION_CANCEL } from 'action-types';
import { startConfirmationLoading } from 'actions';

export default () => {
  const callbacks = [];
  return ({ dispatch }) => (next) => async (action) => {
    const { payload, type } = action;

    if (type === OPEN_CONFIRMATION) {
      const { onOK, onCancel } = payload;
      callbacks.push({ onOK, onCancel });

    } else if (type === CONFIRMATION_OK || type === CONFIRMATION_CANCEL) {
      const { onOK, onCancel } = callbacks.pop();
      const result = type === CONFIRMATION_OK ? onOK() : onCancel();

      if (isPromise(result)) {
        dispatch(startConfirmationLoading());
        await result;
        return next(action);
      }
    }
    return next(action);
  };
}

Login

  • Login 成功或失败以后有后续操作
  • 后续操作需要依赖于现有数据或 API 返回数据
import { OPEN_LOGIN_MODAL, CLOSE_LOGIN_MODAL } from 'action-types';
import { prop } from 'lodash/fp';

export default () => {
  let loginPromise = null;
  let resolveLogin = null;
  let rejectLogin = null;

  return ({ getState }) => (next) => (action) => {
    const { type, payload } = action;
    if (type === OPEN_LOGIN_MODAL) {
      loginPromise = new Promise((resolve, reject) => {
        resolveLogin = resolve;
        rejectLogin = reject;
      });
      next(action);
      return loginPromise;
    }
    if (type === CLOSE_LOGIN_MODAL) {
      if (loginPromise) {
        if (payload.success) {
          resolveLogin({
            payload: prop('currentAccount.accountId')(getState()),
          });
        } else {
          rejectLogin();
        }
        loginPromise = null;
      }
    }
    return next(action);
  };
};

总结

  • 只需要调用一个 action 来打开共用的 Modal
  • 使用 Store 来存储共用 Modal 的可序列化状态
  • 使用 Middleware 来存储不可序列化的 callback

问题

  • Login 后续操作依赖的数据可能会变多

Modal in React and Redux

By zation

Modal in React and Redux

  • 1,492