Skip to content

Commit c8d35fe

Browse files
committed
Merge branch 'redux-thunk'
2 parents 342a6ad + 0ecf245 commit c8d35fe

19 files changed

+7099
-5482
lines changed

README.md

+147-94
Large diffs are not rendered by default.

README_SOURCE.md

+76-62
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ _"This guide is a **living compendium** documenting the most important patterns
77
88
:tada: _Now updated to be compatible with **TypeScript v3.1.6**_ :tada:
99

10-
:computer: _Reference implementation of Todo-App with `typesafe-actions`: https://codesandbox.io/s/github/piotrwitek/typesafe-actions-todo-app_ :computer:
10+
:computer: _Reference implementation of Todo-App with `typesafe-actions`: https://codesandbox.io/s/github/piotrwitek/typesafe-actions/tree/master/codesandbox_ :computer:
1111

1212
### Goals
1313
- Complete type safety (with [`--strict`](https://www.typescriptlang.org/docs/handbook/compiler-options.html) flag) without losing type information downstream through all the layers of our application (e.g. no type assertions or hacking with `any` type)
@@ -59,9 +59,10 @@ Issues can be funded by anyone and the money will be transparently distributed t
5959
- [Testing reducer](#testing-reducer)
6060
- [Async Flow with `redux-observable`](#async-flow-with-redux-observable)
6161
- [Typing Epics](#typing-epics)
62-
- [Testing Epics](#testing-epics) 🌟 __NEW__
63-
- [Selectors](#selectors)
64-
- [Typing connect](#typing-connect)
62+
- [Testing Epics](#testing-epics)
63+
- [Async Flow with `redux-thunk`](#async-flow-with-redux-thunk) 🌟 __NEW__
64+
- [Selectors with `reselect`](#selectors-with-reselect)
65+
- [Connect with `react-redux`](#connect-with-react-redux) 🌟 __NEW__
6566
- [Tools](#tools)
6667
- [TSLint](#tslint)
6768
- [Jest](#jest)
@@ -138,7 +139,7 @@ const Component = ({ children: React.ReactNode }) => ...
138139
```
139140
140141
#### `React.CSSProperties`
141-
Type representing style object in JSX (usefull for css-in-js styles)
142+
Type representing style object in JSX (useful for css-in-js styles)
142143
```tsx
143144
const styles: React.CSSProperties = { flexDirection: 'row', ...
144145
const element = <div style={styles} ...
@@ -266,38 +267,24 @@ Adds error handling using componentDidCatch to any component
266267
267268
## Redux Connected Components
268269
269-
### Caveat with `bindActionCreators`
270-
**If you try to use `connect` or `bindActionCreators` explicitly and want to type your component callback props as `() => void` this will raise compiler errors. It happens because `bindActionCreators` typings will not map the return type of action creators to `void`, due to a current TypeScript limitations.**
271-
272-
A decent alternative I can recommend is to use `() => any` type, it will work just fine in all possible scenarios and should not cause any typing problems whatsoever. All the code examples in the Guide with `connect` are also using this pattern.
273-
274-
> If there is any progress or fix in regard to the above caveat I'll update the guide and make an announcement on my twitter/medium (There are a few existing proposals already).
275-
276-
> There is alternative way to retain type soundness but it requires an explicit wrapping with `dispatch` and will be very tedious for the long run. See example below:
277-
```ts
278-
const mapDispatchToProps = (dispatch: Dispatch<ActionType>) => ({
279-
onIncrement: () => dispatch(actions.increment()),
280-
});
281-
```
282-
283270
#### - redux connected counter
284271
285272
::codeblock='playground/src/connected/fc-counter-connected.tsx'::
286273
::expander='playground/src/connected/fc-counter-connected.usage.tsx'::
287274
288275
[⇧ back to top](#table-of-contents)
289276
290-
#### - redux connected counter (verbose)
277+
#### - redux connected counter with own props
291278
292-
::codeblock='playground/src/connected/fc-counter-connected-verbose.tsx'::
293-
::expander='playground/src/connected/fc-counter-connected-verbose.usage.tsx'::
279+
::codeblock='playground/src/connected/fc-counter-connected-own-props.tsx'::
280+
::expander='playground/src/connected/fc-counter-connected-own-props.usage.tsx'::
294281
295282
[⇧ back to top](#table-of-contents)
296283
297-
#### - with own props
284+
#### - redux connected counter with `redux-thunk` integration
298285
299-
::codeblock='playground/src/connected/fc-counter-connected-extended.tsx'::
300-
::expander='playground/src/connected/fc-counter-connected-extended.usage.tsx'::
286+
::codeblock='playground/src/connected/fc-counter-connected-bind-action-creators.tsx'::
287+
::expander='playground/src/connected/fc-counter-connected-bind-action-creators.usage.tsx'::
301288
302289
[⇧ back to top](#table-of-contents)
303290
@@ -386,9 +373,9 @@ When creating a store instance we don't need to provide any additional types. It
386373
## Action Creators
387374
388375
> We'll be using a battle-tested library [![NPM Downloads](https://img.shields.io/npm/dm/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)
389-
that automates and simplify maintenace of **type annotations in Redux Architectures** [`typesafe-actions`](https://github.com/piotrwitek/typesafe-actions#typesafe-actions)
376+
that'll help retain complete type soundness and simplify maintenace of **types in Redux Architectures** [`typesafe-actions`](https://github.com/piotrwitek/typesafe-actions#typesafe-actions)
390377
391-
### For more examples and in-depth tutorial you should check [The Mighty Tutorial](https://github.com/piotrwitek/typesafe-actions#behold-the-mighty-tutorial)!
378+
> You can find more real-world examples and in-depth tutorial in: [Typesafe-Actions - The Mighty Tutorial](https://github.com/piotrwitek/typesafe-actions#behold-the-mighty-tutorial)!
392379
393380
A solution below is using a simple factory function to automate the creation of type-safe action creators. The goal is to decrease maintenance effort and reduce code repetition of type annotations for actions and creators. The result is completely typesafe action-creators and their actions.
394381
@@ -425,12 +412,28 @@ state.todos.push('Learn about tagged union types') // TS Error: Property 'push'
425412
const newTodos = state.todos.concat('Learn about tagged union types') // OK
426413
```
427414
428-
#### Caveat: Readonly is not recursive
415+
#### Caveat - `Readonly` is not recursive
429416
This means that the `readonly` modifier doesn't propagate immutability down the nested structure of objects. You'll need to mark each property on each level explicitly.
430417
431-
To fix this we can use [`DeepReadonly`](https://github.com/piotrwitek/utility-types#deepreadonlyt) type (available in `utility-types` npm library - collection of reusable types extending the collection of **standard-lib** in TypeScript.
418+
> **TIP:** use `Readonly` or `ReadonlyArray` [Mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
419+
420+
```ts
421+
export type State = Readonly<{
422+
counterPairs: ReadonlyArray<Readonly<{
423+
immutableCounter1: number,
424+
immutableCounter2: number,
425+
}>>,
426+
}>;
427+
428+
state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // TS Error: cannot be mutated
429+
state.counterPairs[0].immutableCounter1 = 1; // TS Error: cannot be mutated
430+
state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated
431+
```
432+
433+
#### Solution - recursive `Readonly` is called `DeepReadonly`
434+
435+
To fix this we can use [`DeepReadonly`](https://github.com/piotrwitek/utility-types#deepreadonlyt) type (available from `utility-types`).
432436
433-
Check the example below:
434437
```ts
435438
import { DeepReadonly } from 'utility-types';
436439

@@ -446,21 +449,6 @@ state.containerObject.innerValue = 1; // TS Error: cannot be mutated
446449
state.containerObject.numbers.push(1); // TS Error: cannot use mutator methods
447450
```
448451
449-
#### Best-practices for nested immutability
450-
> use `Readonly` or `ReadonlyArray` [Mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
451-
452-
```ts
453-
export type State = Readonly<{
454-
counterPairs: ReadonlyArray<Readonly<{
455-
immutableCounter1: number,
456-
immutableCounter2: number,
457-
}>>,
458-
}>;
459-
460-
state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // TS Error: cannot be mutated
461-
state.counterPairs[0].immutableCounter1 = 1; // TS Error: cannot be mutated
462-
state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated
463-
```
464452
465453
[⇧ back to top](#table-of-contents)
466454
@@ -482,8 +470,6 @@ state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated
482470
483471
## Async Flow with `redux-observable`
484472
485-
### For more examples and in-depth tutorial you should check [The Mighty Tutorial](https://github.com/piotrwitek/typesafe-actions#behold-the-mighty-tutorial)!
486-
487473
### Typing epics
488474
489475
::codeblock='playground/src/features/todos/epics.ts'::
@@ -498,47 +484,75 @@ state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated
498484
499485
---
500486
501-
## Selectors
502-
503-
### "reselect"
487+
## Selectors with `reselect`
504488
505489
::codeblock='playground/src/features/todos/selectors.ts'::
506490
507491
[⇧ back to top](#table-of-contents)
508492
509493
---
510494
511-
## Typing connect
495+
## Connect with `react-redux`
512496
513-
Below snippet can be find in the `playground/` folder, you can checkout the repo and follow all dependencies to understand the bigger picture.
514-
`playground/src/connected/fc-counter-connected-verbose.tsx`
497+
*__NOTE__: Below you'll find only a short explanation of concepts behind typing `connect`. For more real-world examples please check [Redux Connected Components](#redux-connected-components) section.*
515498
516499
```tsx
517-
import Types from 'Types';
500+
import MyTypes from 'MyTypes';
518501

519502
import { bindActionCreators, Dispatch } from 'redux';
520503
import { connect } from 'react-redux';
521504

522505
import { countersActions } from '../features/counters';
523506
import { FCCounter } from '../components';
524507

525-
// `state` parameter needs a type annotation to type-check the correct shape of a state object but also it'll be used by "type inference" to infer the type of returned props
526-
const mapStateToProps = (state: Types.RootState, ownProps: FCCounterProps) => ({
508+
// `state` argument annotation is mandatory to check the correct shape of a state object and injected props
509+
// you can also extend connected component Props type by annotating `ownProps` argument
510+
const mapStateToProps = (state: MyTypes.RootState, ownProps: FCCounterProps) => ({
527511
count: state.counters.reduxCounter,
528512
});
529513

530-
// `dispatch` parameter needs a type annotation to type-check the correct shape of an action object when using dispatch function
531-
const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) => bindActionCreators({
514+
// `dispatch` argument needs an annotation to check the correct shape of an action object
515+
// when using dispatch function
516+
const mapDispatchToProps = (dispatch: Dispatch<MyTypes.RootAction>) => bindActionCreators({
532517
onIncrement: countersActions.increment,
533-
// without using action creators, this will be validated using your RootAction union type
534-
// onIncrement: () => dispatch({ type: "counters/INCREMENT" }),
535518
}, dispatch);
536519

537-
// NOTE: We don't need to pass generic type arguments to neither connect nor mapping functions because type inference will do all this work automatically. So there's really no reason to increase the noise ratio in your codebase!
538-
export const FCCounterConnectedVerbose =
520+
// shorter alternative is to use an object instead of mapDispatchToProps function
521+
const dispatchToProps = {
522+
onIncrement: countersActions.increment,
523+
};
524+
525+
// Notice ee don't need to pass any generic type parameters to neither connect nor map functions above
526+
// because type inference will infer types from arguments annotations automatically
527+
// It's much cleaner and idiomatic approach
528+
export const FCCounterConnected =
539529
connect(mapStateToProps, mapDispatchToProps)(FCCounter);
540530
```
541531
532+
*__NOTE__ (for `redux-thunk`): When using thunk action creators you need to use `bindActionCreators`. Only this way you can get corrected dispatch props type signature like below.*
533+
534+
*__WARNING__: As of now (Apr 2019) `bindActionCreators` signature of the latest `redux-thunk` release will not work as below, you need to use updated type definitions that you can find in `/playground/typings/redux-thunk` folder and then add paths overload in your tsconfig like this: `"paths":{"redux-thunk":["typings/redux-thunk"]}`.*
535+
536+
```tsx
537+
const thunkAsyncAction = () => async (dispatch: Dispatch): Promise<void> => {
538+
// dispatch actions, return Promise, etc.
539+
}
540+
541+
const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) =>
542+
bindActionCreators(
543+
{
544+
thunkAsyncAction,
545+
},
546+
dispatch
547+
);
548+
549+
type DispatchProps = ReturnType<typeof mapDispatchToProps>;
550+
// { thunkAsyncAction: () => Promise<void>; }
551+
552+
/* Without "bindActionCreators" fix signature will be the same as the original "unbound" thunk function: */
553+
// { thunkAsyncAction: () => (dispatch: Dispatch<AnyAction>) => Promise<void>; }
554+
```
555+
542556
[⇧ back to top](#table-of-contents)
543557
544558
---

0 commit comments

Comments
 (0)