web-dev-qa-db-ja.com

非同期アクションを実行するモーダルダイアログをReduxで表示するにはどうすればよいですか?

状況によっては確認ダイアログを表示する必要があるアプリを作成しています。

何かを削除したいとしましょう。それからdeleteSomething(id)のようなアクションをディスパッチします。そうすれば、何らかのリデューサーがそのイベントをキャッチし、それを表示するためにダイアログリデューサーを埋めます。

この対話が提出されたとき、私の疑問は起こります。

  • 最初に送出されたアクションに従って、このコンポーネントはどのようにして適切なアクションを送出できますか?
  • アクション作成者はこのロジックを処理する必要がありますか?
  • レデューサーの中にアクションを追加できますか?

編集:

明確にするために:

deleteThingA(id) => show dialog with Questions => deleteThingARemotely(id)

createThingB(id) => Show dialog with Questions => createThingBRemotely(id)

だから私はダイアログコンポーネントを再利用しようとしています。ダイアログの表示/非表示は、これがリデューサーで簡単に実行できるため、問題ではありません。私が指定しようとしているのは、左側のフローを開始するアクションに従って、右側からアクションをディスパッチする方法です。

214
carlesba

更新 :React 16.0はReactDOM.createPortalを通じてポータルを導入しました link

更新 :Reactの次期バージョン(Fibre:おそらく16または17)はポータルを作成するメソッドを含むでしょう:ReactDOM.unstable_createPortal()link


ポータルを使用する

ダンAbramov答えの最初の部分は大丈夫ですが、定型文がたくさん含まれています。彼が言ったように、あなたもポータルを使うことができます。私はその考えを少し拡張します。

ポータルの利点はポップアップとボタンがReactツリーに非常に近いところにあり、小道具を使った非常に簡単な親子通信であるということです。ポータルで非同期アクションを扱うことも、親にポータルをカスタマイズさせることもできます。

ポータルとは

ポータルでは、Reactツリーに深くネストされている要素をdocument.body内に直接レンダリングできます。

そのアイデアは、例えばあなたが次のReactツリーをbodyにレンダリングするということです:

<div className="layout">
  <div className="outside-portal">
    <Portal>
      <div className="inside-portal">
        PortalContent
      </div>
    </Portal>
  </div>
</div>

そしてあなたは出力として得る:

<body>
  <div class="layout">
    <div class="outside-portal">
    </div>
  </div>
  <div class="inside-portal">
    PortalContent
  </div>
</body>

inside-portalノードは、通常の深くネストされた場所ではなく、<body>内に変換されています。

ポータルを使用する場合

ポータルは、既存のReactコンポーネントの上に表示されるべき要素(ポップアップ、ドロップダウン、提案、ホットスポット)を表示するのに特に役立ちます。

なぜポータルを使うのか

zインデックスの問題はもうありません :ポータルで<body>にレンダリングすることができます。もしあなたがポップアップやドロップダウンを表示したいのであれば、もしz-index問題と戦わなければならないのであれば、これは本当にいい考えです。ポータル要素にはマウント順にdocument.bodyが追加されます。つまり、z-indexを操作しない限り、デフォルトの動作では、ポータルはマウント順に積み重ねられます。実際には、z-indexについて考えなくても、別のポップアップの内側から安全にポップアップを開き、2番目のポップアップが最初のポップアップの上に表示されるようにすることができます。

実際には

最も単純なもの:ローカルのReact状態を使う: 単純な削除確認のポップアップについては、Reduxの定型句を使用する価値がないと思われる場合は、ポータルを使用してコードを大幅に単純化できます。そのようなユースケースでは、インタラクションは非常にローカルで、実際にはかなり実装の詳細なので、ホットリロード、タイムトラベル、アクションロギング、そしてReduxがもたらすすべての利点について本当に気にかけますか?個人的には、私はこの場合ローカルな州を使用せずに使用します。コードは以下のように単純になります。

class DeleteButton extends React.Component {
  static propTypes = {
    onDelete: PropTypes.func.isRequired,
  };

  state = { confirmationPopup: false };

  open = () => {
    this.setState({ confirmationPopup: true });
  };

  close = () => {
    this.setState({ confirmationPopup: false });
  };

  render() {
    return (
      <div className="delete-button">
        <div onClick={() => this.open()}>Delete</div>
        {this.state.confirmationPopup && (
          <Portal>
            <DeleteConfirmationPopup
              onCancel={() => this.close()}
              onConfirm={() => {
                this.close();
                this.props.onDelete();
              }}
            />
          </Portal>
        )}
      </div>
    );
  }
}

単純:まだRedux状態を使用できます :本当に必要な場合は、connectを使用してDeleteConfirmationPopupを表示するかどうかを選択できます。ポータルはReactツリーに深くネストされたままであるため、親がポータルに小道具を渡すことができるため、このポータルの動作をカスタマイズするのは非常に簡単です。ポータルを使わないのであれば、z-indexの理由であなたのReactツリーの一番上にポップアップをレンダリングしなければなりません。 。そして、通常、この問題に対する非常に厄介な解決策を見つけることができます。例えば、ネストした確認/キャンセルアクション、翻訳バンドルキー、さらに悪いことにはレンダリング機能(あるいは他のシリアル化不可能なもの)を含むアクションをディスパッチすることです。 DeleteConfirmationPopupDeleteButtonの子にすぎないので、ポータルでこれを行う必要はなく、通常の小道具を渡すことができます。

結論

ポータルはコードを単純化するのに非常に便利です。私は彼らなしではもうできませんでした。

ポータルの実装は、以下のような他の便利な機能を手助けすることもできます。

  • アクセシビリティ
  • ポータルを閉じるためのEspaceショートカット
  • 外部クリックを処理する(ポータルを閉じるかどうか)
  • リンククリックを処理する(ポータルを閉じるかどうか)
  • React Contextがポータルツリーで利用可能になりました

react-portal または react-modal は、フルスクリーンで表示されるポップアップ、モーダル、およびオーバーレイに適しています。通常は画面の中央に配置されます。

react-tether はほとんどのReact開発者には知られていませんが、そこで見つけることができる最も有用なツールの1つです。 Tether はポータルの作成を許可しますが、特定のターゲットに対してポータルを自動的に配置します。これは、ツールチップ、ドロップダウン、ホットスポット、ヘルプボックスなどに最適です。位置absolute/relativeおよびz-indexに問題がある場合、またはドロップダウンがビューポートの外側にある場合は、Tetherがそれをすべて解決します。

たとえば、クリックするとツールチップに展開される、オンボーディングホットスポットを簡単に実装できます。

Onboarding hotspot

本番コードはこちら。もっと単純にはできません:)

<MenuHotspots.contacts>
  <ContactButton/>
</MenuHotspots.contacts>

編集 :ちょうど発見された react-gateway ポータルをあなたの選択したノードにレンダリングすることを許可する(必ずしも本文ではない)

編集 react-popper はreact-tetherの代わりになるでしょう。 PopperJS は、DOMに直接触れることなく要素の適切な位置を計算するだけのライブラリで、Tetherが直接ボディに追加しながら、DOMノードをどこにいつ配置するかをユーザーに選択させます。

編集 react-slot-fill もあり、これは興味深く、ツリーの好きな場所に要素を予約要素スロットにレンダリングすることで同様の問題を解決するのに役立ちます。

89

このトピックに関するJSコミュニティの有名な専門家による多くの優れたソリューションと貴重な解説がここにあります。それは、見かけほどささいな問題ではないことを示す指標かもしれません。これが問題に対する疑念と不確実性の原因となり得る理由だと思います。

ここでの根本的な問題は、Reactで、コンポーネントをその親にマウントすることしか許可されていないことです。これは常に望ましい動作ではありません。しかし、この問題に対処する方法は?

この問題を解決するための解決策を提案します。より詳細な問題定義、src、および例は次の場所にあります。 https://github.com/fckt/react-layer-stack#rationale

根拠

react/react-domには、2つの基本的な仮定/アイデアがあります。

  • すべてのUIは自然に階層的です。これが、互いにラップするcomponentsのアイデアがある理由です
  • react-domは、デフォルトで(物理的に)子コンポーネントをその親DOMノードにマウントします

問題は、場合によっては2番目のプロパティが必要なものではないことです。コンポーネントを異なる物理DOMノードにマウントし、同時に親子間の論理接続を保持したい場合があります。

標準的な例は、ツールチップのようなコンポーネントです。開発プロセスのある時点で、UI elementの説明を追加する必要があることがわかります。固定レイヤーでレンダリングされ、その座標(UI element座標またはマウス座標)同時に表示する必要があるかどうか、その内容、親コンポーネントのコンテキストなどの情報が必要です。この例は、論理階層が物理DOM階層と一致しない場合があることを示しています。

https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example を見て、答えである具体例を見てくださいあなたの質問に:

import { Layer, LayerContext } from 'react-layer-stack'
// ... for each `object` in array of `objects`
  const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
  return (
    <Cell {...props}>
        // the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
        <Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
            hideMe, // alias for `hide(modalId)`
            index } // useful to know to set zIndex, for example
            , e) => // access to the arguments (click event data in this example)
          <Modal onClick={ hideMe } zIndex={(index + 1) * 1000}>
            <ConfirmationDialog
              title={ 'Delete' }
              message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
              confirmButton={ <Button type="primary">DELETE</Button> }
              onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hideMe) } // hide after confirmation
              close={ hideMe } />
          </Modal> }
        </Layer>

        // this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
        <LayerContext id={ modalId }> {({showMe}) => // showMe is alias for `show(modalId)`
          <div style={styles.iconOverlay} onClick={ (e) => showMe(e) }> // additional arguments can be passed (like event)
            <Icon type="trash" />
          </div> }
        </LayerContext>
    </Cell>)
// ...
8
fckt

私の意見では、最低限の実装には2つの要件があります。モーダルが開いているかどうかを追跡する状態、およびモーダルを標準の反応ツリーの外側にレンダリングするためのポータル。

以下のModalContainerコンポーネントは、モーダルとトリガーに対応するレンダリング機能とともにこれらの要件を実装します。これは、モーダルを開くためのコールバックの実行を担当します。

import React from 'react';
import PropTypes from 'prop-types';
import Portal from 'react-portal';

class ModalContainer extends React.Component {
  state = {
    isOpen: false,
  };

  openModal = () => {
    this.setState(() => ({ isOpen: true }));
  }

  closeModal = () => {
    this.setState(() => ({ isOpen: false }));
  }

  renderModal() {
    return (
      this.props.renderModal({
        isOpen: this.state.isOpen,
        closeModal: this.closeModal,
      })
    );
  }

  renderTrigger() {
     return (
       this.props.renderTrigger({
         openModal: this.openModal
       })
     )
  }

  render() {
    return (
      <React.Fragment>
        <Portal>
          {this.renderModal()}
        </Portal>
        {this.renderTrigger()}
      </React.Fragment>
    );
  }
}

ModalContainer.propTypes = {
  renderModal: PropTypes.func.isRequired,
  renderTrigger: PropTypes.func.isRequired,
};

export default ModalContainer;

そして、これは簡単なユースケースです...

import React from 'react';
import Modal from 'react-modal';
import Fade from 'components/Animations/Fade';
import ModalContainer from 'components/ModalContainer';

const SimpleModal = ({ isOpen, closeModal }) => (
  <Fade visible={isOpen}> // example use case with animation components
    <Modal>
      <Button onClick={closeModal}>
        close modal
      </Button>
    </Modal>
  </Fade>
);

const SimpleModalButton = ({ openModal }) => (
  <button onClick={openModal}>
    open modal
  </button>
);

const SimpleButtonWithModal = () => (
   <ModalContainer
     renderModal={props => <SimpleModal {...props} />}
     renderTrigger={props => <SimpleModalButton {...props} />}
   />
);

export default SimpleButtonWithModal;

レンダリング機能を使用するのは、レンダリングされたモーダルおよびトリガーコンポーネントの実装から状態管理と定型ロジックを分離したいからです。これにより、レンダリングされたコンポーネントを好きなものにすることができます。あなたの場合、モーダルコンポーネントは非同期アクションをディスパッチするコールバック関数を受け取るコネクテッドコンポーネントであると思います。

トリガーコンポーネントからモーダルコンポーネントに動的プロップを送信する必要がある場合は、あまり起こりませんが、動的プロップを独自の状態で管理するコンテナコンポーネントでModalContainerをラップし、オリジナルのレンダリングメソッドを強化することをお勧めします。そう。

import React from 'react'
import partialRight from 'lodash/partialRight';
import ModalContainer from 'components/ModalContainer';

class ErrorModalContainer extends React.Component {
  state = { message: '' }

  onError = (message, callback) => {
    this.setState(
      () => ({ message }),
      () => callback && callback()
    );
  }

  renderModal = (props) => (
    this.props.renderModal({
       ...props,
       message: this.state.message,
    })
  )

  renderTrigger = (props) => (
    this.props.renderTrigger({
      openModal: partialRight(this.onError, props.openModal)
    })
  )

  render() {
    return (
      <ModalContainer
        renderModal={this.renderModal}
        renderTrigger={this.renderTrigger}
      />
    )
  }
}

ErrorModalContainer.propTypes = (
  ModalContainer.propTypes
);

export default ErrorModalContainer;
2
kskkido

モーダルを接続されたコンテナにラップして、ここで非同期操作を実行します。これにより、アクションをトリガーするためのディスパッチとonCloseプロップの両方に到達できます。 (小道具からdispatchに到達するには、mapDispatchToProps関数をconnectに渡します not を実行します。

class ModalConteiner extends React.Component {
  handleDelete = () => {
    const { dispatch, onClose } = this.props;
    dispatch({type: 'DELETE_POST'});

    someAsyncOperation().then(() => {
      dispatch({type: 'DELETE_POST_SUCCESS'});
      onClose();
    })
  }

  render() {
    const { onClose } = this.props;
    return <Modal onClose={onClose} onSubmit={this.handleDelete} />
  }
}

export default connect(/* no map dispatch to props here! */)(ModalContainer);

モーダルがレンダリングされるアプリとその表示状態が設定されます。

class App extends React.Component {
  state = {
    isModalOpen: false
  }

  handleModalClose = () => this.setState({ isModalOpen: false });

  ...

  render(){
    return (
      ...
      <ModalContainer onClose={this.handleModalClose} />  
      ...
    )
  }

}
0
gazdagergo