Skip to content

Commit

Permalink
Merge branch 'master' into unknown-action
Browse files Browse the repository at this point in the history
  • Loading branch information
ben.durrant committed May 16, 2023
2 parents 043da88 + 5076b4f commit 8c0d688
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 127 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ jobs:
fail-fast: false
matrix:
node: ['16.x']
ts: ['4.2', '4.3', '4.4', '4.5', '4.6', '4.7', '4.8', '4.9', '5.0']
ts: ['4.7', '4.8', '4.9', '5.0']
steps:
- name: Checkout repo
uses: actions/checkout@v2
Expand Down
4 changes: 2 additions & 2 deletions docs/faq/Actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ sidebar_label: Actions

## Actions

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

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.

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

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.

Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/fundamentals/part-7-standard-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ Here's what the app looks like with that loading status enabled (to see the spin
## Flux Standard Actions

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

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.

Expand Down
127 changes: 64 additions & 63 deletions docs/usage/WritingCustomMiddleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,74 +39,75 @@ You might still want to use custom middleware in one of two cases:
### Create side effects for actions

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):

```ts
const middleware: ListenerMiddleware<S, D, ExtraArgument> =
(api) => (next) => (action) => {
if (addListener.match(action)) {
return startListening(action.payload)
}

if (clearAllListeners.match(action)) {
clearListenerMiddleware()
return
}
api => next => action => {
if (addListener.match(action)) {
return startListening(action.payload)
}

if (removeListener.match(action)) {
return stopListening(action.payload)
}
if (clearAllListeners.match(action)) {
clearListenerMiddleware()
return
}

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

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

return originalState as S
// `getOriginalState` can only be called synchronously.
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
const getOriginalState = (): S => {
if (originalState === INTERNAL_NIL_TOKEN) {
throw new Error(
`${alm}: getOriginalState can only be called synchronously`
)
}

let result: unknown
return originalState as S
}

try {
// Actually forward the action to the reducer before we handle listeners
result = next(action)
let result: unknown

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

try {
runListener = entry.predicate(action, currentState, originalState)
} catch (predicateError) {
runListener = false
if (listenerMap.size > 0) {
let currentState = api.getState()
// Work around ESBuild+TS transpilation issue
const listenerEntries = Array.from(listenerMap.values())
for (let entry of listenerEntries) {
let runListener = false

safelyNotifyError(onError, predicateError, {
raisedBy: 'predicate',
})
}
try {
runListener = entry.predicate(action, currentState, originalState)
} catch (predicateError) {
runListener = false

if (!runListener) {
continue
}
safelyNotifyError(onError, predicateError, {
raisedBy: 'predicate'
})
}

notifyListener(entry, action, api, getOriginalState)
if (!runListener) {
continue
}

notifyListener(entry, action, api, getOriginalState)
}
} finally {
// Remove `originalState` store from this scope.
originalState = INTERNAL_NIL_TOKEN
}

return result
} finally {
// Remove `originalState` store from this scope.
originalState = INTERNAL_NIL_TOKEN
}

return result
}
```

In the first part, it listens to `addListener`, `clearAllListeners` and `removeListener` actions to change which listeners should be invoked later on.
Expand All @@ -120,20 +121,20 @@ It is common to have side effects after dispatching th eaction, because this all
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):

```ts
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
// If this "action" is really a function, call it and return the result.
if (typeof action === 'function') {
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
return action(dispatch, getState, extraArgument)
}

// Otherwise, pass the action down the middleware chain as usual
return next(action)
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
// If this "action" is really a function, call it and return the result.
if (typeof action === 'function') {
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
return action(dispatch, getState, extraArgument)
}

// Otherwise, pass the action down the middleware chain as usual
return next(action)
}
```

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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux",
"version": "5.0.0-alpha.5",
"version": "5.0.0-alpha.6",
"description": "Predictable state container for JavaScript apps",
"license": "MIT",
"homepage": "http://redux.js.org",
Expand Down
8 changes: 8 additions & 0 deletions src/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,14 @@ export function createStore<
)
}

if (typeof action.type !== 'string') {
throw new Error(
`Action "type" property must be a string. Instead, the actual type was: '${kindOf(
action.type
)}'. Value was: '${action.type}' (stringified)`
)
}

if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
Expand Down
5 changes: 2 additions & 3 deletions src/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@
*
* Actions must have a `type` field that indicates the type of action being
* performed. Types can be defined as constants and imported from another
* module. It's better to use strings for `type` than Symbols because strings
* are serializable.
* module. These must be strings, as strings are serializable.
*
* Other than `type`, the structure of an action object is really up to you.
* If you're interested, check out Flux Standard Action for recommendations on
* how actions should be constructed.
*
* @template T the type of the action's `type` tag.
*/
export type Action<T = unknown> = {
export interface Action<T extends string = string> {
type: T
}

Expand Down
4 changes: 1 addition & 3 deletions src/types/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ export type ReducerFromReducersMapObject<M> = M[keyof M] extends
*
* @template R Type of reducer.
*/
export type ActionFromReducer<R> = R extends
| Reducer<any, infer A, any>
| undefined
export type ActionFromReducer<R> = R extends Reducer<any, infer A, any>
? A
: never

Expand Down
26 changes: 3 additions & 23 deletions test/combineReducers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('Utils', () => {

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

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

it('allows a symbol to be used as an action type', () => {
const increment = Symbol('INCREMENT')

const reducer = combineReducers({
counter(state: number = 0, action: Action<unknown>) {
switch (action.type) {
case increment:
return state + 1
default:
return state
}
}
})

expect(reducer({ counter: 0 }, { type: increment }).counter).toEqual(1)
})

it('maintains referential equality if the reducers it is combining do', () => {
const reducer = combineReducers({
child1(state = {}) {
Expand All @@ -160,10 +143,7 @@ describe('Utils', () => {
child1(state = {}) {
return state
},
child2(
state: { count: number } = { count: 0 },
action: Action<unknown>
) {
child2(state: { count: number } = { count: 0 }, action: Action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
Expand Down
14 changes: 10 additions & 4 deletions test/createStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,14 +595,20 @@ describe('createStore', () => {
)
})

it('does not throw if action type is falsy', () => {
it('throws if action type is not string', () => {
const store = createStore(reducers.todos)
// @ts-expect-error
expect(() => store.dispatch({ type: false })).not.toThrow()
expect(() => store.dispatch({ type: false })).toThrow(
/the actual type was: 'boolean'.*Value was: 'false'/
)
// @ts-expect-error
expect(() => store.dispatch({ type: 0 })).not.toThrow()
expect(() => store.dispatch({ type: 0 })).toThrow(
/the actual type was: 'number'.*Value was: '0'/
)
// @ts-expect-error
expect(() => store.dispatch({ type: null })).not.toThrow()
expect(() => store.dispatch({ type: null })).toThrow(
/the actual type was: 'null'.*Value was: 'null'/
)
// @ts-expect-error
expect(() => store.dispatch({ type: '' })).not.toThrow()
})
Expand Down
18 changes: 0 additions & 18 deletions test/typescript/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,3 @@ namespace StringLiteralTypeAction {

const type: ActionType = action.type
}

namespace EnumTypeAction {
enum ActionType {
A,
B,
C
}

interface Action extends ReduxAction {
type: ActionType
}

const action: Action = {
type: ActionType.A
}

const type: ActionType = action.type
}
Loading

0 comments on commit 8c0d688

Please sign in to comment.