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


240

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

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

このダイアログが送信されたときに私の疑問が生じます。

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

編集:

より明確にするために:

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

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

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


1
あなたの場合、ダイアログの状態(非表示/表示)はローカルだと思います。ダイアログの表示/非表示を管理するために、反応状態を使用することを選択します。このようにして、「最初の行動による適切な行動」の問題はなくなります。
Ming

回答:


516

私が提案するアプローチは少し冗長ですが、複雑なアプリにかなりうまく拡張できることがわかりました。あなたはモーダルを表示したい場合は、記述したアクションを発射され、あなたが見たい、モーダル:

モーダルを表示するアクションをディスパッチする

this.props.dispatch({
  type: 'SHOW_MODAL',
  modalType: 'DELETE_POST',
  modalProps: {
    postId: 42
  }
})

(もちろん、文字列は定数にすることができます。簡単にするために、インライン文字列を使用しています。)

モーダル状態を管理するためのレデューサーの作成

次に、これらの値を受け入れるだけのリデューサーがあることを確認します。

const initialState = {
  modalType: null,
  modalProps: {}
}

function modal(state = initialState, action) {
  switch (action.type) {
    case 'SHOW_MODAL':
      return {
        modalType: action.modalType,
        modalProps: action.modalProps
      }
    case 'HIDE_MODAL':
      return initialState
    default:
      return state
  }
}

/* .... */

const rootReducer = combineReducers({
  modal,
  /* other reducers */
})

すごい!これで、アクションをディスパッチすると、state.modalが更新され、現在表示されているモーダルウィンドウに関する情報が含まれます。

ルートモーダルコンポーネントの記述

コンポーネント階層のルートで<ModalRoot>、Reduxストアに接続されているコンポーネントを追加します。state.modal適切なモーダルコンポーネントをリッスンして表示し、プロップをから転送しstate.modal.modalPropsます。

// These are regular React components we will write soon
import DeletePostModal from './DeletePostModal'
import ConfirmLogoutModal from './ConfirmLogoutModal'

const MODAL_COMPONENTS = {
  'DELETE_POST': DeletePostModal,
  'CONFIRM_LOGOUT': ConfirmLogoutModal,
  /* other modals */
}

const ModalRoot = ({ modalType, modalProps }) => {
  if (!modalType) {
    return <span /> // after React v15 you can return null here
  }

  const SpecificModal = MODAL_COMPONENTS[modalType]
  return <SpecificModal {...modalProps} />
}

export default connect(
  state => state.modal
)(ModalRoot)

ここで何をしましたか?ModalRoot現在の読み出しmodalTypemodalPropsからstate.modal、それが接続されているが、そのように対応するコンポーネントレンダリングDeletePostModalまたはConfirmLogoutModal。すべてのモーダルはコンポーネントです!

特定のモーダルコンポーネントの記述

ここには一般的な規則はありません。これらは、アクションをディスパッチしたり、ストア状態から何かを読み取ったり、たまたまモーダルになったりすることができる単なるReactコンポーネントです。

たとえば、次のDeletePostModalようになります。

import { deletePost, hideModal } from '../actions'

const DeletePostModal = ({ post, dispatch }) => (
  <div>
    <p>Delete post {post.name}?</p>
    <button onClick={() => {
      dispatch(deletePost(post.id)).then(() => {
        dispatch(hideModal())
      })
    }}>
      Yes
    </button>
    <button onClick={() => dispatch(hideModal())}>
      Nope
    </button>
  </div>
)

export default connect(
  (state, ownProps) => ({
    post: state.postsById[ownProps.postId]
  })
)(DeletePostModal)

DeletePostModalそれは任意の連結成分のような記事のタイトルと作品を表示できるようにストアに接続されている:それは含めて、アクションをディスパッチすることができhideModal、それ自体を非表示にする必要があるとき。

プレゼンテーションコンポーネントの抽出

すべての「特定の」モーダルに同じレイアウトロジックをコピーして貼り付けるのは面倒です。しかし、コンポーネントがありますよね?そのため、特定のモーダルの機能はわからないが、その外観を処理するプレゼンテーション <Modal>コンポーネントを抽出できます。

次に、などの特定のモーダルDeletePostModalでレンダリングに使用できます。

import { deletePost, hideModal } from '../actions'
import Modal from './Modal'

const DeletePostModal = ({ post, dispatch }) => (
  <Modal
    dangerText={`Delete post ${post.name}?`}
    onDangerClick={() =>
      dispatch(deletePost(post.id)).then(() => {
        dispatch(hideModal())
      })
    })
  />
)

export default connect(
  (state, ownProps) => ({
    post: state.postsById[ownProps.postId]
  })
)(DeletePostModal)

<Modal>アプリケーションで受け入れることができる一連の小道具を思いつくのはあなた次第ですが、私はあなたがいくつかの種類のモーダル(たとえば、情報モーダル、確認モーダルなど)とそれらのためのいくつかのスタイルを持っていると想像します。

アクセシビリティと外部クリックまたはEscキーの非表示

モーダルに関する最後の重要な部分は、通常、ユーザーが外側をクリックするかEscapeキーを押したときに非表示にすることです。

これを実装することについてアドバイスする代わりに、自分で実装しないことをお勧めします。アクセシビリティを考慮して正しく理解するのは難しい。

代わりに、などのアクセス可能な既製のモーダルコンポーネントを使用することをお勧めしますreact-modal。それは完全にカスタマイズ可能で、好きなものをその中に置くことができますが、それはアクセシビリティを正しく処理するので、視覚障害者がまだあなたのモーダルを使用することができます。

アプリケーションに固有の小道具を受け入れ、子ボタンやその他のコンテンツを生成react-modalする独自の<Modal>ものでラップすることもできます。それはすべて単なるコンポーネントです!

その他のアプローチ

それを行う方法は複数あります。

一部の人々は、このアプローチの冗長性が気に入らず、「ポータル」と呼ばれる手法でコンポーネント内に<Modal>直接レンダリングできるコンポーネントを好む。ポータルを使用すると、コンポーネントをDOM内の所定の場所に実際にレンダリングしながら、コンポーネントを内部にレンダリングできます。これは、モーダルに非常に便利です。

実際、react-modal私が以前にリンクしたことはすでに内部でそれを行っているので、技術的には上からそれをレンダリングする必要さえありません。表示したいモーダルを表示しているコンポーネントから分離するのは良いことですが、コンポーネントからreact-modal直接使用して、上で書いたもののほとんどをスキップすることもできます。

両方のアプローチを検討して実験し、アプリとチームに最適なものを選択することをお勧めします。


35
私がお勧めすることの1つは、リデューサーにプッシュおよびポップできるモーダルのリストを維持させることです。馬鹿げたことに聞こえるかもしれませんが、デザイナー/製品タイプがモーダルからモーダルを開くことを望んでいる状況に常に出くわしました。
カイル

9
ええ、間違いなく、これは、状態を配列に変更するだけなので、Reduxで簡単に構築できるものです。個人的には、反対に、モーダルを排他的にしたいデザイナーと協力してきたので、私が書いたアプローチは、偶発的なネストを解決します。しかし、そうです、あなたはそれを両方の方法で持つことができます。
Dan Abramov、2016

4
私の経験では、モーダルがローカルコンポーネントに関連付けられている場合(削除確認モーダルが削除ボタンに関連付けられているなど)、ポータルを使用する方が簡単です。それ以外の場合は、reuxアクションを使用します。@Kyleに同意すると、モーダルからモーダルを開くことができるはずです。また、ポータルはドキュメントの本文を追加するために追加されるため、デフォルトでポータルでも機能します。ポータルはお互いに適切にスタックされます(z-index:pですべてを台無しにするまで)
Sebastien Lorber

4
@DanAbramov、あなたの解決策は素晴らしいですが、私は小さな問題を抱えています。たいしたことはない。私はプロジェクトでMaterial-uiを使用しています。モーダルを閉じるときは、フェードアウェイアニメーションを「再生」するのではなく、単にオフにします。おそらく何らかの遅延を行う必要がありますか?または、すべてのモーダルをModalRoot内のリストとして保持しますか?提案?
gcerar 2016年

7
モーダルが閉じた後に特定の関数を呼び出したい場合があります(たとえば、モーダル内の入力フィールド値を使用して関数を呼び出します)。modalPropsアクションに関してこれらの関数を渡します。これは、状態をシリアライズ可能に保つというルールに違反します。この問題を解決するにはどうすればよいですか?
-chmanie

98

更新:React 16.0で導入されたポータルをリンク経由で更新ReactDOM.createPortal

更新:次のバージョンのReact(ファイバー:おそらく16または17)には、ポータルを作成するメソッドが含まれます:ReactDOM.unstable_createPortal() リンク


ポータルを使用する

Dan Abramovの回答の最初の部分は問題ありませんが、定型文が多く含まれています。彼が言ったように、ポータルを使用することもできます。その考えを少し拡張します。

ポータルの利点は、ポップアップとボタンがReactツリーの非常に近くにあり、プロップを使用した非常にシンプルな親子通信です。ポータルで非同期アクションを簡単に処理したり、親にポータルをカスタマイズさせたりできます。

ポータルとは何ですか?

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

たとえば、次のReactツリーを本文にレンダリングするとします。

<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-indexの問題はなくなりました。ポータルでにレンダリングできます<body>。ポップアップまたはドロップダウンを表示する場合、これはZインデックスの問題と戦う必要がない場合に非常に便利です。追加されるポータル要素document.bodyはマウント順に実行されます。つまり、で操作しない限りz-index、デフォルトの動作では、ポータルをマウント順に相互にスタックします。実際には、別のポップアップ内から安全にポップアップを開くことができ、2番目のポップアップが最初のポップアップの上に表示されることを確認しますz-index

実際には

最も単純:ローカルの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ツリー内で深くネストされたままなので、親がポータルに小道具を渡すことができるため、このポータルの動作をカスタマイズするのは非常に簡単です。ポータルを使用しない場合は、通常、Reactツリーの最上部にポップアップをレンダリングして、z-index理由については、通常、「ユースケースに応じて作成した汎用的なDeleteConfirmationPopupをどのようにカスタマイズするか」などについて考える必要があります。そして通常、入れ子の確認/キャンセルアクションを含むアクションのディスパッチ、翻訳バンドルキー、さらに悪い場合にはレンダリング関数(または他のシリアライズ不可能なもの)のディスパッチなど、この問題に対するかなりハックな解決策を見つけるでしょう。以来、あなたは、ポータルとそれを行う必要はありませんし、普通の小道具を渡すことができますDeleteConfirmationPopupだけの子でありますDeleteButton

結論

ポータルは、コードを簡略化するのに非常に役立ちます。もうなしではどうにもなりません。

ポータルの実装は、次のような他の便利な機能にも役立ちます。

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

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

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

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

オンボーディングホットスポット

実際の本番コードはこちら。より簡単にすることはできません:)

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

編集:ポータルを選択したノード(必ずしもボディではない)にレンダリングすることを許可する、反応ゲートウェイを発見しました

編集反応ポッパーは、反応テザーのまともな代替手段になり得るようです。PopperJSは、DOMに直接触れることなく、要素の適切な位置のみを計算するライブラリで、ユーザーはDOMノードを配置する場所とタイミングを選択でき、Tetherは本体に直接追加します。

編集:興味深い反応スロットフィルもあり、ツリー内の任意の場所に配置した予約済みエレメントスロットにエレメントをレンダリングできるようにすることで、同様の問題の解決に役立ちます


このサンプルスニペットでは、アクションを確認しても確認ポップアップは閉じません([キャンセル]をクリックしたときとは異なります)
dKab

ポータルのインポートをコードスニペットに含めると便利です。どのライブラリ<Portal>から来たのですか?それは反応ポータルだと思いますが、確かに知っておくといいでしょう。

1
@skypecakesは、私の実装を疑似コードと見なしてください。具体的なライブラリに対してはテストしていません。ここでは具体的な実装ではなく、コンセプトを教えようとしています。私はreact-portalに慣れており、上記のコードは問題なく動作するはずですが、ほぼすべての同様のlibで問題なく動作するはずです。
Sebastien Lorber 2017年

反応ゲートウェイは素晴らしいです!サーバーサイドレンダリングをサポートします:)
キリルス2017年

私はかなり初心者なので、このアプローチについての説明があればとても嬉しいです。実際にモーダルを別の場所でレンダリングする場合でも、このアプローチでは、モーダルの特定のインスタンスをレンダリングする必要がある場合は、すべての削除ボタンをチェックする必要があります。reduxアプローチでは、モーダルのインスタンスが1つしか表示されません。パフォーマンスの問題ではないですか?
Amit Neuhaus

9

このトピックについて、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>)
// ...

2

私の意見では、最低限の実装には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;

0

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

class ModalContainer 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} />  
      ...
    )
  }

}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.