Skip to content

Stop opening four different files to code a simple feature when using redux

License

Notifications You must be signed in to change notification settings

RealmTeam/react-redux-reliever

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

81 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-redux-reliever

npm version code style: prettier

From redux's creator himself Dan Abramov :

Why is this stuff in five different files and constants SHOUTING at me

react-redux-reliever is a simple package that aims to relieve you from the pain of opening multiple different files and write a lot of boring code each time you want to add a small feature in your React app.

The principle is as follow : regroup all the logic from redux in a single file when you're developing new features while defaulting to certain behaviors to save you some time. You can easily override anything you don't like so you're not stuck either.

It obviously uses react and redux.

Getting started

Install

$ npm install --save react-redux-reliever

or

$ yarn add react-redux-reliever

Plugins

   import RelieverRegistry, {plugins} from 'react-redux-reliever'

   RelieverRegistry.use(plugins.RxRelieverPlugin)
   import RelieverRegistry, {plugins} from 'react-redux-reliever'

   RelieverRegistry.use(plugins.SagaRelieverPlugin)

You may also create your own plugins using the following interface.

	class SomePlugin {
    		createMiddleware(reliever) {}
	        setupStore(store) {}
	}
    
    RelieverRegistry.use(SomePlugin)

Usage Example

First, import RelieverRegistry and Reliever

import RelieverRegistry, {Reliever} from 'react-redux-reliever'

Create a class that extends Reliever

class ComponentReliever extends Reliever {

ACTION_PREFIX is used if you want to use the default behavior for the reducer which is to check if the action type starts with this prefix and merges the action payload (action.payload = {}) with the state if that's the case.
Leave it out if you don't wish to use the default behavior.

    ACTION_PREFIX = 'WHATEVER'

The initial state for the reducer. Note that it will be transformed to a SeamlessImmutable object.

    getInitialState() {
        return {
            value: null
        }
    }

getActions is where you define actions that could be used by other containers (otherwise, simply use the payload)

    getActions() {
        return {
            doSomething: () => ({type: 'WHATEVER_ACTION'}),
            doSomethingElse: () => ({type: 'WHATEVER_ACTION_ASYNC'})         
        }
    }

The reducer is not required but you can still override it. Here we have a mix between the default react-redux-reliever behavior and the standard redux behavior.
Don't forget that the state is a SeamlessImmutable object.
The most frequent case for a mixed reducer is something needing to be added to some value in the state.

    reducer(state, action) {
        switch (action.type) {
            case 'WHATEVER_ADD':
                return state.set('value', state.value + "!")
            default:
                // Don't forget to call super
                return super.reducer(state, action)
        }
    }
}

export default ComponentReliever

Using Rx

Create your epics (see redux-observable). All methods that have a name ending with 'Epic' will be used.

    import {Reliever} from 'react-redux-reliever'
    import {flatMap} from 'rxjs/operators'
    import {defer, map, mapTo} from 'rxjs'

    class SomeReliever extends Reliever {

        someEpic(action$) {
            return action$
                .ofType('WHATEVER_ACTION') // takes every action of type 'WHATEVER_ACTION' from the action stream
                .pipe(mapTo({type: 'WHATEVER_UPDATE', payload: {value: 'foo'}})) // then maps the action to an action of type 'WHATEVER_UPDATE'. payload will be applied to the state automatically without using a reducer
        }

        // you can also easily handle async actions
        someAsyncEpic(action$) {
            return action$
                .ofType('WHATEVER_ACTION_ASYNC')
                .pipe(
                  flatMap(action =>
                    defer(async () => {
                      const result = await fetch(`https://some-api.com/foo?userId=${action.userId}`)
                      return await result.json()
                    })
                  ),
                  map(json => ({
                     type: 'WHATEVER_USER_DATA_FETCHED',
                     payload: {
                       userData: json.foo
                     }
                  }))
                )
        }
    }

RxRelieverPlugin also provides extensions for rxjs to provide you with convenient methods to access and observe the store and state

    import {plugins} from 'react-redux-reliever'
    const extensions = plugins.RxRelieverPlugin.extensions()
    extensions.getStore() // store observable, triggers once upon subscription
    extensions.getState() // state observable, triggers once upon subscription
    extensions.getState('substate') // substate observable, triggers once upon subscription
    extensions.observeState() // state observable, triggers when the state changes
    extensions.observeState('substate') // substate observable, triggers when the state changes
    extensions.reduxActionStream() // returns the global action stream

This allows you to build complex sequences of actions while leveraging the flexibility and operators of rxjs

    fooEpic(action$) {
        const shouldStop$ = extensions.observeState('substate').pipe(
            map(state => state.toMutable().someProp),
            filter(prop => prop === 'foo'), // this observable will trigger when the property someProp === 'foo'
            take(1) // unsubscribe once the filter operator has triggered
        )

        return action$.ofType('WHATEVER_FOO_ACTION').pipe(
            mapTo({type: 'WHATEVER_SOMETHING_ELSE'}),
            takeUntil(shouldStop$)
        )
    }

Using saga

add a *saga() method to your reliever and add all your saga logic there

import {takeLatest} from 'redux-saga/effects'

class MyReliever extends Reliever {
	*handleSomeAction(action) {
		// do something
	}
	
	*saga() {
		yield takeLatest('SOME_ACTION', this.handleSomeAction.bind(this))
	}
}

Adding the store

in your store file

import RelieverRegistry, {plugins} from "react-redux-reliever"

We can register our plugins and reliever(s) to the registry

RelieverRegistry.register(ComponentReliever, "whatever")

RelieverRegistry.use(plugins.RxRelieverPlugin)
RelieverRegistry.use(plugins.SagaRelieverPlugin)
RelieverRegistry.use(MyOwnPlugin)

We can then use the registry to create the store and rootReducer like so

const rootReducer = RelieverRegistry.buildRootReducer()

// You can pass an object to include other reducers you may have
// By default everything will be on the same level in your store but you can pass
// an extra argument to put reducers from the registry on another level
const rootReducer = RelieverRegistry.buildRootReducer({
    otherReducer: myOtherReducer
}, "customLevelInStore")

const store = createStore(RelieverRegistry.buildRootReducer(), applyMiddleware(...RelieverRegistry.middlewares(), logger))
RelieverRegistry.setupStore(store)

Now you can connect your component to the store.

You can choose to do it the usual way or you can use our custom connect function. In your Component.js, we import the necessary functions

import RelieverRegistry, {connect} from "react-redux-reliever"

The connect function takes two named parameters : props and functions

props is exactly like mapStateToProps except that you also need to return ownProps (that way you are able to easily remove unwanted props)

    props(state, ownProps) {
        // moduleState is used to retrieve the module's state in the whole store
        return {value: RelieverRegistry.moduleState("whatever", state).get('value'), ...ownProps}
    }

functions is the same as mapDispatchToProps except that you have access to all the props returned by props.

    functions(ownProps, dispatch) {
        return {
            test: () => {
                // Note that we generally don't use action creators.
                // This is purely a choice and you can still use them if you want.
                dispatch({type: 'WHATEVER_TEST', payload: {value: "Looking good !"}})
            },
            doSomething: () => {
                // If you want to use action creators from a module, you could do so like that by using its name
                dispatch(RelieverRegistry.moduleActions("whatever").doSomething())
            }
        }
    }

In the end, connect is used like that

export default connect({
    props: (state, ownProps) => {/* Your code here */},
    functions: (ownProps, dispatch) => {/* Your code here */}
})(Component)

That's it !

Building examples from sources

$ git clone https://github.com/aronse/react-redux-reliever.git
$ cd react-redux-reliever
$ npm install

As of today, only a simple counter example has been implemented.

Counter example

$ npm run counter

Contributing

Feel free to open issues and submit pull-requests.

About

Stop opening four different files to code a simple feature when using redux

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published