diff --git a/docs/api/createSlice.md b/docs/api/createSlice.md index b698f0d3c2..5054ba1283 100644 --- a/docs/api/createSlice.md +++ b/docs/api/createSlice.md @@ -7,7 +7,7 @@ hide_title: true # `createSlice` -A function that accepts an initial state, an object full of reducer functions, and optionally a "slice name", +A function that accepts an initial state, an object full of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state. ## Parameters @@ -20,8 +20,8 @@ function createSlice({ reducers: Object // The initial state for the reducer initialState: any, - // An optional name, used in action types - slice?: string, + // A name, used in action types + name: string, // An additional object of "case reducers". Keys should be other action types. extraReducers?: Object }) @@ -44,9 +44,9 @@ state they are given. The initial state value for this slice of state. -### `slice` +### `name` -An optional string name for this slice of state. Generated action type constants will use this as a prefix. +A string name for this slice of state. Generated action type constants will use this as a prefix. ### `extraReducers` @@ -73,7 +73,7 @@ to force the TS compiler to accept the computed property.) ```ts { - slice : string, + name : string, reducer : ReducerFunction, actions : Object, } @@ -104,7 +104,7 @@ import { createSlice } from 'redux-starter-kit' import { createStore, combineReducers } from 'redux' const counter = createSlice({ - slice: 'counter', // slice is optional, and could be blank '' + name: 'counter', initialState: 0, reducers: { increment: state => state + 1, @@ -114,7 +114,7 @@ const counter = createSlice({ }) const user = createSlice({ - slice: 'user', + name: 'user', initialState: { name: '', age: 20 }, reducers: { setUserName: (state, action) => { diff --git a/docs/tutorials/advanced-tutorial.md b/docs/tutorials/advanced-tutorial.md index d9ca50b8fd..fb9c0454de 100644 --- a/docs/tutorials/advanced-tutorial.md +++ b/docs/tutorials/advanced-tutorial.md @@ -21,7 +21,7 @@ In the process, we'll look at a few examples of TypeScript techniques you can us > > In addition, this tutorial does not mean you _must_ convert your React app logic completely to Redux. [It's up to you to decide what state should live in React components, and what should be in Redux](https://redux.js.org/faq/organizing-state#do-i-have-to-put-all-my-state-into-redux-should-i-ever-use-reacts-setstate). This is just an example of how you _could_ convert logic to use Redux if you choose to. -The complete source code for the converted application from this tutorial is available at [github.com/markerikson/rsk-github-issues-example](https://github.com/markerikson/rsk-github-issues-example). We'll be walking through the conversion process as shown in this repo's history. Links to meaningful individual commits will be highlighted in quote blocks, like this: +The complete source code for the converted application from this tutorial is available at [github.com/reduxjs/rsk-github-issues-example](https://github.com/reduxjs/rsk-github-issues-example). We'll be walking through the conversion process as shown in this repo's history. Links to meaningful individual commits will be highlighted in quote blocks, like this: > - Commit message here @@ -52,7 +52,7 @@ The codebase is already laid out in a "feature folder" structure, The main piece Since this app doesn't yet use Redux at all, the first step is to install Redux Starter Kit and React-Redux. Since this is a TypeScript app, we'll also need to add `@types/react-redux` as well. Add those packages to the project via either Yarn or NPM. -> - [Add Redux Starter Kit and React-Redux packages](https://github.com/markerikson/rsk-github-issues-example/commit/8f69804d8940ba40604949ca682a7ae968e8bc4f) +> - [Add Redux Starter Kit and React-Redux packages](https://github.com/reduxjs/rsk-github-issues-example/commit/8f69804d8940ba40604949ca682a7ae968e8bc4f) Next, we need to set up the usual pieces: a root reducer function, the Redux store, and the `` to make that store available to our component tree. @@ -60,7 +60,7 @@ In the process, we're going to set up "Hot Module Replacement" for our app. That #### Creating the Root Reducer -> - [Add store and root reducer with reducer HMR](https://github.com/markerikson/rsk-github-issues-example/commit/26054ea8be1a44cac75fd55f497ce20e264de2b0) +> - [Add store and root reducer with reducer HMR](https://github.com/reduxjs/rsk-github-issues-example/commit/26054ea8be1a44cac75fd55f497ce20e264de2b0) First, we'll create the root reducer function. We don't have any slices yet, so it will just return an empty object. @@ -113,7 +113,7 @@ The `require('./rootReducer').default` looks a bit odd. That's because we're mix Now that the store has been created, we can add it to the React component tree. -> - [Render Redux Provider with app HMR](https://github.com/markerikson/rsk-github-issues-example/commit/49cf5caebd427e7bb6b7ab07098c3bbb12134faf) +> - [Render Redux Provider with app HMR](https://github.com/reduxjs/rsk-github-issues-example/commit/49cf5caebd427e7bb6b7ab07098c3bbb12134faf) As with the root reducer, we can hot-reload the React component tree whenever a component file changes. The best way is to write a function that imports the `` component and renders it, call that once on startup to show the React component tree as usual, and then reuse that function any time a component is changed. @@ -170,7 +170,7 @@ The first step is to look at the data that is currently being kept in ``, a Let's look at the source for the whole slice, and then break down what it's doing: -> - [Add initial state slice for UI display](https://github.com/markerikson/rsk-github-issues-example/commit/ec809346d5afe8f96bb56e487c2e41d274d80c69) +> - [Add initial state slice for UI display](https://github.com/reduxjs/rsk-github-issues-example/commit/ec809346d5afe8f96bb56e487c2e41d274d80c69) **features/issuesDisplay/issuesDisplaySlice.ts** @@ -206,7 +206,7 @@ let initialState: CurrentDisplayState = { } const issuesDisplaySlice = createSlice({ - slice: 'issuesDisplay', + name: 'issuesDisplay', initialState, reducers: { displayRepo(state, action: PayloadAction) { @@ -288,7 +288,7 @@ import { combineReducers } from 'redux-starter-kit' Now that the issues display slice is hooked up to the store, we can update `` to use that instead of its internal component state. -> - [Convert main issues display control to Redux](https://github.com/markerikson/rsk-github-issues-example/commit/07bea70da4439c4c38b9b8d4eb0f10c67e6feee2) +> - [Convert main issues display control to Redux](https://github.com/reduxjs/rsk-github-issues-example/commit/07bea70da4439c4c38b9b8d4eb0f10c67e6feee2) We need to make three groups of changes to the `App` component: @@ -514,7 +514,7 @@ Since the thunk middleware is already set up, we don't have to do any work there Before we go any further, let's add a type declaration we can reuse instead. -> - [Add AppThunk type](https://github.com/markerikson/rsk-github-issues-example/commit/2ac93bb089705847a8ce349864d885a5039eff4b) +> - [Add AppThunk type](https://github.com/reduxjs/rsk-github-issues-example/commit/2ac93bb089705847a8ce349864d885a5039eff4b) **app/store.ts** @@ -544,7 +544,7 @@ There are many cases where you would want different type settings here, but thes Now that we have that type, we can write a slice of state for fetching details on a repo. -> - [Add a slice for storing repo details](https://github.com/markerikson/rsk-github-issues-example/commit/da9291bf428a96c3f2e8862f42e3be08461d514c) +> - [Add a slice for storing repo details](https://github.com/reduxjs/rsk-github-issues-example/commit/da9291bf428a96c3f2e8862f42e3be08461d514c) **features/repoSearch/repoDetailsSlice.ts** @@ -566,7 +566,7 @@ const initialState: RepoDetailsState = { } const repoDetails = createSlice({ - slice: 'repoDetails', + name: 'repoDetails', initialState, reducers: { getRepoDetailsSuccess(state, action: PayloadAction) { @@ -615,7 +615,7 @@ While not shown, we also add the slice reducer to our root reducer. Now that the repo details slice exists, we can use it in the `` component. -> - [Update IssuesListPage to fetch repo details via Redux](https://github.com/markerikson/rsk-github-issues-example/commit/964134a00bc1a54ba8758ca274049c9174e88f9a) +> - [Update IssuesListPage to fetch repo details via Redux](https://github.com/reduxjs/rsk-github-issues-example/commit/964134a00bc1a54ba8758ca274049c9174e88f9a) **features/issuesList/IssuesListPage.tsx** @@ -694,7 +694,7 @@ Inside our `useEffect`, we drop the `fetchIssueCount` function, and dispatch `fe Next up, we need to replace the logic for fetching a list of open issues. -> - [Add a slice for tracking issues state](https://github.com/markerikson/rsk-github-issues-example/commit/b2e5919651a5076e3857da96321bc979a8ae54b9) +> - [Add a slice for tracking issues state](https://github.com/reduxjs/rsk-github-issues-example/commit/b2e5919651a5076e3857da96321bc979a8ae54b9) **features/issuesList/issuesSlice.ts** @@ -733,7 +733,7 @@ function loadingFailed(state: IssuesState, action: PayloadAction) { } const issues = createSlice({ - slice: 'issues', + name: 'issues', initialState: issuesInitialState, reducers: { getIssueStart: startLoading, @@ -811,7 +811,7 @@ This slice is a bit longer, but it's the same basic approach as before: write th Now we can finish converting the `` component by swapping out the issues fetching logic. -> - [Update IssuesListPage to fetch issues data via Redux](https://github.com/markerikson/rsk-github-issues-example/commit/8dbdc0726ccecf354a01351786196648c752c0a6) +> - [Update IssuesListPage to fetch issues data via Redux](https://github.com/reduxjs/rsk-github-issues-example/commit/8dbdc0726ccecf354a01351786196648c752c0a6) Let's look at the changes. @@ -950,7 +950,7 @@ It's very similar to ``. We store the current displayed `Issue`, We conveniently already have the Redux logic for fetching a single issue - we wrote that already as part of `issuesSlice.ts`. So, we can immediately jump straight to using that here in ``. -> - [Update IssueDetailsPage to fetch issue data via Redux](https://github.com/markerikson/rsk-github-issues-example/commit/46bcddbe1078574fab649a13f61a6bf3d0f42839) +> - [Update IssueDetailsPage to fetch issue data via Redux](https://github.com/reduxjs/rsk-github-issues-example/commit/46bcddbe1078574fab649a13f61a6bf3d0f42839) **features/issueDetails/IssueDetailsPage.tsx** @@ -1015,7 +1015,7 @@ Interestingly, there's actually a bit of a change in behavior here. The original We have one more slice left to write - we need to fetch and store comments for the current issue. -> - [Add a slice for tracking comments data](https://github.com/markerikson/rsk-github-issues-example/commit/46bcddbe1078574fab649a13f61a6bf3d0f42839) +> - [Add a slice for tracking comments data](https://github.com/reduxjs/rsk-github-issues-example/commit/46bcddbe1078574fab649a13f61a6bf3d0f42839) **features/issueDetails/commentsSlice.ts** @@ -1043,7 +1043,7 @@ const initialState: CommentsState = { } const comments = createSlice({ - slice: 'comments', + name: 'comments', initialState, reducers: { getCommentsStart(state) { @@ -1087,7 +1087,7 @@ The slice should look pretty familiar at this point. Our main bit of state is a The final step is to swap the comments fetching logic in ``. -> - [Update IssueDetailsPage to fetch comments via Redux](https://github.com/markerikson/rsk-github-issues-example/commit/9d1246a4d89f21da1f0e5377f040bc766e1fc0fd) +> - [Update IssueDetailsPage to fetch comments via Redux](https://github.com/reduxjs/rsk-github-issues-example/commit/9d1246a4d89f21da1f0e5377f040bc766e1fc0fd) **features/issueDetails/IssueDetailsPage.tsx** diff --git a/docs/tutorials/basic-tutorial.md b/docs/tutorials/basic-tutorial.md index 51210d5074..c40f06712c 100644 --- a/docs/tutorials/basic-tutorial.md +++ b/docs/tutorials/basic-tutorial.md @@ -225,6 +225,7 @@ Here's what our counter example would look like using `createSlice` instead: ```js const counterSlice = createSlice({ + name: 'counter', initialState: 0, reducers: { increment: state => state + 1, diff --git a/docs/tutorials/intermediate-tutorial.md b/docs/tutorials/intermediate-tutorial.md index bb1270f67b..18b4e20b04 100644 --- a/docs/tutorials/intermediate-tutorial.md +++ b/docs/tutorials/intermediate-tutorial.md @@ -19,7 +19,7 @@ This will show several concepts: Also, while this isn't specific to RSK, we'll look at a couple ways you can improve your React-Redux code as well. -The complete source code for the converted application from this tutorial is available at [github.com/markerikson/rsk-convert-todos-example](https://github.com/markerikson/rsk-convert-todos-example). We'll be walking through the conversion process as shown in this repo's history. Links to meaningful individual commits will be highlighted in quote blocks, like this: +The complete source code for the converted application from this tutorial is available at [github.com/reduxjs/rsk-convert-todos-example](https://github.com/reduxjs/rsk-convert-todos-example). We'll be walking through the conversion process as shown in this repo's history. Links to meaningful individual commits will be highlighted in quote blocks, like this: > - Commit message here @@ -41,8 +41,8 @@ On the one hand, this is a small example app. It's meant to illustrate the basic Since the original todos example is in the Redux repo, we start by copying the Redux "todos" source code to a fresh Create-React-App project, and adding Prettier to the project to help make sure the code is formatted consistently. There's also a `jsconfig.json` file to enable us to use "absolute import paths" that start from the `/src` folder. -> - [Initial commit](https://github.com/markerikson/rsk-convert-todos-example/commit/6b143697dee801e51ecfa43d5ebd48414afefff7). -> - [Add jsconfig.json to support absolute imports](https://github.com/markerikson/rsk-convert-todos-example/commit/bdd494032d1513e657adb20727eb6d9460d8eb72) +> - [Initial commit](https://github.com/reduxjs/rsk-convert-todos-example/commit/6b143697dee801e51ecfa43d5ebd48414afefff7). +> - [Add jsconfig.json to support absolute imports](https://github.com/reduxjs/rsk-convert-todos-example/commit/bdd494032d1513e657adb20727eb6d9460d8eb72) In the Basic Tutorial, we just linked to Redux Starter Kit as an individual script tag. But, in a typical application, you need to add RSK as a package dependency in your project. This can be done with either the NPM or Yarn package managers: @@ -64,7 +64,7 @@ Just like with the "counter" example, we can replace the plain Redux `createStor The changes here are simple. We update `src/index.js` to import `configureStore` instead of `createStore`, and replace the function call. Remember that `configureStore` takes an options object as a parameter with named fields, so instead of passing `rootReducer` directly as the first parameter, we pass it as an object field named `reducer`: -> - [Convert store setup to use configureStore](https://github.com/markerikson/rsk-convert-todos-example/commit/89d527fabacfd6b1ee401e6092f1dc849f16cac9) +> - [Convert store setup to use configureStore](https://github.com/reduxjs/rsk-convert-todos-example/commit/89d527fabacfd6b1ee401e6092f1dc849f16cac9) ```diff import React from "react"; @@ -157,13 +157,13 @@ We'll start by adding a new file called `/features/todos/todosSlice.js`. Note th In this file, we'll add the following logic: -> - [Add an initial todos slice](https://github.com/markerikson/rsk-convert-todos-example/commit/f53db013e42976f4e5830ba2e6d92f2f7695a10e) +> - [Add an initial todos slice](https://github.com/reduxjs/rsk-convert-todos-example/commit/f53db013e42976f4e5830ba2e6d92f2f7695a10e) ```js import { createSlice } from 'redux-starter-kit' const todosSlice = createSlice({ - slice: 'todos', + name: 'todos', initialState: [], reducers: { addTodo(state, action) { @@ -189,7 +189,7 @@ export default todosSlice.reducer Let's break down what this does: - `createSlice` takes an options object as its argument, with these options: - - `slice`: a string that is used as the prefix for generated action types + - `name`: a string that is used as the prefix for generated action types - `initialState`: the initial state value for the reducer - `reducers`: an object, where the keys will become action type strings, and the functions are reducers that will be run when that action type is dispatched. (These are sometimes referred to as ["case reducers"](https://redux.js.org/recipes/structuring-reducers/splitting-reducer-logic), because they're similar to a `case` in a `switch` statement) @@ -213,11 +213,16 @@ Normal immutable update logic tends to obscure what you're actually trying to do ```js { - reducer: (state, action) => newState, - actions: { - addTodo: (payload) => ({type: "todos/addTodo", payload}), - toggleTodo: (payload) => ({type: "todos/toggleTodo", payload}) - } + name: "todos", + reducer: (state, action) => newState, + actions: { + addTodo: (payload) => ({type: "todos/addTodo", payload}), + toggleTodo: (payload) => ({type: "todos/toggleTodo", payload}) + }, + caseReducers: { + addTodo: (state, action) => newState, + toggleTodo: (state, action) => newState, + } } ``` @@ -249,7 +254,7 @@ The original todos reducer has a tests file with it. We can port those over to w The first step is to copy `reducers/todos.spec.js` over to `features/todos/todosSlice.spec.js`, and change the import path to read the reducer from the slice file. -> - [Copy tests to todos slice](https://github.com/markerikson/rsk-convert-todos-example/commit/1df3f69b5d63faeeacc52d6e1901ff433e93485c) +> - [Copy tests to todos slice](https://github.com/reduxjs/rsk-convert-todos-example/commit/1df3f69b5d63faeeacc52d6e1901ff433e93485c) Once that is done, we need to update the tests to match how RSK works. @@ -259,7 +264,7 @@ The other problem is that the action objects in the tests look like `{type, id, (We really _could_ just replace all the inline action objects in the test with calls like `addTodo({id : 0, text: "Buy milk"})`, but this is a simpler set of changes to show for now.) -> - [Port the todos tests to work with the todos slice](https://github.com/markerikson/rsk-convert-todos-example/commit/ac05da954df706ff711cf3dbde0c6cebbc886d85) +> - [Port the todos tests to work with the todos slice](https://github.com/reduxjs/rsk-convert-todos-example/commit/ac05da954df706ff711cf3dbde0c6cebbc886d85) An example of the changes would be: @@ -305,7 +310,7 @@ We _could_ add that behavior for requiring that whatever code dispatches the add RSK allows you to customize how the `payload` field is created in your action objects. If you are using `createAction` by itself, you can pass a "prepare callback" as the second argument. Here's what this would look like: -> - [Implement addTodo ID generation](https://github.com/markerikson/rsk-convert-todos-example/commit/2af28fe0f71ebf03ae0e66874a73aca6925fba9a) +> - [Implement addTodo ID generation](https://github.com/reduxjs/rsk-convert-todos-example/commit/2af28fe0f71ebf03ae0e66874a73aca6925fba9a) ```js let nextTodoId = 0 @@ -325,7 +330,7 @@ If you're using `createSlice`, it automatically calls `createAction` for you. If let nextTodoId = 0 const todosSlice = createSlice({ - slice: 'todos', + name: 'todos', initialState: [], reducers: { addTodo: { @@ -363,7 +368,7 @@ We have a shiny new todos reducer function, but it isn't hooked up to anything y The first step is to go update our root reducer to use the reducer from the todos slice instead of the original reducer. We just need to change the import statement in `reducers/index.js`: -> - [Use the todos slice reducer](https://github.com/markerikson/rsk-convert-todos-example/commit/f7ab327a4c3192d0c8479cbc3ddd59029bddb499) +> - [Use the todos slice reducer](https://github.com/reduxjs/rsk-convert-todos-example/commit/f7ab327a4c3192d0c8479cbc3ddd59029bddb499) ```diff import { combineReducers } from 'redux' @@ -390,7 +395,7 @@ Second, the connected component is getting `dispatch` as a prop. Again, this wor Since we've got this component open, we can fix those issues too. Here's what the final version looks like: -> - [Update AddTodo to dispatch the new action type](https://github.com/markerikson/rsk-convert-todos-example/commit/1ebf475b37063888265b6ed99ff4568acde8a5e9) +> - [Update AddTodo to dispatch the new action type](https://github.com/reduxjs/rsk-convert-todos-example/commit/1ebf475b37063888265b6ed99ff4568acde8a5e9) ```js import React, { useState } from 'react' @@ -439,7 +444,7 @@ Finally, we use the ["object shorthand" form of `mapDispatch`](https://react-red The `TodoList` and `VisibleTodoList` components have similar issues: they're using the older `toggleTodo` action creator, and the `connect` setup isn't using the "object shorthand" form of `mapDispatch`. We can fix both of those. -> - [Update TodoList to dispatch the new toggle action type](https://github.com/markerikson/rsk-convert-todos-example/commit/621289ff8534fb0f0949002996bd7eb812e600d9) +> - [Update TodoList to dispatch the new toggle action type](https://github.com/reduxjs/rsk-convert-todos-example/commit/621289ff8534fb0f0949002996bd7eb812e600d9) ```diff // VisibleTodoList.js @@ -472,7 +477,7 @@ Now that we've created the todos slice and hooked it up to the UI, we can do the The filter logic is really simple. We have one action, which sets the current filter value by returning what's in the action. Here's the whole slice: -> - [Add the filters slice](https://github.com/markerikson/rsk-convert-todos-example/commit/cb4cfd3f4d03bf6d3d00405e9b46ee452f1eaff9) +> - [Add the filters slice](https://github.com/reduxjs/rsk-convert-todos-example/commit/cb4cfd3f4d03bf6d3d00405e9b46ee452f1eaff9) ```js import { createSlice } from 'redux-starter-kit' @@ -484,7 +489,7 @@ export const VisibilityFilters = { } const filtersSlice = createSlice({ - slice: 'visibilityFilters', + name: 'visibilityFilters', initialState: VisibilityFilters.SHOW_ALL, reducers: { setVisibilityFilter(state, action) { @@ -504,7 +509,7 @@ We've copied over the `VisibilityFilters` enum object that was originally in `ac As with the todos reducer, we need to import and add the visibility reducer to our root reducer: -> - [Use the filters slice reducer](https://github.com/markerikson/rsk-convert-todos-example/commit/5fb254924d345284315e2c582bb08153f8ccb7c0) +> - [Use the filters slice reducer](https://github.com/reduxjs/rsk-convert-todos-example/commit/5fb254924d345284315e2c582bb08153f8ccb7c0) ```diff import todosReducer from 'features/todos/todosSlice' @@ -522,7 +527,7 @@ From there, we need to dispatch the `setVisibilityFilter` action when the user c From there, the link components will take just a bit more work. `FilterLink` is currently creating new functions that capture the current value of `ownProps.filter`, so that `Link` is just getting a function called `onClick`. While that's a valid way to do it, for consistency we'd like to continue using the object shorthand form of `mapDispatch`, and modify `Link` to pass the filter value in when it dispatches the action. -> - [Use the new filters action in the UI](https://github.com/markerikson/rsk-convert-todos-example/commit/c1bd4ede7b04be0eb166577671b3d64fb9a444bc) +> - [Use the new filters action in the UI](https://github.com/reduxjs/rsk-convert-todos-example/commit/c1bd4ede7b04be0eb166577671b3d64fb9a444bc) ```diff // FilterLink.js @@ -582,7 +587,7 @@ Redux apps commonly use a library called [Reselect](https://github.com/reduxjs/r RSK re-exports the `createSelector` function from Reselect, so we can import that and use it in `VisibleTodoList`. -> - [Convert visible todos to a memoized selector](https://github.com/markerikson/rsk-convert-todos-example/commit/9f2991bd796138c784ae0d20193c1a399dbe59de) +> - [Convert visible todos to a memoized selector](https://github.com/reduxjs/rsk-convert-todos-example/commit/9f2991bd796138c784ae0d20193c1a399dbe59de) ```diff import { connect } from 'react-redux' @@ -648,8 +653,8 @@ We can safely remove `actions/index.js`, `reducers/todos.js`, `reducers/visibili We can also try completely switching from the "folder-by-type" structure to a "feature folder" structure, by moving all of the component files into the matching feature folders. -> - [Remove unused action and reducer files](https://github.com/markerikson/rsk-convert-todos-example/commit/3ee39ee69917469321903fffbc691a418139f7ef) -> - [Consolidate components into feature folders](https://github.com/markerikson/rsk-convert-todos-example/commit/accc5896b5d092e736ec2d24d10246ff18aec7cf) +> - [Remove unused action and reducer files](https://github.com/reduxjs/rsk-convert-todos-example/commit/3ee39ee69917469321903fffbc691a418139f7ef) +> - [Consolidate components into feature folders](https://github.com/reduxjs/rsk-convert-todos-example/commit/accc5896b5d092e736ec2d24d10246ff18aec7cf) If we do that, the final source code structure looks like this: diff --git a/src/createAction.ts b/src/createAction.ts index 008b24b3a1..2ce3f319f6 100644 --- a/src/createAction.ts +++ b/src/createAction.ts @@ -10,7 +10,7 @@ import { IsUnknownOrNonInferrable } from './tsHelpers' * @template M The type of the action's meta (optional) */ export type PayloadAction< - P = any, + P = void, T extends string = string, M = void > = WithOptionalMeta>> @@ -62,7 +62,7 @@ export type ActionCreatorWithPayload< * An action creator that produces actions with a `payload` attribute. */ export type PayloadActionCreator< - P = any, + P = void, T extends string = string, PA extends PrepareAction

| void = void > = IfPrepareActionMethodProvided< @@ -94,7 +94,7 @@ export type PayloadActionCreator< * If this is given, the resulting action creator will pass it's arguments to this method to calculate payload & meta. */ -export function createAction

( +export function createAction

( type: T ): PayloadActionCreator diff --git a/src/createSlice.test.ts b/src/createSlice.test.ts index 05166924ff..4b4b76d7ae 100644 --- a/src/createSlice.test.ts +++ b/src/createSlice.test.ts @@ -2,56 +2,45 @@ import { createSlice } from './createSlice' import { createAction, PayloadAction } from './createAction' describe('createSlice', () => { - describe('when slice is empty', () => { - const { actions, reducer } = createSlice({ - reducers: { - increment: state => state + 1, - multiply: (state, action: PayloadAction) => - state * action.payload - }, - initialState: 0 - }) - - it('should create increment action', () => { - expect(actions.hasOwnProperty('increment')).toBe(true) - }) - - it('should create multiply action', () => { - expect(actions.hasOwnProperty('multiply')).toBe(true) + describe('when slice is undefined', () => { + it('should throw an error', () => { + expect(() => + // @ts-ignore + createSlice({ + reducers: { + increment: state => state + 1, + multiply: (state, action: PayloadAction) => + state * action.payload + }, + initialState: 0 + }) + ).toThrowError() }) + }) - it('should have the correct action for increment', () => { - expect(actions.increment()).toEqual({ - type: 'increment', - payload: undefined - }) - }) - - it('should have the correct action for multiply', () => { - expect(actions.multiply(3)).toEqual({ - type: 'multiply', - payload: 3 - }) - }) - - describe('when using reducer', () => { - it('should return the correct value from reducer with increment', () => { - expect(reducer(undefined, actions.increment())).toEqual(1) - }) - - it('should return the correct value from reducer with multiply', () => { - expect(reducer(2, actions.multiply(3))).toEqual(6) - }) + describe('when slice is an empty string', () => { + it('should throw an error', () => { + expect(() => + createSlice({ + name: '', + reducers: { + increment: state => state + 1, + multiply: (state, action: PayloadAction) => + state * action.payload + }, + initialState: 0 + }) + ).toThrowError() }) }) describe('when passing slice', () => { - const { actions, reducer } = createSlice({ + const { actions, reducer, caseReducers } = createSlice({ reducers: { increment: state => state + 1 }, initialState: 0, - slice: 'cool' + name: 'cool' }) it('should create increment action', () => { @@ -68,6 +57,12 @@ describe('createSlice', () => { it('should return the correct value from reducer', () => { expect(reducer(undefined, actions.increment())).toEqual(1) }) + + it('should include the generated case reducers', () => { + expect(caseReducers).toBeTruthy() + expect(caseReducers.increment).toBeTruthy() + expect(typeof caseReducers.increment).toBe('function') + }) }) describe('when mutating state object', () => { @@ -80,7 +75,7 @@ describe('createSlice', () => { } }, initialState, - slice: 'user' + name: 'user' }) it('should set the username', () => { @@ -94,6 +89,7 @@ describe('createSlice', () => { const addMore = createAction('ADD_MORE') const { reducer } = createSlice({ + name: 'test', reducers: { increment: state => state + 1, multiply: (state, action) => state * action.payload @@ -116,7 +112,7 @@ describe('createSlice', () => { const prepare = jest.fn((payload, somethingElse) => ({ payload })) const testSlice = createSlice({ - slice: 'test', + name: 'test', initialState: 0, reducers: { testReducer: { @@ -137,7 +133,7 @@ describe('createSlice', () => { const reducer = jest.fn() const testSlice = createSlice({ - slice: 'test', + name: 'test', initialState: 0, reducers: { testReducer: { diff --git a/src/createSlice.ts b/src/createSlice.ts index 09499d393e..42d437023e 100644 --- a/src/createSlice.ts +++ b/src/createSlice.ts @@ -18,12 +18,14 @@ export type SliceActionCreator

= PayloadActionCreator

export interface Slice< State = any, - ActionCreators extends { [key: string]: any } = { [key: string]: any } + CaseReducers extends SliceCaseReducerDefinitions = { + [key: string]: any + } > { /** * The slice name. */ - slice: string + name: string /** * The slice's reducer. @@ -34,7 +36,9 @@ export interface Slice< * Action creators for the types of actions that are handled by the slice * reducer. */ - actions: ActionCreators + actions: CaseReducerActions + + caseReducers: SliceDefinedCaseReducers } /** @@ -42,12 +46,15 @@ export interface Slice< */ export interface CreateSliceOptions< State = any, - CR extends SliceCaseReducers = SliceCaseReducers + CR extends SliceCaseReducerDefinitions< + State, + any + > = SliceCaseReducerDefinitions > { /** * The slice's name. Used to namespace the generated action types. */ - slice?: string + name: string /** * The initial state to be returned by the slice reducer. @@ -74,15 +81,15 @@ type PayloadActions = Record< PayloadAction > -type EnhancedCaseReducer = { +type CaseReducerWithPrepare = { reducer: CaseReducer prepare: PrepareAction } -type SliceCaseReducers = { +type SliceCaseReducerDefinitions = { [ActionType in keyof PA]: | CaseReducer - | EnhancedCaseReducer + | CaseReducerWithPrepare } type IfIsReducerFunctionWithoutAction = R extends ( @@ -90,7 +97,7 @@ type IfIsReducerFunctionWithoutAction = R extends ( ) => any ? True : False -type IfIsEnhancedReducer = R extends { +type IfIsCaseReducerWithPrepare = R extends { prepare: Function } ? True @@ -106,8 +113,21 @@ type PrepareActionForReducer = R extends { prepare: infer Prepare } ? Prepare : never -type CaseReducerActions> = { - [Type in keyof CaseReducers]: IfIsEnhancedReducer< +type ActionForReducer = R extends ( + state: S, + action: PayloadAction +) => S + ? PayloadAction

+ : R extends { + reducer(state: any, action: PayloadAction): any + } + ? PayloadAction

+ : unknown + +type CaseReducerActions< + CaseReducers extends SliceCaseReducerDefinitions +> = { + [Type in keyof CaseReducers]: IfIsCaseReducerWithPrepare< CaseReducers[Type], ActionCreatorWithPreparedPayload< PrepareActionForReducer @@ -122,6 +142,16 @@ type CaseReducerActions> = { > } +type SliceDefinedCaseReducers< + CaseReducers extends SliceCaseReducerDefinitions, + State = any +> = { + [Type in keyof CaseReducers]: CaseReducer< + State, + ActionForReducer + > +} + type NoInfer = [T][T extends any ? 0 : never] type SliceCaseReducersCheck = { @@ -134,18 +164,18 @@ type SliceCaseReducersCheck = { : {} } -type RestrictEnhancedReducersToMatchReducerAndPrepare< +type RestrictCaseReducerDefinitionsToMatchReducerAndPrepare< S, - CR extends SliceCaseReducers + CR extends SliceCaseReducerDefinitions > = { reducers: SliceCaseReducersCheck> } function getType(slice: string, actionKey: string): string { - return slice ? `${slice}/${actionKey}` : actionKey + return `${slice}/${actionKey}` } /** * A function that accepts an initial state, an object full of reducer - * functions, and optionally a "slice name", and automatically generates + * functions, and a "slice name", and automatically generates * action creators and action types that correspond to the * reducers and state. * @@ -153,51 +183,59 @@ function getType(slice: string, actionKey: string): string { */ export function createSlice< State, - CaseReducers extends SliceCaseReducers + CaseReducers extends SliceCaseReducerDefinitions >( options: CreateSliceOptions & - RestrictEnhancedReducersToMatchReducerAndPrepare -): Slice> + RestrictCaseReducerDefinitionsToMatchReducerAndPrepare +): Slice // internal definition is a little less restrictive export function createSlice< State, - CaseReducers extends SliceCaseReducers + CaseReducers extends SliceCaseReducerDefinitions >( options: CreateSliceOptions -): Slice> { - const { slice = '', initialState } = options +): Slice { + const { name, initialState } = options + if (!name) { + throw new Error('`name` is a required option for createSlice') + } const reducers = options.reducers || {} const extraReducers = options.extraReducers || {} - const actionKeys = Object.keys(reducers) - - const reducerMap = actionKeys.reduce((map, actionKey) => { - let maybeEnhancedReducer = reducers[actionKey] - map[getType(slice, actionKey)] = - typeof maybeEnhancedReducer === 'function' - ? maybeEnhancedReducer - : maybeEnhancedReducer.reducer - return map - }, extraReducers) - - const reducer = createReducer(initialState, reducerMap) - - const actionMap = actionKeys.reduce( - (map, action) => { - let maybeEnhancedReducer = reducers[action] - const type = getType(slice, action) - map[action] = - typeof maybeEnhancedReducer === 'function' - ? createAction(type) - : createAction(type, maybeEnhancedReducer.prepare) - return map - }, - {} as any - ) + const reducerNames = Object.keys(reducers) + + const sliceCaseReducersByName: Record = {} + const sliceCaseReducersByType: Record = {} + const actionCreators: Record = {} + + reducerNames.forEach(reducerName => { + const maybeReducerWithPrepare = reducers[reducerName] + const type = getType(name, reducerName) + + let caseReducer: CaseReducer + let prepareCallback: PrepareAction | undefined + + if (typeof maybeReducerWithPrepare === 'function') { + caseReducer = maybeReducerWithPrepare + } else { + caseReducer = maybeReducerWithPrepare.reducer + prepareCallback = maybeReducerWithPrepare.prepare + } + + sliceCaseReducersByName[reducerName] = caseReducer + sliceCaseReducersByType[type] = caseReducer + actionCreators[reducerName] = prepareCallback + ? createAction(type, prepareCallback) + : createAction(type) + }) + + const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType } + const reducer = createReducer(initialState, finalCaseReducers) return { - slice, + name, reducer, - actions: actionMap + actions: actionCreators as any, + caseReducers: sliceCaseReducersByName as any } } diff --git a/type-tests/files/createAction.typetest.ts b/type-tests/files/createAction.typetest.ts index fed37a1d9b..b4755c462d 100644 --- a/type-tests/files/createAction.typetest.ts +++ b/type-tests/files/createAction.typetest.ts @@ -19,11 +19,14 @@ function expectType(p: T): T { } /* - * Test: PayloadAction type parameter is optional (defaults to `any`). + * Test: PayloadAction type parameter is required. */ { + // typings:expect-error const action: PayloadAction = { type: '', payload: 5 } + // typings:expect-error const numberPayload: number = action.payload + // typings:expect-error const stringPayload: string = action.payload } @@ -31,7 +34,7 @@ function expectType(p: T): T { * Test: PayloadAction has a string type tag. */ { - const action: PayloadAction = { type: '', payload: 5 } + const action: PayloadAction = { type: '', payload: 5 } // typings:expect-error const action2: PayloadAction = { type: 1, payload: 5 } @@ -41,7 +44,7 @@ function expectType(p: T): T { * Test: PayloadAction is compatible with Action */ { - const action: PayloadAction = { type: '', payload: 5 } + const action: PayloadAction = { type: '', payload: 5 } const stringAction: Action = action } @@ -58,7 +61,7 @@ function expectType(p: T): T { payload }), { type: 'action' } - ) as PayloadActionCreator + ) as PayloadActionCreator expectType>(actionCreator(1)) expectType>(actionCreator()) @@ -110,22 +113,18 @@ function expectType(p: T): T { } /* - * Test: createAction() type parameter is optional (defaults to `any`). + * Test: createAction() type parameter is required, not inferred (defaults to `void`). */ { const increment = createAction('increment') - const n: number = increment(1).payload - const s: string = increment('1').payload - - // but infers the payload type to be the argument type // typings:expect-error - const t: string = increment(1).payload + const n: number = increment(1).payload } /* * Test: createAction().type is a string literal. */ { - const increment = createAction('increment') + const increment = createAction('increment') const n: string = increment(1).type const s: 'increment' = increment(1).type diff --git a/type-tests/files/createSlice.typetest.ts b/type-tests/files/createSlice.typetest.ts index 0aa8d92cec..ff6299d8d0 100644 --- a/type-tests/files/createSlice.typetest.ts +++ b/type-tests/files/createSlice.typetest.ts @@ -12,7 +12,7 @@ function expectType(t: T) { const firstAction = createAction<{ count: number }>('FIRST_ACTION') const slice = createSlice({ - slice: 'counter', + name: 'counter', initialState: 0, reducers: { increment: (state: number, action) => state + action.payload, @@ -47,7 +47,7 @@ function expectType(t: T) { */ { const counter = createSlice({ - slice: 'counter', + name: 'counter', initialState: 0, reducers: { increment: state => state + 1, @@ -85,7 +85,7 @@ function expectType(t: T) { */ { const counter = createSlice({ - slice: 'counter', + name: 'counter', initialState: 0, reducers: { increment: state => state + 1, @@ -112,7 +112,7 @@ function expectType(t: T) { */ { const counter = createSlice({ - slice: 'test', + name: 'test', initialState: { counter: 0, concat: '' }, reducers: { incrementByStrLen: { @@ -147,13 +147,64 @@ function expectType(t: T) { expectType(counter.actions.concatMetaStrLen('test').meta) } +/* + * Test: returned case reducer has the correct type + */ +{ + const counter = createSlice({ + name: 'counter', + initialState: 0, + reducers: { + increment(state, action: PayloadAction) { + return state + action.payload + }, + decrement: { + reducer(state, action: PayloadAction) { + return state - action.payload + }, + prepare(amount: number) { + return { payload: amount } + } + } + } + }) + + // Should match positively + expectType<(state: number, action: PayloadAction) => number | void>( + counter.caseReducers.increment + ) + + // Should match positively for reducers with prepare callback + expectType<(state: number, action: PayloadAction) => number | void>( + counter.caseReducers.decrement + ) + + // Should not mismatch the payload if it's a simple reducer + // typings:expect-error + expectType<(state: number, action: PayloadAction) => number | void>( + counter.caseReducers.increment + ) + + // Should not mismatch the payload if it's a reducer with a prepare callback + // typings:expect-error + expectType<(state: number, action: PayloadAction) => number | void>( + counter.caseReducers.decrement + ) + + // Should not include entries that don't exist + // typings:expect-error + expectType<(state: number, action: PayloadAction) => number | void>( + counter.caseReducers.someThingNonExistant + ) +} + /* * Test: prepared payload does not match action payload - should cause an error. */ { // typings:expect-error const counter = createSlice({ - slice: 'counter', + name: 'counter', initialState: { counter: 0 }, reducers: { increment: { @@ -180,6 +231,7 @@ function expectType(t: T) { } const mySlice = createSlice({ + name: 'name', initialState, reducers: { setName: (state, action) => {