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 ----------- 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"