Force PushedFP
  • Home
  • Blog
  • Workbooks

My goto Redux / React application pattern (not create react app)

Since I talk about a good amount of React / Redux work on here, I thought it'd be appropriate to share the patterns I like to use when I build React / Redux applications.

I'll break down my patterns into the various concepts React and Redux provide.

I'm sure I can probably go more into detail about the intracacies of each, or how they differ from what Redux recommends whatever. I really like what React and Redux are doing so I'm not writing any specific implementation because I disagree with Redux - I do it because it works for me and it creates less work.

Maybe I'll revise this post at some point.

React bundle entry point

import Container from './containers/AppContainer'
import configureStore from './store/configureStore'
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'

const store = configureStore()

render(
    <Provider store={store}>
        <Container />
    </Provider>,
    document.getElementById('app')
)

Redux - configureStore

import actionListener from './middleware/listeners/Action'
import pageMiddleware from './middleware/Page'
import listenerMiddleware from './middleware/Listener'
import rootReducer from './reducers'

import { applyMiddleware, createStore } from 'redux'

const configureStore = (initialState) => {
    const middleware = [
        pageMiddleware,
        listenerMiddleware(
            actionListener,
        )
    ]

    const store = createStore(rootReducer, initialState, applyMiddleware(...middleware))

    return store
}

export default configureStore

Redux - Root reducer

import App from './App'
import { combineReducers } from 'redux'

const rootReducer = combineReducers({
    app: App,
})

export default rootReducer

Redux - App reducer

import * as types from './constants/ActionTypes'

export const createReducer = (initialState, handlers) => {
    return (state = initialState, action) => {
        if (typeof handlers[action.type] !== 'undefined') {
            return handlers[action.type](state, action)
        } else {
            return state
        }
    }
}

//...

export const reduxActionTypeReducer = (state, action) => {
    return {
        ...state,
        errors: {},
    }
}

//...

const reducers = {}
reducers[types.REDUX_ACTION_TYPE] = reduxActionTypeReducer

const appReducer = createReducer(getInitialState(), reducers)

export default appReducer

Redux - Action listener

import * as Actions from './actions'
import * as types from './constants/ActionTypes'

export default {
    //...
    [types.REDUX_ACTION_TYPE]: (action, dispatch, state) => {
        dispatch(Actions.reduxActionTypeAction(action))
        dispatch(Actions.reduxAjaxActionTypeAction({
          next: () => {
            console.log('Hello World')
          }
        }))
    },
    //...
}

Redux - Ajax listener

import * as Actions from '../../actions'
import * as types from '../../constants/ActionTypes'

const ajaxAsync = (url, method = 'GET') => {
    return async (action, dispatch, state) => {
        const { fail = Actions.setLoadingError, next, payload } = action

        const xhrApiMap = {
            DELETE: xhrDelete,
            GET: xhrGet,
            POST: xhrPost,
            PUT: xhrPut,
        }

        const xhrApiFunc = xhrApiMap[method]

        if (typeof xhrApiFunc === 'undefined') {
            throw new Error('Unsupported HTTP Method')
        }

        let query
        let body

        if (typeof payload !== 'undefined' && (method === 'PUT' || method === 'POST')) {
            body = payload
        }

        if (typeof payload !== 'undefined' && (method === 'DELETE' || method === 'GET')) {
            query = payload
        }

        const [error, response] = await xhrApiFunc({
            body,
            query,
            url,
        })

        if (typeof error !== 'undefined') {
            dispatch(
                fail({
                    config: {
                        method,
                        payload,
                    },
                    error,
                    url,
                })
            )
            return
        }

        // Received a signal to refresh the domain
        if (typeof response !== 'undefined' && response.refreshDomain === true) {
            dispatch(Actions.getShoppingCart())
            return
        }

        if (typeof next === 'function') {
            dispatch(next(response))
        }
    }
}

export default {
    [types.AJAX_REDUX_ACTION_TYPE]: ajaxAsync('/api'),
    // ...
}

Redux - Actions

import * as types from './constants/ActionTypes'

//...

export const reduxActionTypeAction = ({ fail, next, payload }) => ({
    fail,
    next,
    payload,
    type: types.REDUX_ACTION_TYPE,
})

export const reduxAjaxActionTypeAction = ({ fail, next, payload }) => ({
    fail,
    next,
    payload,
    type: types.AJAX_REDUX_ACTION_TYPE,
})

//...

Redux - Action types

export const REDUX_ACTION_TYPE = 'REDUX_ACTION_TYPE'
export const REDUX_ACTION_TYPE_2 = 'REDUX_ACTION_TYPE_2'
//...

Redux - Listener middleware

export default (...listeners) => (store) => (next) => (action) => {
    // listeners are provided with a picture
    // of the world before the action is applied
    const preActionState = store.getState()

    // release the action to reducers before
    // firing additional actions
    next(action)

    // always async
    setTimeout(() => {
        listeners.forEach((listener) => {
            if (listener[action.type]) {
                listener[action.type](action, store.dispatch, preActionState)
            }
        })
    })
}

Redux - Container

import React from 'react'
import * as Actions from './actions'
import ReactComponent from './components/ReactComponent'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

class Container extends React.Component {

    render() {
        const boundActions = bindActionCreators(Actions, this.props.dispatch)
        return <ReactComponent {...this.props} {...boundActions} />
    }
}

const mapStateToProps = (state) => {
    const { app } = state

    return {
      app
    }
}

export default connect(mapStateToProps)(Container)

And a React component to demonstrate some Redux action

import React from 'react'

const ReactComponent = (p) => (
  <div className="some-className">
    <button onClick={p.reduxActionTypeAction}>
      Click Me
    </button>
  </div>
}

export default ReactComponent