From 766ab2f4e9d6be79e9171d38b87adac4f96e332c Mon Sep 17 00:00:00 2001 From: Ward Oosterlijnck Date: Tue, 16 Apr 2019 19:26:01 +1000 Subject: [PATCH 1/6] createReducer for redux middleware, related #164 --- package.json | 2 + src/__stories__/createReducer.story.tsx | 56 +++++++++++++++++++++++++ src/createReducer.ts | 35 ++++++++++++++++ src/index.ts | 2 + yarn.lock | 17 ++++++++ 5 files changed, 112 insertions(+) create mode 100644 src/__stories__/createReducer.story.tsx create mode 100644 src/createReducer.ts diff --git a/package.json b/package.json index 9a24b263..ad05cffd 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,8 @@ "react-hooks-testing-library": "0.4.0", "react-spring": "6.1.10", "rebound": "0.1.0", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0", "rimraf": "2.6.3", "rxjs": "6.4.0", "semantic-release": "15.13.3", diff --git a/src/__stories__/createReducer.story.tsx b/src/__stories__/createReducer.story.tsx new file mode 100644 index 00000000..1edaf91e --- /dev/null +++ b/src/__stories__/createReducer.story.tsx @@ -0,0 +1,56 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import logger from 'redux-logger'; +import thunk from 'redux-thunk'; + +import { createReducer } from '..'; +// import ShowDocs from './util/ShowDocs'; + +const useThunkReducer = createReducer(thunk, logger); + +// React useReducer lazy initialization example: https://reactjs.org/docs/hooks-reference.html#lazy-initialization +function init(initialCount) { + return { count: initialCount }; +} + +function reducer(state, action) { + switch (action.type) { + case 'increment': + return { count: state.count + 1 }; + case 'decrement': + return { count: state.count - 1 }; + case 'reset': + return init(action.payload); + default: + throw new Error(); + } +} + +const Demo = ({ initialCount = 1 }) => { + // Action creator to increment count, wait a second and then reset + const addAndReset = React.useCallback(() => { + return dispatch => { + dispatch({ type: 'increment' }); + + setTimeout(() => { + dispatch({ type: 'reset', payload: initialCount }); + }, 1000); + }; + }, [initialCount]); + + const [state, dispatch] = useThunkReducer(reducer, initialCount, init); + + return ( +
+
{JSON.stringify(state, null, 2)}
+ + + + +
+ ); +}; + +storiesOf('State|createReducer', module) + // .add('Docs', () => ) + .add('Demo', () => ); diff --git a/src/createReducer.ts b/src/createReducer.ts new file mode 100644 index 00000000..7ff5fd3b --- /dev/null +++ b/src/createReducer.ts @@ -0,0 +1,35 @@ +import { useReducer } from 'react'; + +function compose(...funcs) { + if (funcs.length === 0) { + return arg => arg; + } + + if (funcs.length === 1) { + return funcs[0]; + } + + return funcs.reduce((a, b) => (...args) => a(b(...args))); +} + +const createReducer = (...middlewares) => (...args) => { + const [state, dispatch] = useReducer(...args); + + let middlewareDispatch = () => { + 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) => middlewareDispatch(...args), + }; + const chain = middlewares.map(middleware => middleware(middlewareAPI)); + middlewareDispatch = compose(...chain)(dispatch); + + return [state, middlewareDispatch]; +}; + +export default createReducer; diff --git a/src/index.ts b/src/index.ts index 3923f60c..bd458171 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import createMemo from './createMemo'; +import createReducer from './createReducer'; import useAsync from './useAsync'; import useAsyncFn from './useAsyncFn'; import useAsyncRetry from './useAsyncRetry'; @@ -72,6 +73,7 @@ import useWindowSize from './useWindowSize'; export { createMemo, + createReducer, useAsync, useAsyncFn, useAsyncRetry, diff --git a/yarn.lock b/yarn.lock index 95ca898a..d71ee0ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5007,6 +5007,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ= + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -11333,6 +11338,18 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + integrity sha1-91VZZvMJjzyIYExEnPC69XeCdL8= + dependencies: + deep-diff "^0.3.5" + +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + refractor@^2.4.1: version "2.7.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.7.0.tgz#3ed9a96a619e75326a429e644241dea51be070a3" From 6af59e959b1055dbf3c4a69ef00f5d9fa59dc7ed Mon Sep 17 00:00:00 2001 From: areschen Date: Thu, 9 May 2019 11:18:47 +0800 Subject: [PATCH 2/6] chore: use useRef to save state make async action to get current state --- src/createReducer.ts | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/createReducer.ts b/src/createReducer.ts index 7ff5fd3b..f68a3f7f 100644 --- a/src/createReducer.ts +++ b/src/createReducer.ts @@ -1,35 +1,37 @@ -import { useReducer } from 'react'; +import { useMemo, useRef, useState } from 'react'; -function compose(...funcs) { - if (funcs.length === 0) { - return arg => arg; - } - - if (funcs.length === 1) { - return funcs[0]; - } - - return funcs.reduce((a, b) => (...args) => a(b(...args))); +function composeMiddleware(chain) { + return (context, dispatch) => { + return chain.reduceRight((res, middleware) => { + return middleware(context)(res); + }, dispatch); + }; } -const createReducer = (...middlewares) => (...args) => { - const [state, dispatch] = useReducer(...args); - +const createReducer = (...middlewares) => (reducer, initialState, initializer = value => value) => { + const ref = useRef({}) + const [hooksState, setState] = useState(initializer(initialState)) + ref.current = hooksState let middlewareDispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ); }; - + const dispatch = action => { + ref.current = reducer(ref.current, action) + setState(ref.current) + return action; + } + const composedMiddleware = useMemo(() => { + return composeMiddleware(middlewares); + }, middlewares); const middlewareAPI = { - getState: () => state, + getState: () => ref.current, dispatch: (...args) => middlewareDispatch(...args), }; - const chain = middlewares.map(middleware => middleware(middlewareAPI)); - middlewareDispatch = compose(...chain)(dispatch); - - return [state, middlewareDispatch]; + middlewareDispatch = composedMiddleware(middlewareAPI, dispatch); + return [ref.current, middlewareDispatch]; }; export default createReducer; From 2c1a67188a88dab36e00c7f9c3a3b3af4ed7c91f Mon Sep 17 00:00:00 2001 From: areschen Date: Thu, 9 May 2019 11:30:57 +0800 Subject: [PATCH 3/6] refactor: move initialState to useRef --- src/createReducer.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/createReducer.ts b/src/createReducer.ts index f68a3f7f..749edfb1 100644 --- a/src/createReducer.ts +++ b/src/createReducer.ts @@ -9,9 +9,8 @@ function composeMiddleware(chain) { } const createReducer = (...middlewares) => (reducer, initialState, initializer = value => value) => { - const ref = useRef({}) - const [hooksState, setState] = useState(initializer(initialState)) - ref.current = hooksState + const ref = useRef(initializer(initialState)) + const [, setState] = useState(ref.current) let middlewareDispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + From 72e3036b15f5671aee039ecfc5da598780de3653 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Sun, 2 Jun 2019 07:15:39 +0200 Subject: [PATCH 4/6] fix: fix TypeScript error --- src/createReducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/createReducer.ts b/src/createReducer.ts index 749edfb1..ff0c8e67 100644 --- a/src/createReducer.ts +++ b/src/createReducer.ts @@ -11,7 +11,7 @@ function composeMiddleware(chain) { const createReducer = (...middlewares) => (reducer, initialState, initializer = value => value) => { const ref = useRef(initializer(initialState)) const [, setState] = useState(ref.current) - let middlewareDispatch = () => { + let middlewareDispatch = (_ = {}) => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' From 17036105adba90108100a6975124ce11bcac4dd1 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Sun, 2 Jun 2019 07:28:09 +0200 Subject: [PATCH 5/6] style: don't shadow variable --- src/__stories__/createReducer.story.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/__stories__/createReducer.story.tsx b/src/__stories__/createReducer.story.tsx index 1edaf91e..65006db7 100644 --- a/src/__stories__/createReducer.story.tsx +++ b/src/__stories__/createReducer.story.tsx @@ -4,7 +4,6 @@ import logger from 'redux-logger'; import thunk from 'redux-thunk'; import { createReducer } from '..'; -// import ShowDocs from './util/ShowDocs'; const useThunkReducer = createReducer(thunk, logger); @@ -29,11 +28,11 @@ function reducer(state, action) { const Demo = ({ initialCount = 1 }) => { // Action creator to increment count, wait a second and then reset const addAndReset = React.useCallback(() => { - return dispatch => { - dispatch({ type: 'increment' }); + return dispatch2 => { + dispatch2({ type: 'increment' }); setTimeout(() => { - dispatch({ type: 'reset', payload: initialCount }); + dispatch2({ type: 'reset', payload: initialCount }); }, 1000); }; }, [initialCount]); From 01f96bd304a48015b22f59378c106e0f248fa1d0 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Sun, 2 Jun 2019 07:28:39 +0200 Subject: [PATCH 6/6] style: fix linter errors --- src/createReducer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/createReducer.ts b/src/createReducer.ts index ff0c8e67..fb0d0b8c 100644 --- a/src/createReducer.ts +++ b/src/createReducer.ts @@ -9,8 +9,8 @@ function composeMiddleware(chain) { } const createReducer = (...middlewares) => (reducer, initialState, initializer = value => value) => { - const ref = useRef(initializer(initialState)) - const [, setState] = useState(ref.current) + const ref = useRef(initializer(initialState)); + const [, setState] = useState(ref.current); let middlewareDispatch = (_ = {}) => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + @@ -18,10 +18,10 @@ const createReducer = (...middlewares) => (reducer, initialState, initializer = ); }; const dispatch = action => { - ref.current = reducer(ref.current, action) - setState(ref.current) + ref.current = reducer(ref.current, action); + setState(ref.current); return action; - } + }; const composedMiddleware = useMemo(() => { return composeMiddleware(middlewares); }, middlewares);