Skip to content

Instantly share code, notes, and snippets.

@0xdevalias
Last active Mar 10, 2019
Embed
What would you like to do?
Implementing Redux style reducers/actions/middleware with React Hooks

I wanted to play around with React hooks, and I like patterns from Redux, so I figured.. why not combine them?

This implementes a Proof of Concept (PoC) 'local state' using hooks/reducers, but also has the ability to use Redux middleware (eg. thunks, sagas, etc)

const mapOwnPropsToInitialState = ownProps => {
return {
foo: ownProps.defaultFoo || '',
bar: true,
}
}
const mapStateToProps = state => {
return {
foo: state.get('foo'),
bar: state.get('bar'),
}
}
const mapDispatchToProps = dispatch => {
return {
handleSomeEvent: event => dispatch(myAction(event)),
}
}
const FooContainer = wrapWithHookStateReducer({
reducer,
mapOwnPropsToInitialState,
mapStateToProps,
mapDispatchToProps
})(Foo)
export default FooContainer
export const getDisplayName = WrappedComponent =>
WrappedComponent.displayName || WrappedComponent.name || 'Component'
export const withWrappedDisplayName = (
wrapperDisplayName,
wrappedComponent,
component
) => {
const innerDisplayName = getDisplayName(wrappedComponent)
component.displayName = `${wrapperDisplayName}(${innerDisplayName})`
return component
}
import React, { useReducer } from 'react'
import { compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { fromJS } from 'immutable'
import { withWrappedDisplayName } from './helpers'
// Based off react-redux's connect(), but we use hooks instead of a global Redux store
// Ref: https://github.com/reduxjs/react-redux/blob/master/docs/api/connect.md
export const wrapWithHookStateReducer = ({
reducer,
mapOwnPropsToInitialState,
mapStateToProps,
mapDispatchToProps,
mergeProps,
middlewares = [thunkMiddleware]
}) => WrappedComponent => {
if (!reducer) throw Error("'reducer' is a required parameter")
if (!Array.isArray(middlewares)) throw Error("'middlewares' must be an array")
return withWrappedDisplayName(
'WithHookStateReducer',
WrappedComponent,
ownProps => {
const initialState = mapOwnPropsToInitialState(ownProps)
const [state, rawDispatch] = useReducer(reducer, fromJS(initialState))
// TODO: Can we optimize this?
// https://reactjs.org/docs/hooks-reference.html#usecallback
// https://reactjs.org/docs/hooks-reference.html#usememo
const { dispatch } = applyMiddleware(state, rawDispatch)(...middlewares)
let stateProps = {}
if (mapStateToProps) {
stateProps = mapStateToProps(state, ownProps)
}
let dispatchProps = {}
if (mapDispatchToProps) {
dispatchProps = mapDispatchToProps(dispatch, ownProps)
}
let mergedProps = { ...ownProps, ...stateProps, ...dispatchProps }
if (mergeProps) {
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
}
return <WrappedComponent {...mergedProps} />
}
)
}
// Ref: https://github.com/reduxjs/redux/blob/3f93d6bb21fd104263bc83da87bd2e113e82bd9f/src/applyMiddleware.js#L3-L41
const applyMiddleware = (state, rawDispatch) => (...middlewares) => {
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: () => state,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(rawDispatch)
return {
getState: () => state,
dispatch
}
}
// TODO: How could we best implement Saga's in this, ideally entirely within this HoC
//
// import createSagaMiddleware from 'redux-saga'
// const sagaMiddleware = createSagaMiddleware()
// applyMiddleware(sagaMiddleware, thunkMiddleware)
// sagaMiddleware.run(rootSaga)
//
// https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect
// https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
//
// createSagaMiddleware
// https://github.com/redux-saga/redux-saga/blob/6d7cb99cf489da0b4de72eee44b7333d3c8f2cba/packages/core/index.d.ts#L71-L105
// SagaMiddleware.run -> Task
// https://github.com/redux-saga/redux-saga/blob/6d7cb99cf489da0b4de72eee44b7333d3c8f2cba/packages/core/index.d.ts#L129-L169
// Task
// https://github.com/redux-saga/redux-saga/blob/master/packages/types/index.d.ts#L141-L173
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment