Skip to content

KarimAziev/redux-saga-modal

Repository files navigation

redux-saga-modal

Manage modals with redux-saga.

Install

npm i redux-saga-modal

Or

yarn add redux-saga-modal

Usage

Pass the reducer to your store. It keeps the state of all your modal components, so you only have to pass it once.

import { createStore, combineReducers } from 'redux';
import { reducer as modalReducer } from 'redux-saga-modal';

const rootReducer = combineReducers({
  // ...your other reducers
  // modalReducer should be mounted under 'modals' key,
  modals: modalReducer,
});

const store = createStore(rootReducer);

Connect a component to Redux Store with sagaModal.

import {
  sagaModal,
  createModal,
  SagaModalInjectedProps,
} from 'redux-saga-modal';
import ReactModal from 'react-modal';
import { ReactNode } from 'react';

ReactModal.setAppElement('#root');

export interface ConfirmModalOwnProps {
  title: ReactNode;
  text: ReactNode;
}

const Confirm: React.FC<ConfirmModalOwnProps & SagaModalInjectedProps> = ({
  isOpen,
  submit,
  hide,
  title,
  text,
}) => {
  const handleSubmit = () => {
    submit();
  };
  return (
    <ReactModal isOpen={isOpen} onRequestClose={hide} closeTimeoutMS={500}>
      <h3>{title}</h3>
      <p>{text}</p>
      <div>
        <button onClick={hide}>Cancel</button>
        <button onClick={handleSubmit}>Confirm</button>
      </div>
    </ReactModal>
  );
};

const modal = createModal('confirm');

const ConnectedConfirmModal = sagaModal({
  name: modal.name,
})(Confirm);

export { modal, ConnectedConfirmModal };

To create an instance use createModal and pass the modal name as the first argument.

import { createModal } from 'redux-saga-modal';
const modal = createModal('CONFIRM');

Result

{
  name: 'CONFIRM',
  selector: function (r) {},
  patterns: {
    show: function (n) {},
    hide: function (n) {},
    destroy: function (n) {},
    update: function (n) {},
    click: function (n) {},
    submit: function (n) {},
  },
  actions: {
    show: function (n) {},
    update: function (n) {},
    submit: function (n) {},
    click: function (n) {},
    hide: function () {},
    destroy: function () {},
  },
  takeShow: function (t) {},
  takeUpdate: function (t) {},
  takeClick: function (t) {},
  takeDestroy: function (t) {},
  takeSubmit: function (t) {},
  takeHide: function (t) {},
  show: function (r) {},
  update: function (r) {},
  click: function (r) {},
  submit: function (r) {},
  hide: function () {},
  destroy: function () {},
  select: function () {},
};

All methods already bound to the modal's name so you don't need manually pass it. The instance will receive properties actions, patterns, name, selector and high-level effects show, update, hide, submit, click, destroy, takeShow, takeUpdate, takeHide, takeSubmit, takeClick and takeDestroy.

Both patterns and actions have methods named show, update, hide, submit, click and destroy.

import { race } from 'redux-saga/effects';
import { createModal } from 'redux-saga-modal';

const modal = createModal('confirm');

const confirmModal = function* (initProps: ConfirmModalOwnProps) {
  yield modal.show(initProps);

  const [submit]: boolean[] = yield race([
    modal.takeSubmit(),
    modal.takeHide(),
  ]);

  yield modal.hide();

  return !!submit;
};

Alternatively if you don't need high-level effects you can use createModalHelpers. The example above can be rewritten like this:

import { race, put, take } from 'redux-saga/effects';
import { createModalHelpers } from 'redux-saga-modal';
import { ConfirmModalOwnProps } from './Modal';

const modal = createModalHelpers('confirm');
const confirmModal = function* (initProps: ConfirmModalOwnProps) {
  yield put(modal.actions.show(initProps));

  const [submit]: boolean[] = yield race([
    take(modal.patterns.submit()),
    take(modal.patterns.hide()),
  ]);

  yield put(modal.actions.hide());

  return !!submit;
};

For frequently repeated modals tasks create a config object with modals names as keys and tasks as values. Pass the config as the argument to sagas and fork it in your rootSaga.

Your tasks will be called with a this context every time when an action showModal has been dispatched with it's name and cancelled on destroyModal.

The context of your task will have properties actions, patterns, name, selector and high-level effects show, update, hide, submit, click, destroy, takeShow, takeUpdate, takeHide, takeSubmit, takeClick and takeDestroy.

Both patterns and actions have methods named show, update, hide, submit, click and destroy.

import { race, call, fork, all } from 'redux-saga/effects';
import {
  sagas as modalsSaga,
  SagaModalInstance,
  SagaModalAction,
} from 'redux-saga-modal';

interface CommentData {
  id: number;
  comment: string;
}

const saveComment = (data: CommentData) =>
  new Promise((resolve) => setTimeout(() => resolve({ ok: true }), 1000));

const saveCommentWorker = function* (this: SagaModalInstance) {
  const [submitAction]: SagaModalAction<CommentData>[] = yield race([
    this.takeSubmit(),
    this.takeHide(),
  ]);

  if (submitAction) {
    yield this.update({
      title: 'Saving',
      loading: true,
    });

    yield call(saveComment, submitAction.payload as CommentData);
    yield this.update({
      title: 'Changes saved',
      loading: false,
    });
  }

  yield this.hide();
};

const modalsTasks = {
  saveComment: saveCommentWorker,
};

export default function* rootSaga() {
  yield all([fork(modalsSaga, modalsTasks)]);
}

API

Actions Creators

Basic actions creators.

Name Arguments Description
showModal (name: String, payload: Object = {}) Triggers to show a modal and sets payload as props
updateModal (name: String, payload: Object = {}) Updates the modal by merging payload with it's current props
submitModal (name: String, payload: any) Triggers that a target's button was pressed
clickModal (name: String, payload: any) A trigger for handling any click on modal
hideModal (name: String) Hide modal without destroying its state in the redux store
destroyModal (name: String) Close a modal by destroying its props

Instance Creators

createModal

Creates a modal instance with properties name, selector, actions, patterns, and effects creators show, update, hide, submit, click, destroy, takeShow, takeUpdate, takeHide, takeSubmit, takeClick and takeDestroy.

Both patterns and actions includes methods named show, update, hide, submit, click and destroy. All methods refer to the modal's name so you don't need manually pass it.

Arguments

  • name(String)(Required) the name of a modal

  • config(Object)

    • getModalsState (Function) A selector that takes the Redux store and returns the slice which corresponds to where the redux-saga-modal reducer was mounted. By default, the reducer is mounted under the modals key.
import { createModal } from 'redux-saga-modal';

const {
  name,
  selector,
  //patterns creators used for filtering actions inside take effects
  patterns: { show, click, submit, update, hide, destroy },
  //action creators
  actions: { show, click, submit, update, hide, destroy },
  //put effects (already wrapped in redux-saga put)
  show,
  click,
  submit,
  update,
  hide,
  destroy,
  //take effects (already wrapped in redux-saga take)
  takeShow,
  takeClick,
  takeSubmit,
  takeUpdate,
  takeHide,
  takeDestroy,
} = createModal('CONFIRM');

createModalHelpers

Creates a modal instance with properties actions, patterns, selector and name.

Both patterns and actions have methods named show, update, hide, submit, click and destroy.

Arguments

  • name(String)(Required) the name of a modal

  • config(Object)

    • getModalsState (Function) A selector that takes the Redux store and returns the slice which corresponds to where the redux-saga-modal reducer was mounted. By default, the reducer is mounted under the modals key.
import { createModalHelpers } from 'redux-saga-modal';

const {
  name,
  selector,
  patterns: { show, click, submit, update, hide, destroy },
  actions: { show, click, submit, update, hide, destroy },
} = createModalHelpers('CONFIRM');

Instance properties

name

The name of the modal instance which was passed to createModal or createModalHelpers. Should be the same as passed one in sagaModal.

actions

Same as basic actions creators but the first argument is already bound to the modal name.

Name Arguments Description
show (payload: Object = {}) Triggers to show a modal and sets payload as it's props
update (payload: Object = {}) Updates the modal by merging payload with it's current props
submit (payload: any) Triggers that a target's button was pressed
click (payload: any) A trigger for handling any click on modal
hide () Hide modal without destroying its state in the redux store
destroy () Close a modal by destroying its props

Example

import { createModal, createModalActions, showModal } from 'redux-saga-modal';

const modal = createModal('CONFIRM');
yield put(modal.actions.show({ title: 'Some title' }));

//or
const confirmActions = createModalActions('CONFIRM');
yield put(confirmActions.show({ title: 'Some title' }));

//or
yield put(showModal('CONFIRM', { title: 'Some title' }));

patterns

Matcher methods for filtering modal actions inside redux-saga take effects, such as take, takeLatest, takeEvery etc. Every pattern accepts optional argument for checking payload.

Payload pattern has the same meaning and rules as in the redux-saga but applies not for the whole action but only to its payload.

Name Arguments Description
show payloadPattern?: String Function
update payloadPattern?: String Function
submit payloadPattern?: String Function
click payloadPattern?: String Function
hide An action matcher for hideModal with the same name as it's instance has
destroy An action matcher for destroyModal with the same name as it's instance has

Example

import { createModal } from 'redux-saga-modal';

const { patterns } = createModal('CONFIRM');

//result: take(action => action.type === clickModal().type && action.meta.name === 'CONFIRM' && payload === 'value'
yield take(patterns.click(payload === 'value'));

//result: take(action => action.type === clickModal().type && action.meta.name === 'CONFIRM' && payload && payload.text === 'Some text'

yield take(patterns.show((payload) => payload && payload.text === 'Some text'));

//result: take(action => action.type === clickModal().type && action.meta.name === 'CONFIRM'
yield take(patterns.click);

selector

import { createModal } from 'redux-saga-modal';

const { selector, ...effects } = createModal('CONFIRM');

// the same
yield select(selector);
yield effects.select();

effects

Produced with createModal or createModalEffects. Returns scoped put, select and take effects.

Payload pattern in the take effects has the same meaning and rules as in's the redux-saga, but applies not for the whole action but only to the payload.

Name Arguments Effect
show (payload: Object) put Triggers to show a modal and sets payload as it's props
update (payload: Object) put Updates the modal by merging payload with it's current props
submit (payload: any) put Triggers that a target's button was pressed
click (payload: any) put A trigger for handling any click on modal
hide (name: String) put Hide modal without destroying it's state in the redux store
destroy (name: String) put Close a modal by destroying it's props
select (name: String) select Select a modal state from the Redux Store
takeShow (payloadPattern?: String Function Array
takeUpdate (payloadPattern?: String Function Array
takeSubmit (payloadPattern?: String Function Array
takeClick (payloadPattern?: String Function Array
takeHide take Returns a take effect for an action hideModal with scoped name
takeDestroy take Returns a take effect for an action destroyModal with scoped name

Example

import { createModal } from 'redux-saga-modal';

const { name, ...effects } = createModal('CONFIRM');

//result: put(showModal('CONFIRM', ({ text: 'Some text' }))
yield effects.show({ text: 'Some text' });

//result: take(action => action.type === clickModal().type && action.meta.name === 'CONFIRM' && payload === 'value'
yield effects.takeClick('value');

//result: take(action => action.type === clickModal().type && action.meta.name === 'CONFIRM' && payload && payload.text === 'Some text'
yield effects.takeClick((payload) => payload && payload.text === 'Some text');
//result: take(action => action.type === clickModal().type && action.meta.name === 'CONFIRM'
yield effects.takeClick();

sagas

Invoke your sagas with a this context whenever an action showModal has been dispatched with passed name and cancelled on destroyModal.

Arguments

  • config(Object)(Required)

    • <[key: ModalName], Saga: Generator>

sagaModal

Connects a component to Redux store and injects next props:

  • modal: { name }
  • name;
  • isOpen;
  • show;
  • update;
  • destroy;
  • click;
  • submit;
  • hide;
  • showModal;
  • updateModal;
  • submitModal;
  • clickModal;
  • hideModal;
  • destroyModal;

Arguments

  • name(String)(Required) the name of a modal

  • config(Object)

    • name (String)(Required) the name of a modal
    • getModalsState (Function) A selector that takes the Redux store and returns the slice which corresponds to where the redux-saga-modal reducer was mounted. By default reducer is mounted under the 'modals' key: state => state.modals
    • actions (Object) Custom actions to bind with redux dispatch
    • keepComponentOnHide (Boolean) Whether keep modal component when isOpen equals false By default equals false
    • destroyOnHide (Boolean) Whether automatically dispatch destroy to after hide. By default equals true

reducer

The modals reducer keeps state of the all modals. Should be mounted to redux store under name modals or any other but in this case you need to pass selector getModalsState to sagaModal, createModal and createModalHelpers.

createModalActions

Arguments

  • name(String)(Required) the name of a modal

Creates actions creators show, update, hide, submit, click and destroy bound to the name of the modal.

createModalEffects

Arguments

  • name (String)(Required) the name of a modal

Creates effects show, update, hide, submit, click and destroy bound to the name of the modal.

createModalPatterns

Arguments

  • name(String)(Required) the name of a modal

Creates patterns creators show, update, hide, submit, click and destroy bound to the name of modal. Every pattern accepts optional matcher for checking payload.

License

MIT © KarimAziev