From 88e4b7b86209cbd2df148578b0ef6b02905b39ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=83=E8=B7=83=E6=9D=B0?= Date: Fri, 19 Nov 2021 18:32:06 +0800 Subject: [PATCH 1/2] feat: supports immer --- package.json | 5 ++- src/immer.js | 11 ++++++ src/reducer-immer.js | 89 ++++++++++++++++++++++++++++++++++++++++++++ test/reducer.test.js | 86 ++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 5 +++ 5 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 src/immer.js create mode 100644 src/reducer-immer.js diff --git a/package.json b/package.json index c6c2c286..ba6ec5cf 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,9 @@ "posttest": "npm run lint" }, "dependencies": { - "prop-types": "^15.7.2", - "lodash.isequalwith": "^4.4.0" + "immer": "^9.0.6", + "lodash.isequalwith": "^4.4.0", + "prop-types": "^15.7.2" }, "peerDependencies": { "history": "^4.7.2", diff --git a/src/immer.js b/src/immer.js new file mode 100644 index 00000000..b2eed4d7 --- /dev/null +++ b/src/immer.js @@ -0,0 +1,11 @@ +import createConnectedRouter from "./ConnectedRouter" +import createConnectRouter from "./reducer-immer" +import createSelectors from "./selectors" +import immerStructure from "./structure/plain" + +export { LOCATION_CHANGE, CALL_HISTORY_METHOD, onLocationChanged, push, replace, go, goBack, goForward, routerActions } from "./actions" +export { default as routerMiddleware } from "./middleware" + +export const ConnectedRouter = /*#__PURE__*/ createConnectedRouter(immerStructure) +export const connectRouter = /*#__PURE__*/ createConnectRouter() +export const { getLocation, getAction, getHash, getRouter, getSearch, createMatchSelector } = /*#__PURE__*/ createSelectors(immerStructure) diff --git a/src/reducer-immer.js b/src/reducer-immer.js new file mode 100644 index 00000000..0688782f --- /dev/null +++ b/src/reducer-immer.js @@ -0,0 +1,89 @@ +import { LOCATION_CHANGE } from './actions' +import produce from 'immer' + +/** + * Adds query to location. + * Utilises the search prop of location to construct query. + */ +const injectQuery = (location) => { + if (location && location.query) { + // Don't inject query if it already exists in history + return location + } + + const searchQuery = location && location.search + + if (typeof searchQuery !== 'string' || searchQuery.length === 0) { + return { + ...location, + query: {} + } + } + + // Ignore the `?` part of the search string e.g. ?username=codejockie + const search = searchQuery.substring(1) + // Split the query string on `&` e.g. ?username=codejockie&name=Kennedy + const queries = search.split('&') + // Contruct query + const query = queries.reduce((acc, currentQuery) => { + // Split on `=`, to get key and value + const [queryKey, queryValue] = currentQuery.split('=') + return { + ...acc, + [queryKey]: queryValue + } + }, {}) + + return { + ...location, + query + } +} + +const createConnectRouter = () => { + + const createRouterReducer = (history) => { + const initialRouterState = { + location: injectQuery(history.location), + action: history.action, + } + + /* + * This reducer will update the state with the most recent location history + * has transitioned to. + */ + // return (state = initialRouterState, { type, payload } = {}) => { + // if (type === LOCATION_CHANGE) { + // const { location, action, isFirstRendering } = payload + // // Don't update the state ref for the first rendering + // // to prevent the double-rendering issue on initilization + // return isFirstRendering + // ? state + // : merge(state, { location: fromJS(injectQuery(location)), action }) + // } + + // return state + // } + + + return produce((draft, { type, payload } = {}) => { + if (type === LOCATION_CHANGE) { + const { location, action, isFirstRendering } = payload + + // Don't update the state ref for the first rendering + // to prevent the double-rendering issue on initilization + if(!isFirstRendering){ + draft.action=action + draft.location=injectQuery(location) + } + } + return draft + }, initialRouterState) + + + } + + return createRouterReducer +} + +export default createConnectRouter diff --git a/test/reducer.test.js b/test/reducer.test.js index 502aae3e..5caed0a0 100644 --- a/test/reducer.test.js +++ b/test/reducer.test.js @@ -2,8 +2,10 @@ import { combineReducers } from 'redux' import { combineReducers as combineReducersImmutable } from 'redux-immutable' import { combineReducers as combineReducersSeamlessImmutable } from 'redux-seamless-immutable' import Immutable from 'immutable' +import produce from 'immer' import { LOCATION_CHANGE, connectRouter } from '../src' import { connectRouter as connectRouterImmutable } from '../src/immutable' +import {connectRouter as connectRouterImmer } from "../src/immer" import { connectRouter as connectRouterSeamlessImmutable } from '../src/seamless-immutable' describe('connectRouter', () => { @@ -331,4 +333,88 @@ describe('connectRouter', () => { expect(nextState).toBe(currentState) }) }) + + + describe('with immer structure', () => { + it('creates new root reducer with router reducer inside', () => { + const mockReducer =produce((draft, action) => { + switch (action.type) { + default: + return draft + } + },{}) + const rootReducer = combineReducers({ + mock: mockReducer, + router: connectRouterImmer(mockHistory) + }) + + const currentState = { + mock: {}, + router: { + location: { + pathname: '/', + search: '', + hash: '', + }, + action: 'POP', + }, + } + const action = { + type: LOCATION_CHANGE, + payload: { + location: { + pathname: '/path/to/somewhere', + search: '?query=test', + hash: '', + }, + action: 'PUSH', + } + } + const nextState = rootReducer(currentState, action) + const expectedState = { + mock: {}, + router: { + location: { + pathname: '/path/to/somewhere', + search: '?query=test', + hash: '', + query: { query: 'test' } + }, + action: 'PUSH', + }, + } + expect(nextState).toEqual(expectedState) + }) + + it('does not change state ref when receiving LOCATION_CHANGE for the first rendering', () => { + const rootReducer = combineReducers({ + router: connectRouter(mockHistory) + }) + const currentState = { + router: { + location: { + pathname: '/', + search: '', + hash: '', + }, + action: 'POP', + }, + } + const action = { + type: LOCATION_CHANGE, + payload: { + location: { + pathname: '/', + search: '', + hash: '', + }, + action: 'POP', + isFirstRendering: true, + } + } + const nextState = rootReducer(currentState, action) + expect(nextState).toBe(currentState) + }) + }) + }) diff --git a/yarn.lock b/yarn.lock index d032034c..142f4e5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3595,6 +3595,11 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +immer@^9.0.6: + version "9.0.6" + resolved "https://registry.nlark.com/immer/download/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73" + integrity sha1-epa/JnTQbIFD4yfL9zU5OI3fGnM= + immutable@^3.8.1: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" From 87aab0bfbb0bf93b0b7ae5151604f4fc11204830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=83=E8=B7=83=E6=9D=B0?= Date: Fri, 19 Nov 2021 18:44:07 +0800 Subject: [PATCH 2/2] docs: update README supports immer --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 69e8c8bb..81676296 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ Main features :muscle: Supports TypeScript +:gem: Supports [Immer.js](https://immerjs.github.io/immer/) + Installation -----------