Skip to content

Commit e234bd5

Browse files
authored
improved redux section text and examples (#36)
* improved redux section text and examples updated tslint to strict for 2.6 and future new strict features * review fixes * updated deps * closes #6 * updated TOC * final touches
1 parent 3d66461 commit e234bd5

38 files changed

+517
-592
lines changed

README.md

Lines changed: 179 additions & 187 deletions
Large diffs are not rendered by default.

docs/markdown/1_react.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,20 @@ Adds error handling using componentDidCatch to any component
134134
135135
## Redux Connected Components
136136
137+
### Caveat with `bindActionCreators`
138+
**If you try to use `connect` or `bindActionCreators` explicitly and want to type your component callback props as `() => void` this will raise compiler errors. I happens because `bindActionCreators` typings will not map the return type of action creators to `void`, due to a current TypeScript limitations.**
139+
140+
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.
141+
142+
> 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).
143+
144+
> 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:
145+
```
146+
const mapDispatchToProps = (dispatch: Dispatch) => ({
147+
onIncrement: () => dispatch(actions.increment()),
148+
});
149+
```
150+
137151
#### - redux connected counter
138152
139153
::example='../../playground/src/connected/sfc-counter-connected.tsx'::

docs/markdown/2_redux.md

Lines changed: 68 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -2,72 +2,30 @@
22

33
## Action Creators
44

5-
### KISS Style
6-
This pattern is focused on a KISS principle - to stay clear of complex proprietary abstractions and follow simple and familiar JavaScript const based types:
5+
> Using Typesafe Action Creators helpers for Redux [`typesafe-actions`](https://github.com/piotrwitek/typesafe-actions)
76
8-
Advantages:
9-
- simple "const" based types
10-
- familiar to standard JS usage
11-
12-
Disadvantages:
13-
- significant amount of boilerplate and duplication
14-
- necessary to export both action types and action creators to re-use in other places, e.g. `redux-saga` or `redux-observable`
7+
A recommended approach is to use a simple functional helper to automate the creation of type-safe action creators. The advantage is that we can reduce a lot of code repetition and also minimize surface of errors by using type-checked API.
8+
> There are more specialized functional helpers available that will help you to further reduce tedious boilerplate and type-annotations in common scenarios like reducers (`getType`) or epics (`isActionOf`).
9+
All that without losing type-safety! Please check this very short [Tutorial](https://github.com/piotrwitek/typesafe-actions#tutorial)
1510

1611
::example='../../playground/src/redux/counters/actions.ts'::
1712
::usage='../../playground/src/redux/counters/actions.usage.ts'::
1813

1914
[⇧ back to top](#table-of-contents)
2015

21-
### DRY Style
22-
In a DRY approach, we're introducing a simple factory function to automate the creation process of type-safe action creators. The advantage here is that we can reduce boilerplate and repetition significantly. It is also easier to re-use action creators in other layers thanks to `getType` helper function returning "type constant".
23-
24-
Advantages:
25-
- using factory function to automate creation of type-safe action creators
26-
- less boilerplate and code repetition than KISS Style
27-
- getType helper to obtain action creator type (this makes using "type constants" unnecessary)
28-
29-
```ts
30-
import { createAction, getType } from 'react-redux-typescript';
31-
32-
// Action Creators
33-
export const actionCreators = {
34-
incrementCounter: createAction('INCREMENT_COUNTER'),
35-
showNotification: createAction('SHOW_NOTIFICATION',
36-
(message: string, severity: Severity = 'default') => ({
37-
type: 'SHOW_NOTIFICATION', payload: { message, severity },
38-
})
39-
),
40-
};
41-
42-
// Usage
43-
store.dispatch(actionCreators.incrementCounter(4)); // Error: Expected 0 arguments, but got 1.
44-
store.dispatch(actionCreators.incrementCounter()); // OK: { type: "INCREMENT_COUNTER" }
45-
getType(actionCreators.incrementCounter) === "INCREMENT_COUNTER" // true
46-
47-
store.dispatch(actionCreators.showNotification()); // Error: Supplied parameters do not match any signature of call target.
48-
store.dispatch(actionCreators.showNotification('Hello!')); // OK: { type: "SHOW_NOTIFICATION", payload: { message: 'Hello!', severity: 'default' } }
49-
store.dispatch(actionCreators.showNotification('Hello!', 'info')); // OK: { type: "SHOW_NOTIFICATION", payload: { message: 'Hello!', severity: 'info' } }
50-
getType(actionCreators.showNotification) === "SHOW_NOTIFICATION" // true
51-
```
52-
53-
[⇧ back to top](#table-of-contents)
54-
5516
---
5617

5718
## Reducers
58-
Relevant TypeScript Docs references:
59-
- [Discriminated Union types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
60-
- [Mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html) e.g. `Readonly` & `Partial`
6119

62-
### Tutorial
63-
Declare reducer `State` type definition with readonly modifier for `type level` immutability
20+
### State with Type-level Immutability
21+
Declare reducer `State` type with `readonly` modifier to get "type level" immutability
6422
```ts
6523
export type State = {
6624
readonly counter: number,
6725
};
6826
```
6927

70-
Readonly modifier allow initialization, but will not allow rassignment highlighting an error
28+
Readonly modifier allow initialization, but will not allow rassignment by highlighting compiler errors
7129
```ts
7230
export const initialState: State = {
7331
counter: 0,
@@ -76,69 +34,51 @@ export const initialState: State = {
7634
initialState.counter = 3; // Error, cannot be mutated
7735
```
7836

79-
#### Caveat: Readonly does not provide recursive immutability on objects
80-
> This means that readonly modifier does not propagate immutability on nested properties of objects or arrays of objects. You'll need to set it explicitly on each nested property.
37+
#### Caveat: Readonly does not provide a recursive immutability on objects
38+
This means that the `readonly` modifier doesn't propagate immutability down to "properties" of objects. You'll need to set it explicitly on each nested property that you want.
8139

40+
Check the example below:
8241
```ts
8342
export type State = {
84-
readonly counterContainer: {
85-
readonly readonlyCounter: number,
86-
mutableCounter: number,
43+
readonly containerObject: {
44+
readonly immutableProp: number,
45+
mutableProp: number,
8746
}
8847
};
8948

90-
state.counterContainer = { mutableCounter: 1 }; // Error, cannot be mutated
91-
state.counterContainer.readonlyCounter = 1; // Error, cannot be mutated
49+
state.containerObject = { mutableProp: 1 }; // Error, cannot be mutated
50+
state.containerObject.immutableProp = 1; // Error, cannot be mutated
9251

93-
state.counterContainer.mutableCounter = 1; // No error, can be mutated
52+
state.containerObject.mutableProp = 1; // OK! No error, can be mutated
9453
```
9554

96-
> There are few utilities to help you achieve nested immutability. e.g. you can do it quite easily by using convenient `Readonly` or `ReadonlyArray` mapped types.
55+
#### Best-practices for nested immutability
56+
> use `Readonly` or `ReadonlyArray` [Mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
9757
9858
```ts
9959
export type State = Readonly<{
100-
countersCollection: ReadonlyArray<Readonly<{
101-
readonlyCounter1: number,
102-
readonlyCounter2: number,
60+
counterPairs: ReadonlyArray<Readonly<{
61+
immutableCounter1: number,
62+
immutableCounter2: number,
10363
}>>,
10464
}>;
10565

106-
state.countersCollection[0] = { readonlyCounter1: 1, readonlyCounter2: 1 }; // Error, cannot be mutated
107-
state.countersCollection[0].readonlyCounter1 = 1; // Error, cannot be mutated
108-
state.countersCollection[0].readonlyCounter2 = 1; // Error, cannot be mutated
66+
state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // Error, cannot be mutated
67+
state.counterPairs[0].immutableCounter1 = 1; // Error, cannot be mutated
68+
state.counterPairs[0].immutableCounter2 = 1; // Error, cannot be mutated
10969
```
11070

111-
> _There are some experiments in the community to make a `ReadonlyRecursive` mapped type, but I'll need to investigate if they really works_
71+
> _There are some experiments in the community to make a `ReadonlyRecursive` mapped type. I'll update this section of the guide as soon as they are stable_
11272
11373
[⇧ back to top](#table-of-contents)
11474

115-
### Examples
116-
117-
#### Reducer with classic `const types`
75+
### Reducer Example
76+
> using `getType` helper and [Discriminated Union types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
11877
11978
::example='../../playground/src/redux/counters/reducer.ts'::
12079

12180
[⇧ back to top](#table-of-contents)
12281

123-
#### Reducer with getType helper from `react-redux-typescript`
124-
```ts
125-
import { getType } from 'react-redux-typescript';
126-
127-
export const reducer: Reducer<State> = (state = 0, action: RootAction) => {
128-
switch (action.type) {
129-
case getType(actionCreators.increment):
130-
return state + 1;
131-
132-
case getType(actionCreators.decrement):
133-
return state - 1;
134-
135-
default: return state;
136-
}
137-
};
138-
```
139-
140-
[⇧ back to top](#table-of-contents)
141-
14282
---
14383

14484
## Store Configuration
@@ -166,36 +106,13 @@ When creating the store, use rootReducer. This will set-up a **strongly typed St
166106
167107
::example='../../playground/src/store.ts'::
168108

169-
[⇧ back to top](#table-of-contents)
170-
171109
---
172110

173111
## Async Flow
174112

175113
### "redux-observable"
176114

177-
```ts
178-
// import rxjs operators somewhere...
179-
import { combineEpics, Epic } from 'redux-observable';
180-
181-
import { RootAction, RootState } from '@src/redux';
182-
import { saveState } from '@src/services/local-storage-service';
183-
184-
const SAVING_DELAY = 1000;
185-
186-
// persist state in local storage every 1s
187-
const saveStateInLocalStorage: Epic<RootAction, RootState> = (action$, store) => action$
188-
.debounceTime(SAVING_DELAY)
189-
.do((action: RootAction) => {
190-
// handle side-effects
191-
saveState(store.getState());
192-
})
193-
.ignoreElements();
194-
195-
export const epics = combineEpics(
196-
saveStateInLocalStorage,
197-
);
198-
```
115+
::example='../../playground/src/redux/toasts/epics.ts'::
199116

200117
[⇧ back to top](#table-of-contents)
201118

@@ -233,3 +150,43 @@ export const getFilteredTodos = createSelector(
233150
```
234151

235152
[⇧ back to top](#table-of-contents)
153+
154+
---
155+
156+
### Action Creators - Alternative Pattern
157+
This pattern is focused on a KISS principle - to stay clear of abstractions and to follow a more complex but familiar JavaScript "const" based approach:
158+
159+
Advantages:
160+
- familiar to standard JS "const" based approach
161+
162+
Disadvantages:
163+
- significant amount of boilerplate and duplication
164+
- more complex compared to `createAction` helper library
165+
- necessary to export both action types and action creators to re-use in other places, e.g. `redux-saga` or `redux-observable`
166+
167+
```tsx
168+
export const INCREMENT = 'INCREMENT';
169+
export const ADD = 'ADD';
170+
171+
export type Actions = {
172+
INCREMENT: {
173+
type: typeof INCREMENT,
174+
},
175+
ADD: {
176+
type: typeof ADD,
177+
payload: number,
178+
},
179+
};
180+
181+
export const actions = {
182+
increment: (): Actions[typeof INCREMENT] => ({
183+
type: INCREMENT,
184+
}),
185+
add: (amount: number): Actions[typeof ADD] => ({
186+
type: ADD,
187+
payload: amount,
188+
}),
189+
};
190+
```
191+
192+
[⇧ back to top](#table-of-contents)

docs/markdown/4_extras.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"noImplicitReturns": true,
3939
"noImplicitThis": true,
4040
"noUnusedLocals": true,
41-
"strictNullChecks": true,
41+
"strict": true,
4242
"pretty": true,
4343
"removeComments": true,
4444
"sourceMap": true

docs/markdown/_toc.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
- [Higher-Order Components](#higher-order-components) 📝 __UPDATED__
99
- [Redux Connected Components](#redux-connected-components)
1010
- [Redux](#redux)
11-
- [Action Creators](#action-creators)
12-
- [Reducers](#reducers)
13-
- [Store Configuration](#store-configuration)
14-
- [Async Flow](#async-flow) _("redux-observable")_
15-
- [Selectors](#selectors) _("reselect")_
11+
- [Action Creators](#action-creators) 📝 __UPDATED__
12+
- [Reducers](#reducers) 📝 __UPDATED__
13+
- [State with Type-level Immutability](#state-with-type-level-immutability)
14+
- [Reducer Example](#reducer-example)
15+
- [Store Configuration](#store-configuration) 📝 __UPDATED__
16+
- [Async Flow](#async-flow) 📝 __UPDATED__
17+
- [Selectors](#selectors)
1618
- [Tools](#tools)
17-
- [Living Style Guide](#living-style-guide) _("react-styleguidist")_ 🌟 __NEW__
19+
- [Living Style Guide](#living-style-guide) 🌟 __NEW__
1820
- [Extras](#extras)
1921
- [tsconfig.json](#tsconfigjson)
2022
- [tslint.json](#tslintjson)

playground/.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"typescript.tsdk": "node_modules/typescript/lib"
3+
}

playground/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
"redux": "3.7.2",
2929
"redux-observable": "0.17.0",
3030
"reselect": "3.0.1",
31-
"rxjs": "5.5.5",
31+
"rxjs": "5.5.6",
3232
"tslib": "1.8.1",
33+
"typesafe-actions": "1.0.0-rc.1",
3334
"uuid": "3.1.0"
3435
},
3536
"devDependencies": {

playground/src/api/models.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface ITodoModel {
2-
id: string,
3-
text: string,
4-
completed: false,
2+
id: string;
3+
text: string;
4+
completed: false;
55
}

playground/src/api/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export const resolveWithDelay = <T>(value: T, time: number = 1000) => new Promise(
2-
(resolve) => setTimeout(() => resolve(value), time),
2+
(resolve) => setTimeout(() => resolve(value), time)
33
);
44

55
export const rangeQueryString = (count: number, pageNumber?: number) =>

playground/src/components/generic-list.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as React from 'react';
22

33
export interface GenericListProps<T> {
4-
items: T[],
5-
itemRenderer: (item: T) => JSX.Element,
4+
items: T[];
5+
itemRenderer: (item: T) => JSX.Element;
66
}
77

88
export class GenericList<T> extends React.Component<GenericListProps<T>, {}> {

0 commit comments

Comments
 (0)