From 54d06f0fc95e60e55cad23dd28ba665f1592fdc6 Mon Sep 17 00:00:00 2001 From: Jo Hanna Pearce Date: Fri, 17 Apr 2020 16:42:08 +0100 Subject: [PATCH] feat(react): upgrade redux toolkit version and generated slice code (#2835) --- e2e/react.test.ts | 18 ++- e2e/utils.ts | 11 +- package.json | 3 + .../__fileName__.slice.spec.ts__tmpl__ | 67 +++++---- .../__fileName__.slice.ts__tmpl__ | 128 ++++++++---------- packages/react/src/schematics/redux/redux.ts | 27 ++-- packages/react/src/utils/versions.ts | 2 +- yarn.lock | 70 +++++++++- 8 files changed, 209 insertions(+), 117 deletions(-) diff --git a/e2e/react.test.ts b/e2e/react.test.ts index d92ba8ffdc833..85a7423ed6539 100644 --- a/e2e/react.test.ts +++ b/e2e/react.test.ts @@ -9,7 +9,6 @@ import { renameFile, runCLI, runCLIAsync, - supportUi, uniq, updateFile, workspaceConfigName @@ -220,6 +219,23 @@ forEachCli(currentCLIName => { expect(libTestResults.stderr).toContain('Test Suites: 1 passed, 1 total'); }, 120000); + it('should be able to add a redux slice', async () => { + ensureProject(); + const appName = uniq('app'); + const libName = uniq('lib'); + + runCLI(`g @nrwl/react:app ${appName} --no-interactive`); + runCLI(`g @nrwl/react:redux lemon --project=${appName}`); + runCLI(`g @nrwl/react:lib ${libName} --no-interactive`); + runCLI(`g @nrwl/react:redux orange --project=${libName}`); + + const appTestResults = await runCLIAsync(`test ${appName}`); + expect(appTestResults.stderr).toContain('Test Suites: 2 passed, 2 total'); + + const libTestResults = await runCLIAsync(`test ${libName}`); + expect(libTestResults.stderr).toContain('Test Suites: 2 passed, 2 total'); + }, 120000); + it('should be able to use JSX', async () => { ensureProject(); const appName = uniq('app'); diff --git a/e2e/utils.ts b/e2e/utils.ts index 349c12254fd7b..2539fb5af3690 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -237,10 +237,14 @@ export function copyMissingPackages(): void { 'react', 'react-dom', + 'react-redux', 'react-router-dom', + '@reduxjs', + '@reduxjs/toolkit', 'styled-components', '@types/react', '@types/react-dom', + '@types/react-redux', '@types/react-router-dom', '@testing-library', @@ -342,8 +346,11 @@ export function copyMissingPackages(): void { } function copyNodeModule(name: string) { - execSync(`rm -rf ${tmpProjPath('node_modules/' + name)}`); - execSync(`cp -a node_modules/${name} ${tmpProjPath('node_modules/' + name)}`); + const source = `node_modules/${name}`; + const destination = tmpProjPath(source); + + execSync(`rm -rf ${destination}`); + execSync(`cp -a ${source} ${destination}`); } export function runCommandAsync( diff --git a/package.json b/package.json index 2d3cd6d8685fc..34d42b466d7fc 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@ngrx/store": "9.0.0", "@ngrx/store-devtools": "9.0.0", "@ngtools/webpack": "~9.1.0", + "@reduxjs/toolkit": "1.3.2", "@rollup/plugin-commonjs": "11.0.2", "@rollup/plugin-image": "2.0.4", "@rollup/plugin-node-resolve": "7.1.1", @@ -93,6 +94,7 @@ "@types/prettier": "^1.10.0", "@types/react": "16.9.17", "@types/react-dom": "16.9.4", + "@types/react-redux": "7.1.5", "@types/react-router-dom": "5.1.3", "@types/webpack": "^4.4.24", "@types/yargs": "^11.0.0", @@ -189,6 +191,7 @@ "raw-loader": "3.1.0", "react": "16.10.2", "react-dom": "16.10.2", + "react-redux": "7.1.3", "react-router-dom": "5.1.2", "regenerator-runtime": "0.13.3", "release-it": "^7.4.0", diff --git a/packages/react/src/schematics/redux/files/__directory__/__fileName__.slice.spec.ts__tmpl__ b/packages/react/src/schematics/redux/files/__directory__/__fileName__.slice.spec.ts__tmpl__ index dafc6eea39e48..b48463f1116ba 100644 --- a/packages/react/src/schematics/redux/files/__directory__/__fileName__.slice.spec.ts__tmpl__ +++ b/packages/react/src/schematics/redux/files/__directory__/__fileName__.slice.spec.ts__tmpl__ @@ -1,40 +1,53 @@ -import { - <%= propertyName %>Reducer, - get<%= className %>Start, - get<%= className %>Failure, - get<%= className %>Success -} from './<%= fileName %>.slice'; +import { <%= propertyName %>Actions, <%= propertyName %>Adapter, <%= propertyName %>Reducer } from './<%= fileName %>.slice'; describe('<%= propertyName %> reducer', () => { it('should handle initial state', () => { - expect(<%= propertyName %>Reducer(undefined, { type: '' })).toMatchObject({ - entities: [] + const expected = <%= propertyName %>Adapter.getInitialState({ + loadingStatus: 'not loaded', + error: null }); + + expect(<%= propertyName %>Reducer(undefined, { type: '' })).toEqual(expected); }); - it('should handle get <%= propertyName %> actions', () => { - let state = <%= propertyName %>Reducer(undefined, get<%= className %>Start()); + it('should handle fetch<%= className %>s', () => { + let state = <%= propertyName %>Reducer( + undefined, + <%= propertyName %>Actions.fetch<%= className %>s.pending(null, null) + ); - expect(state).toEqual({ - loaded: false, - error: null, - entities: [] - }); + expect(state).toEqual( + expect.objectContaining({ + loadingStatus: 'loading', + error: null, + entities: {} + }) + ); - state = <%= propertyName %>Reducer(state, get<%= className %>Success([{ id: 1 }])); + state = <%= propertyName %>Reducer( + state, + <%= propertyName %>Actions.fetch<%= className %>s.fulfilled([{ id: 1 }], null, null) + ); - expect(state).toEqual({ - loaded: true, - error: null, - entities: [{ id: 1 }] - }); + expect(state).toEqual( + expect.objectContaining({ + loadingStatus: 'loaded', + error: null, + entities: { 1: { id: 1 } } + }) + ); - state = <%= propertyName %>Reducer(state, get<%= className %>Failure('Uh oh')); + state = <%= propertyName %>Reducer( + state, + <%= propertyName %>Actions.fetch<%= className %>s.rejected(new Error('Uh oh'), null, null) + ); - expect(state).toEqual({ - loaded: true, - error: 'Uh oh', - entities: [{ id: 1 }] - }); + expect(state).toEqual( + expect.objectContaining({ + loadingStatus: 'error', + error: 'Uh oh', + entities: { 1: { id: 1 } } + }) + ); }); }); diff --git a/packages/react/src/schematics/redux/files/__directory__/__fileName__.slice.ts__tmpl__ b/packages/react/src/schematics/redux/files/__directory__/__fileName__.slice.ts__tmpl__ index 80a73adef65f9..980a2aae0ff82 100644 --- a/packages/react/src/schematics/redux/files/__directory__/__fileName__.slice.ts__tmpl__ +++ b/packages/react/src/schematics/redux/files/__directory__/__fileName__.slice.ts__tmpl__ @@ -1,13 +1,13 @@ -import { createSlice, createSelector, Action, PayloadAction } from '@reduxjs/toolkit'; -import { ThunkAction } from 'redux-thunk'; +import { + createAsyncThunk, + createEntityAdapter, + createSlice, + EntityState, + PayloadAction +} from '@reduxjs/toolkit'; export const <%= constantName %>_FEATURE_KEY = '<%= propertyName %>'; -/* - * Change this from `any` if there is a more specific error type. - */ -export type <%= className %>Error = any; - /* * Update these interfaces according to your requirements. */ @@ -15,32 +15,54 @@ export interface <%= className %>Entity { id: number; } -export interface <%= className %>State { - entities: <%= className %>Entity[]; - loaded: boolean; - error: <%= className %>Error; +export interface <%= className %>State extends EntityState<<%= className %>Entity> { + loadingStatus: 'not loaded' | 'loading' | 'loaded' | 'error'; + error: string; } -export const initial<%= className %>State: <%= className %>State = { - entities: [], - loaded: false, +export const <%= propertyName %>Adapter = createEntityAdapter<<%= className %>Entity>(); + +/** + * Export an effect using createAsyncThunk from + * the Redux Toolkit: https://redux-toolkit.js.org/api/createAsyncThunk + */ +export const fetch<%= className %>s = createAsyncThunk( + '<%= propertyName %>/fetchStatus', + async (_, thunkAPI) => { + /** + * Replace this with your custom fetch call. + * For example, `return myApi.get<%= className %>s()`; + * Right now we just return an empty array. + */ + return Promise.resolve([]); + } +); + +export const initial<%= className %>State: <%= className %>State = <%= propertyName %>Adapter.getInitialState({ + loadingStatus: 'not loaded', error: null -}; +}); export const <%= propertyName %>Slice = createSlice({ name: <%= constantName %>_FEATURE_KEY, - initialState: initial<%= className %>State as <%= className %>State, + initialState: initial<%= className %>State, reducers: { - get<%= className %>Start: (state, action: PayloadAction) => { - state.loaded = false; - }, - get<%= className %>Success: (state, action: PayloadAction<<%= className %>Entity[]>) => { - state.loaded = true; - state.entities = action.payload; - }, - get<%= className %>Failure: (state, action: PayloadAction<<%= className %>Error>) => { - state.error = action.payload; - } + add<%= className %>: <%= propertyName %>Adapter.addOne, + remove<%= className %>: <%= propertyName %>Adapter.removeOne + // ... + }, + extraReducers: builder => { + builder.addCase(fetch<%= className %>s.pending, (state: <%= className %>State) => { + state.loadingStatus = 'loading'; + }); + builder.addCase(fetch<%= className %>s.fulfilled, (state: <%= className %>State, action: PayloadAction<<%= className %>Entity[]>) => { + <%= propertyName %>Adapter.addMany(state, action.payload); + state.loadingStatus = 'loaded'; + }); + builder.addCase(fetch<%= className %>s.rejected, (state: <%= className %>State, action) => { + state.loadingStatus = 'error'; + state.error = action.error.message; + }); } }); @@ -55,16 +77,15 @@ export const <%= propertyName %>Reducer = <%= propertyName %>Slice.reducer; * e.g. * ``` * const dispatch = useDispatch(); - * dispatch(get<%= className %>Success([{ id: 1 }])); + * dispatch(<%= propertyName %>Actions.add<%= className %>([{ id: 1 }])); * ``` * * See: https://react-redux.js.org/next/api/hooks#usedispatch */ -export const { - get<%= className %>Start, - get<%= className %>Success, - get<%= className %>Failure -} = <%= propertyName %>Slice.actions; +export const <%= propertyName %>Actions = { + ...<%= propertyName %>Slice.actions, + fetch<%= className %>s +}; /* * Export selectors to query state. For use with the `useSelector` hook. @@ -76,42 +97,7 @@ export const { * * See: https://react-redux.js.org/next/api/hooks#useselector */ -export const get<%= className %>State = (rootState: any): <%= className %>State => - rootState[<%= constantName %>_FEATURE_KEY]; - -export const select<%= className %>Entities = createSelector( - get<%= className %>State, - s => s.entities -); - -export const select<%= className %>Loaded = createSelector( - get<%= className %>State, - s => s.loaded -); - -export const select<%= className %>Error = createSelector( - get<%= className %>State, - s => s.error -); - -/* - * Export default effect, handled by redux-thunk. - * You can replace this with your own effects solution. - */ -export const fetch<%= className %> = (): ThunkAction< - void, - any, - null, - Action -> => async dispatch => { - try { - dispatch(get<%= className %>Start()); - // Replace this with your custom fetch call. - // For example, `const data = await myApi.get<%= className %>`; - // Right now we just load an empty array. - const data = []; - dispatch(get<%= className %>Success(data)); - } catch (err) { - dispatch(get<%= className %>Failure(err)); - } -}; +export const <%= propertyName %>Selectors = { + get<%= className %>State: (rootState: unknown): <%= className %>State => rootState[<%= constantName %>_FEATURE_KEY], + ...<%= propertyName %>Adapter.getSelectors() +}; \ No newline at end of file diff --git a/packages/react/src/schematics/redux/redux.ts b/packages/react/src/schematics/redux/redux.ts index aa22a07f06c3c..52bf64a1ed706 100644 --- a/packages/react/src/schematics/redux/redux.ts +++ b/packages/react/src/schematics/redux/redux.ts @@ -1,13 +1,4 @@ -import * as ts from 'typescript'; import { join, Path, strings } from '@angular-devkit/core'; -import '@nrwl/tao/src/compat/compat'; -import { - addDepsToPackageJson, - addGlobal, - getProjectConfig, - insert, - readJsonInTree -} from '@nrwl/workspace/src/utils/ast-utils'; import { apply, chain, @@ -20,17 +11,25 @@ import { Tree, url } from '@angular-devkit/schematics'; - -import { NormalizedSchema, Schema } from './schema'; +import '@nrwl/tao/src/compat/compat'; import { formatFiles, getWorkspace, names, toFileName } from '@nrwl/workspace'; +import { + addDepsToPackageJson, + addGlobal, + getProjectConfig, + insert, + readJsonInTree +} from '@nrwl/workspace/src/utils/ast-utils'; +import { toJS } from '@nrwl/workspace/src/utils/rules/to-js'; import * as path from 'path'; +import * as ts from 'typescript'; import { addReduxStoreToMain, updateReduxStore } from '../../utils/ast-utils'; import { - typesReactReduxVersion, reactReduxVersion, - reduxjsToolkitVersion + reduxjsToolkitVersion, + typesReactReduxVersion } from '../../utils/versions'; -import { toJS } from '@nrwl/workspace/src/utils/rules/to-js'; +import { NormalizedSchema, Schema } from './schema'; export default function(schema: any): Rule { return async (host: Tree, context: SchematicContext) => { diff --git a/packages/react/src/utils/versions.ts b/packages/react/src/utils/versions.ts index e96bd5389d86a..92dc7fd714567 100644 --- a/packages/react/src/utils/versions.ts +++ b/packages/react/src/utils/versions.ts @@ -16,7 +16,7 @@ export const typesReactRouterDomVersion = '5.1.3'; export const testingLibraryReactVersion = '9.4.0'; -export const reduxjsToolkitVersion = '1.1.0'; +export const reduxjsToolkitVersion = '1.3.2'; export const reactReduxVersion = '7.1.3'; export const typesReactReduxVersion = '7.1.5'; diff --git a/yarn.lock b/yarn.lock index 0c1651ac4a986..c8a9487ef56dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3762,6 +3762,16 @@ react-lifecycles-compat "^3.0.4" warning "^3.0.0" +"@reduxjs/toolkit@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.3.2.tgz#cbd062f0b806eb4611afeb2b30240e5186d6dd27" + integrity sha512-IRI9Nx6Ys/u4NDqPvUC0+e8MH+e1VME9vn30xAmd+MBqDsClc0Dhrlv4Scw2qltRy/mrINarU6BqJp4/dcyyFg== + dependencies: + immer "^6.0.1" + redux "^4.0.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + "@rollup/plugin-commonjs@11.0.2", "@rollup/plugin-commonjs@^11.0.2": version "11.0.2" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.2.tgz#837cc6950752327cb90177b608f0928a4e60b582" @@ -4587,6 +4597,14 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.3.tgz#856c99cdc1551d22c22b18b5402719affec9839a" integrity sha512-cS5owqtwzLN5kY+l+KgKdRJ/Cee8tlmQoGQuIE9tWnSmS3JMKzmxo2HIAk2wODMifGwO20d62xZQLYz+RLfXmw== +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/is-function@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" @@ -4746,6 +4764,16 @@ dependencies: "@types/react" "*" +"@types/react-redux@7.1.5": + version "7.1.5" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.5.tgz#c7a528d538969250347aa53c52241051cf886bd3" + integrity sha512-ZoNGQMDxh5ENY7PzU7MVonxDzS1l/EWiy8nUhDqxFqUZn4ovboCyvk4Djf68x6COb7vhGTKjyjxHxtFdAA5sUA== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-router-dom@5.1.3": version "5.1.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196" @@ -12826,6 +12854,11 @@ immer@1.10.0: resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immer@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/immer/-/immer-6.0.3.tgz#94d5051cd724668160a900d66d85ec02816f29bd" + integrity sha512-12VvNrfSrXZdm/BJgi/KDW2soq5freVSf3I1+4CLunUM8mAGx2/0Njy0xBVzi5zewQZiwM7z1/1T+8VaI7NkmQ== + import-cwd@^2.0.0, import-cwd@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -19223,6 +19256,11 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== +react-is@^16.9.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -19248,6 +19286,18 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" +react-redux@7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79" + integrity sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-router-dom@5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" @@ -19603,6 +19653,19 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" +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== + +redux@^4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" + integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + reflect-metadata@^0.1.2: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" @@ -20002,6 +20065,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + reserved-words@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1" @@ -21783,7 +21851,7 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" -symbol-observable@1.2.0, symbol-observable@^1.1.0: +symbol-observable@1.2.0, symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==