Skip to content

Commit 8c0d688

Browse files
author
ben.durrant
committed
Merge branch 'master' into unknown-action
2 parents 043da88 + 5076b4f commit 8c0d688

File tree

12 files changed

+101
-127
lines changed

12 files changed

+101
-127
lines changed

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ jobs:
100100
fail-fast: false
101101
matrix:
102102
node: ['16.x']
103-
ts: ['4.2', '4.3', '4.4', '4.5', '4.6', '4.7', '4.8', '4.9', '5.0']
103+
ts: ['4.7', '4.8', '4.9', '5.0']
104104
steps:
105105
- name: Checkout repo
106106
uses: actions/checkout@v2

docs/faq/Actions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ sidebar_label: Actions
2323

2424
## Actions
2525

26-
### Why should `type` be a string, or at least serializable? Why should my action types be constants?
26+
### Why should `type` be a string? Why should my action types be constants?
2727

2828
As with state, serializable actions enable several of Redux's defining features, such as time travel debugging, and recording and replaying actions. Using something like a `Symbol` for the `type` value or using `instanceof` checks for actions themselves would break that. Strings are serializable and easily self-descriptive, and so are a better choice. Note that it _is_ okay to use Symbols, Promises, or other non-serializable values in an action if the action is intended for use by middleware. Actions only need to be serializable by the time they actually reach the store and are passed to the reducers.
2929

30-
We can't reliably enforce serializable actions for performance reasons, so Redux only checks that every action is a plain object, and that the `type` is defined. The rest is up to you, but you might find that keeping everything serializable helps debug and reproduce issues.
30+
We can't reliably enforce serializable actions for performance reasons, so Redux only checks that every action is a plain object, and that the `type` is a string. The rest is up to you, but you might find that keeping everything serializable helps debug and reproduce issues.
3131

3232
Encapsulating and centralizing commonly used pieces of code is a key concept in programming. While it is certainly possible to manually create action objects everywhere, and write each `type` value by hand, defining reusable constants makes maintaining code easier. If you put constants in a separate file, you can [check your `import` statements against typos](https://www.npmjs.com/package/eslint-plugin-import) so you can't accidentally use the wrong string.
3333

docs/tutorials/fundamentals/part-7-standard-patterns.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ Here's what the app looks like with that loading status enabled (to see the spin
651651
652652
## Flux Standard Actions
653653

654-
The Redux store itself does not actually care what fields you put into your action object. It only cares that `action.type` exists and has a value, and normal Redux actions always use a string for `action.type`. That means that you _could_ put any other fields into the action that you want. Maybe we could have `action.todo` for a "todo added" action, or `action.color`, and so on.
654+
The Redux store itself does not actually care what fields you put into your action object. It only cares that `action.type` exists and is a string. That means that you _could_ put any other fields into the action that you want. Maybe we could have `action.todo` for a "todo added" action, or `action.color`, and so on.
655655

656656
However, if every action uses different field names for its data fields, it can be hard to know ahead of time what fields you need to handle in each reducer.
657657

docs/usage/WritingCustomMiddleware.md

Lines changed: 64 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -39,74 +39,75 @@ You might still want to use custom middleware in one of two cases:
3939
### Create side effects for actions
4040

4141
This is the most common middleware. Here's what it looks like for [rtk listener middleware](https://github.com/reduxjs/redux-toolkit/blob/0678c2e195a70c34cd26bddbfd29043bc36d1362/packages/toolkit/src/listenerMiddleware/index.ts#L427):
42+
4243
```ts
4344
const middleware: ListenerMiddleware<S, D, ExtraArgument> =
44-
(api) => (next) => (action) => {
45-
if (addListener.match(action)) {
46-
return startListening(action.payload)
47-
}
48-
49-
if (clearAllListeners.match(action)) {
50-
clearListenerMiddleware()
51-
return
52-
}
45+
api => next => action => {
46+
if (addListener.match(action)) {
47+
return startListening(action.payload)
48+
}
5349

54-
if (removeListener.match(action)) {
55-
return stopListening(action.payload)
56-
}
50+
if (clearAllListeners.match(action)) {
51+
clearListenerMiddleware()
52+
return
53+
}
5754

58-
// Need to get this state _before_ the reducer processes the action
59-
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()
55+
if (removeListener.match(action)) {
56+
return stopListening(action.payload)
57+
}
6058

61-
// `getOriginalState` can only be called synchronously.
62-
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
63-
const getOriginalState = (): S => {
64-
if (originalState === INTERNAL_NIL_TOKEN) {
65-
throw new Error(
66-
`${alm}: getOriginalState can only be called synchronously`
67-
)
68-
}
59+
// Need to get this state _before_ the reducer processes the action
60+
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()
6961

70-
return originalState as S
62+
// `getOriginalState` can only be called synchronously.
63+
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
64+
const getOriginalState = (): S => {
65+
if (originalState === INTERNAL_NIL_TOKEN) {
66+
throw new Error(
67+
`${alm}: getOriginalState can only be called synchronously`
68+
)
7169
}
7270

73-
let result: unknown
71+
return originalState as S
72+
}
7473

75-
try {
76-
// Actually forward the action to the reducer before we handle listeners
77-
result = next(action)
74+
let result: unknown
7875

79-
if (listenerMap.size > 0) {
80-
let currentState = api.getState()
81-
// Work around ESBuild+TS transpilation issue
82-
const listenerEntries = Array.from(listenerMap.values())
83-
for (let entry of listenerEntries) {
84-
let runListener = false
76+
try {
77+
// Actually forward the action to the reducer before we handle listeners
78+
result = next(action)
8579

86-
try {
87-
runListener = entry.predicate(action, currentState, originalState)
88-
} catch (predicateError) {
89-
runListener = false
80+
if (listenerMap.size > 0) {
81+
let currentState = api.getState()
82+
// Work around ESBuild+TS transpilation issue
83+
const listenerEntries = Array.from(listenerMap.values())
84+
for (let entry of listenerEntries) {
85+
let runListener = false
9086

91-
safelyNotifyError(onError, predicateError, {
92-
raisedBy: 'predicate',
93-
})
94-
}
87+
try {
88+
runListener = entry.predicate(action, currentState, originalState)
89+
} catch (predicateError) {
90+
runListener = false
9591

96-
if (!runListener) {
97-
continue
98-
}
92+
safelyNotifyError(onError, predicateError, {
93+
raisedBy: 'predicate'
94+
})
95+
}
9996

100-
notifyListener(entry, action, api, getOriginalState)
97+
if (!runListener) {
98+
continue
10199
}
100+
101+
notifyListener(entry, action, api, getOriginalState)
102102
}
103-
} finally {
104-
// Remove `originalState` store from this scope.
105-
originalState = INTERNAL_NIL_TOKEN
106103
}
107-
108-
return result
104+
} finally {
105+
// Remove `originalState` store from this scope.
106+
originalState = INTERNAL_NIL_TOKEN
109107
}
108+
109+
return result
110+
}
110111
```
111112

112113
In the first part, it listens to `addListener`, `clearAllListeners` and `removeListener` actions to change which listeners should be invoked later on.
@@ -120,20 +121,20 @@ It is common to have side effects after dispatching th eaction, because this all
120121
While these patterns are less common, most of them (except for cancelling actions) are used by [redux thunk middleware](https://github.com/reduxjs/redux-thunk/blob/587a85b1d908e8b7cf2297bec6e15807d3b7dc62/src/index.ts#L22):
121122

122123
```ts
123-
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
124-
({ dispatch, getState }) =>
125-
next =>
126-
action => {
127-
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
128-
// If this "action" is really a function, call it and return the result.
129-
if (typeof action === 'function') {
130-
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
131-
return action(dispatch, getState, extraArgument)
132-
}
133-
134-
// Otherwise, pass the action down the middleware chain as usual
135-
return next(action)
124+
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
125+
({ dispatch, getState }) =>
126+
next =>
127+
action => {
128+
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
129+
// If this "action" is really a function, call it and return the result.
130+
if (typeof action === 'function') {
131+
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
132+
return action(dispatch, getState, extraArgument)
136133
}
134+
135+
// Otherwise, pass the action down the middleware chain as usual
136+
return next(action)
137+
}
137138
```
138139

139140
Usually, `dispatch` can only handle JSON actions. This middleware adds the ability to also handle actions in the form of functions. It also changes the return type of the dispatch function itself by passing the return value of the function-action to be the return value of the dispatch function.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redux",
3-
"version": "5.0.0-alpha.5",
3+
"version": "5.0.0-alpha.6",
44
"description": "Predictable state container for JavaScript apps",
55
"license": "MIT",
66
"homepage": "http://redux.js.org",

src/createStore.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,14 @@ export function createStore<
282282
)
283283
}
284284

285+
if (typeof action.type !== 'string') {
286+
throw new Error(
287+
`Action "type" property must be a string. Instead, the actual type was: '${kindOf(
288+
action.type
289+
)}'. Value was: '${action.type}' (stringified)`
290+
)
291+
}
292+
285293
if (isDispatching) {
286294
throw new Error('Reducers may not dispatch actions.')
287295
}

src/types/actions.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
*
77
* Actions must have a `type` field that indicates the type of action being
88
* performed. Types can be defined as constants and imported from another
9-
* module. It's better to use strings for `type` than Symbols because strings
10-
* are serializable.
9+
* module. These must be strings, as strings are serializable.
1110
*
1211
* Other than `type`, the structure of an action object is really up to you.
1312
* If you're interested, check out Flux Standard Action for recommendations on
1413
* how actions should be constructed.
1514
*
1615
* @template T the type of the action's `type` tag.
1716
*/
18-
export type Action<T = unknown> = {
17+
export interface Action<T extends string = string> {
1918
type: T
2019
}
2120

src/types/reducers.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ export type ReducerFromReducersMapObject<M> = M[keyof M] extends
8383
*
8484
* @template R Type of reducer.
8585
*/
86-
export type ActionFromReducer<R> = R extends
87-
| Reducer<any, infer A, any>
88-
| undefined
86+
export type ActionFromReducer<R> = R extends Reducer<any, infer A, any>
8987
? A
9088
: never
9189

test/combineReducers.spec.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('Utils', () => {
6464

6565
it('throws an error if a reducer returns undefined handling an action', () => {
6666
const reducer = combineReducers({
67-
counter(state: number = 0, action: Action<unknown>) {
67+
counter(state: number = 0, action: Action) {
6868
switch (action && action.type) {
6969
case 'increment':
7070
return state + 1
@@ -94,7 +94,7 @@ describe('Utils', () => {
9494

9595
it('throws an error on first call if a reducer returns undefined initializing', () => {
9696
const reducer = combineReducers({
97-
counter(state: number, action: Action<unknown>) {
97+
counter(state: number, action: Action) {
9898
switch (action.type) {
9999
case 'increment':
100100
return state + 1
@@ -121,23 +121,6 @@ describe('Utils', () => {
121121
)
122122
})
123123

124-
it('allows a symbol to be used as an action type', () => {
125-
const increment = Symbol('INCREMENT')
126-
127-
const reducer = combineReducers({
128-
counter(state: number = 0, action: Action<unknown>) {
129-
switch (action.type) {
130-
case increment:
131-
return state + 1
132-
default:
133-
return state
134-
}
135-
}
136-
})
137-
138-
expect(reducer({ counter: 0 }, { type: increment }).counter).toEqual(1)
139-
})
140-
141124
it('maintains referential equality if the reducers it is combining do', () => {
142125
const reducer = combineReducers({
143126
child1(state = {}) {
@@ -160,10 +143,7 @@ describe('Utils', () => {
160143
child1(state = {}) {
161144
return state
162145
},
163-
child2(
164-
state: { count: number } = { count: 0 },
165-
action: Action<unknown>
166-
) {
146+
child2(state: { count: number } = { count: 0 }, action: Action) {
167147
switch (action.type) {
168148
case 'increment':
169149
return { count: state.count + 1 }

test/createStore.spec.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -595,14 +595,20 @@ describe('createStore', () => {
595595
)
596596
})
597597

598-
it('does not throw if action type is falsy', () => {
598+
it('throws if action type is not string', () => {
599599
const store = createStore(reducers.todos)
600600
// @ts-expect-error
601-
expect(() => store.dispatch({ type: false })).not.toThrow()
601+
expect(() => store.dispatch({ type: false })).toThrow(
602+
/the actual type was: 'boolean'.*Value was: 'false'/
603+
)
602604
// @ts-expect-error
603-
expect(() => store.dispatch({ type: 0 })).not.toThrow()
605+
expect(() => store.dispatch({ type: 0 })).toThrow(
606+
/the actual type was: 'number'.*Value was: '0'/
607+
)
604608
// @ts-expect-error
605-
expect(() => store.dispatch({ type: null })).not.toThrow()
609+
expect(() => store.dispatch({ type: null })).toThrow(
610+
/the actual type was: 'null'.*Value was: 'null'/
611+
)
606612
// @ts-expect-error
607613
expect(() => store.dispatch({ type: '' })).not.toThrow()
608614
})

test/typescript/actions.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,3 @@ namespace StringLiteralTypeAction {
3939

4040
const type: ActionType = action.type
4141
}
42-
43-
namespace EnumTypeAction {
44-
enum ActionType {
45-
A,
46-
B,
47-
C
48-
}
49-
50-
interface Action extends ReduxAction {
51-
type: ActionType
52-
}
53-
54-
const action: Action = {
55-
type: ActionType.A
56-
}
57-
58-
const type: ActionType = action.type
59-
}

0 commit comments

Comments
 (0)