2.1.0 • Published 3 years ago

redux-action-hooks v2.1.0

Weekly downloads
-
License
ISC
Repository
-
Last release
3 years ago

redux-hooks

Simple middleware for side effects in redux

Inspiration

This is inspired by Angular's side effects based on RXJS without all rx library. Redux already has an ability to observe the incoming redux actions and exposes the middleware for the side effects. This library creates very simple API to interact with redux action flow and create simple clear and testable logic.

example

export const topUpEffect = ({ action, dispatch }) => {
  if(action.payload.amount > 100) {
    api.post(URL, action.payload)
    .then((payload) => {
      dispatch(topUpSuccess(payload))
    })
  }
}

ofType('ACCOUNT_TOPUP_REQUEST', topUpEffect)

This simplifies testing and decouples any asnchronous code or third party logic from the redux flow from action creator to reducer

How it works

This is Redux middleware

This middleware calls next(action) as the first statement, so your hook is executed AFTER any other middleware in the pipe which is added subsequently. However, this tool is designed for side effects which are not dependent on the sequence how the middleware is executed.

This library is intended for application with single redux store or if the application has more then one redux store, it will work only with one of them.

It is initialized as any other redux middleware by passing the Hooks into the createMiddleware function

import Hooks from 'redux-action-hooks'

const store = createStore(reducer, applyMiddleware(...othermiddleware, Hooks));

To hook the side effect every hook needs to be registered once. Depending on directory structure it can be run from the createStore file, or if application grows larger it is good idea to store the hooks together with reducers.

.
..
action_creators.js
action_types.js
reducer.js
effects.js

in reducer:

import './hooks.js'

the above code in reducer.js will register all hooks once, since the reducer.js code is executed only once.

Examples

ofType

Observes the type of the action and allows to execute extra logic if the action is in the redux flow.

NOTE - modifying action is not recommended. It can yield unexpected results.

import { ofType } from 'redux-action-hooks'

ofType('action_type_one', ({ action, getState, dispatch }) => {
  console.log(`Hello triggered by "${action.type}".`)
})

// running somewhere in the code will cause the following console output
dispatch({ action_type_one })

> Hello triggered by "action_type_one".

// passing multiple actions will cause the function 'runSideEffect'
// to be executed whenever any action in the list is dispatched
ofType(['action_type_one', 'action_type_two'], runSideEffect);

pipe

pipe simply dispatches another action with the identical payload to the first action(s)

import { pipe } from 'redux-action-hooks'

pipe(['action_type1'], 'action_type2')

ofType('action_type2', () => { console.log('hello world') })

//running somewhere in the code:
dispatch({ type: action_type1 }) // will create console output

> hello world

allCalled

oberves all action given in the array, and exectues the side effects only after all of them had been called at least once. The hook takes opional parameter in the form of { cleanup: boolean } which indicates whether to cleanup the call history once the side effect is run. If not given or set to false the side offect will run on every subseqent call of any given action passed into the hook. #####expample

import { allCalled } from 'redux-action-hooks'

allCalled(['action_A', 'action_B'], () => {  console.log('hello world') }, { cleanup: false })

dispatch({ type: action_A });
// no console output
dispatch({ type: action_B })
> hello world
dispatch({ type: action_A });
> hello world // prints hello world again
allCalled(['action_A', 'action_B'], () => {  console.log('hello world') }, { cleanup: true })

dispatch({ type: action_A });
// no console output
dispatch({ type: action_B })
> hello world
dispatch({ type: action_A });
// no console output
dispatch({ type: action_B })
> hello world

Required

Redux is the only dependency. More specifically it is @types/redux (or types from redux if supplied), However if you are not using typescript, there is no 3rd party dependency besides it is expected to be used with Redux middleware.

How to use it

Installation

npm install redux-action-hooks

Usage

if using redux-thunk this need to go after thunk in the pipeline

ideally hooks will sit in separate file some-hooks.ts (some-hooks.js) along the other redux files (reducers, action creators)

and will be called in the beginning of dedicated reducer

in MyReducer:

import './some-hooks'

which will hook the side effect to the action.

ofType([ActionTypes.PDF_DOCUMENT_FOLDER_SELECTED], ({ action: AnyAction, dispatch: Dispatch }) => {
  someSideEffectCode();
  dispatch({ type: 'other_action' })
})

It is possibe to hook to multiple actions at the same time. e.g. if the app need to perform logout after security breach or after user logout action

ofType([SecurityTypes.INVALIDATE_SESSION, UserAction.LOGOUT], ({ action: AnyAction }) => {
  makeSessionInvalid();
  setLocation('/login');
})

Testing

the effects are here to imporove the testability of the code traditional approach - all the async logic is in action creator

const export myFetchAction = (url) => (dispatch, getState) => {
  dispatch(startFetch());
  myService.fetch(action.payload.url)
    .then((result) => {
      dispatch(successAction(result))
    })
    .catch((error) => {
      dispatch(errorAction(error))
    })
}
export const mySideEffect = ({ action, dispatch, getState }) => {
  myService.fetch(action.payload.url)
    .then((result) => {
      dispatch(successAction(result))
    })
    .catch((error) => {
      dispatch(errorAction(error))
    })
}

ofType([ EXAMPLE_START ], mySideEffect);

which looks already much better then packing all the async logic in the action creator. Since the effect function is used only in one place we actually can take all the 3rd party services outside of the function and inject them only when we need to use the effect.

export const mySideEffect = (service) => ({ action, dispatch }) => {
  service.fetch(action.payload.url)
    .then((result) => {
      dispatch(successAction(result))
    })
    .catch((error) => {
      dispatch(errorAction(error))
    })
}

ofType([ EXAMPLE_START ], mySideEffect(mySideEffectService));

in the unit test we can use very simple mock for our service, 3rd party library or store methods like dispatch and getState.

License

ISC

2.1.0

3 years ago

2.0.4

3 years ago

2.0.3

3 years ago

2.0.2

3 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago