Skip to content

Commit 5837dc6

Browse files
committed
improved redux section text and examples
updated tslint to strict for 2.6 and future new strict features
1 parent 3d66461 commit 5837dc6

37 files changed

+402
-445
lines changed

README.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
### Goals
77
- Complete type safety with [`--strict`](https://www.typescriptlang.org/docs/handbook/compiler-options.html) flag without failing to `any` type for the best static-typing experience
88
- Minimize amount of manually writing type declarations by leveraging [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html)
9-
- Reduce redux boilerplate and complexity of it's type annotations to a minimum with [simple utility functions](https://github.com/piotrwitek/react-redux-typescript) by extensive use of [Generics](https://www.typescriptlang.org/docs/handbook/generics.html) and [Advanced Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html) features
9+
- Reduce redux boilerplate code with [simple utility functions](https://github.com/piotrwitek/typesafe-actions) using [Generics](https://www.typescriptlang.org/docs/handbook/generics.html) and [Advanced Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html) features
1010

1111
### Playground Project
1212
You should check Playground Project located in the `/playground` folder. It is a source of all the code examples found in the guide. They are all tested with the most recent version of TypeScript and 3rd party type definitions (like `@types/react` or `@types/react-redux`) to ensure the examples are up-to-date and not broken with updated definitions.
@@ -618,6 +618,82 @@ export default () => (
618618
619619
[⇧ back to top](#table-of-contents)
620620
621+
## Higher-Order Components
622+
- function that takes a component and returns a new component
623+
- a new component will infer Props interface from wrapped Component extended with Props of HOC
624+
- will filter out props specific to HOC, and the rest will be passed through to wrapped component
625+
626+
### Basic HOC Examples
627+
628+
#### - withState
629+
> enhance stateless counter with state
630+
631+
```tsx
632+
import * as React from 'react';
633+
import { Omit } from 'react-redux-typescript';
634+
635+
interface RequiredProps {
636+
count: number,
637+
onIncrement: () => any,
638+
}
639+
640+
type Props<T extends RequiredProps> = Omit<T, keyof RequiredProps>;
641+
642+
interface State {
643+
count: number,
644+
}
645+
646+
export function withState<WrappedComponentProps extends RequiredProps>(
647+
WrappedComponent: React.ComponentType<WrappedComponentProps>,
648+
) {
649+
const HOC = class extends React.Component<Props<WrappedComponentProps>, State> {
650+
651+
state: State = {
652+
count: 0,
653+
};
654+
655+
handleIncrement = () => {
656+
this.setState({ count: this.state.count + 1 });
657+
};
658+
659+
render() {
660+
const { handleIncrement } = this;
661+
const { count } = this.state;
662+
663+
return (
664+
<WrappedComponent
665+
count={count}
666+
onIncrement={handleIncrement}
667+
/>
668+
);
669+
}
670+
};
671+
672+
return HOC;
673+
}
674+
675+
```
676+
677+
<details><summary>SHOW USAGE</summary><p>
678+
679+
```tsx
680+
import * as React from 'react';
681+
682+
import { withState } from '@src/hoc';
683+
import { SFCCounter } from '@src/components';
684+
685+
const SFCCounterWithState =
686+
withState(SFCCounter);
687+
688+
export default (
689+
({ children }) => (
690+
<SFCCounterWithState label={'SFCCounterWithState'} />
691+
)
692+
) as React.SFC<{}>;
693+
694+
```
695+
</p></details>
696+
621697
---
622698
623699
# Redux

docs/markdown/1_react_redux.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### bindActionCreators caveat
2+
**If you try to use `connect` or `bindActionCreators` explicitly and type your component callback props as `() => void` this will raise compiler errors because `bindActionCreators` typings will not map your action creator type correctly due to current TypeScript limitations.**
3+
4+
As a decent alternative I'm recommending to use `() => any` type instead, it will work just fine in all scenarios and should not cause any type errors in all possible scenarios.
5+
6+
> All the code examples in the Guide using `connect` are also using this pattern, if there is any progress in TypeScript Language that will fix this I'll update the guide and make a big announcement on my twitter/medium. (There are a few proposals already)
7+
8+
> There is also a way to retain type soundness but it will involve an explicit wrapping with `dispatch` and will be very tedious for the long term, see example:
9+
```
10+
const mapDispatchToProps = (dispatch: Dispatch) => ({
11+
onIncrement: () => dispatch(actions.increment()),
12+
});
13+
```

docs/markdown/2_redux.md

Lines changed: 62 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,72 +2,32 @@
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 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 factory function 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 functional helpers available that will help you to further reduce tedious boilerplate and type-annotations in common scenarios like reducers (`getType`) or epics (`isActionOf`). All that without losing type-safety! Please check very short [Tutorial](https://github.com/piotrwitek/typesafe-actions#tutorial)
159
1610
::example='../../playground/src/redux/counters/actions.ts'::
1711
::usage='../../playground/src/redux/counters/actions.usage.ts'::
1812

1913
[⇧ back to top](#table-of-contents)
2014

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-
5515
---
5616

5717
## Reducers
5818
Relevant TypeScript Docs references:
5919
- [Discriminated Union types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
6020
- [Mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html) e.g. `Readonly` & `Partial`
6121

62-
### Tutorial
63-
Declare reducer `State` type definition with readonly modifier for `type level` immutability
22+
### State with Type-level Immutability
23+
Declare reducer `State` type with `readonly` modifier for "type level" immutability
6424
```ts
6525
export type State = {
6626
readonly counter: number,
6727
};
6828
```
6929

70-
Readonly modifier allow initialization, but will not allow rassignment highlighting an error
30+
Readonly modifier allow initialization, but will not allow rassignment by highlighting a compiler error
7131
```ts
7232
export const initialState: State = {
7333
counter: 0,
@@ -76,69 +36,50 @@ export const initialState: State = {
7636
initialState.counter = 3; // Error, cannot be mutated
7737
```
7838

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.
39+
#### Caveat: Readonly does not provide a recursive immutability on objects
40+
This means that the `readonly` modifier doesn't propagate immutability on "properties" of objects. You'll need to set it explicitly on each nested property that you want.
8141

42+
Check the example below:
8243
```ts
8344
export type State = {
8445
readonly counterContainer: {
85-
readonly readonlyCounter: number,
46+
readonly immutableCounter: number,
8647
mutableCounter: number,
8748
}
8849
};
8950

9051
state.counterContainer = { mutableCounter: 1 }; // Error, cannot be mutated
91-
state.counterContainer.readonlyCounter = 1; // Error, cannot be mutated
52+
state.counterContainer.immutableCounter = 1; // Error, cannot be mutated
9253

9354
state.counterContainer.mutableCounter = 1; // No error, can be mutated
9455
```
9556

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.
57+
#### Best-practices for nested immutability
58+
> use `Readonly` or `ReadonlyArray` mapped types
9759
9860
```ts
9961
export type State = Readonly<{
100-
countersCollection: ReadonlyArray<Readonly<{
101-
readonlyCounter1: number,
102-
readonlyCounter2: number,
62+
counterPairs: ReadonlyArray<Readonly<{
63+
immutableCounter1: number,
64+
immutableCounter2: number,
10365
}>>,
10466
}>;
10567

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
68+
state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // Error, cannot be mutated
69+
state.counterPairs[0].immutableCounter1 = 1; // Error, cannot be mutated
70+
state.counterPairs[0].immutableCounter2 = 1; // Error, cannot be mutated
10971
```
11072

111-
> _There are some experiments in the community to make a `ReadonlyRecursive` mapped type, but I'll need to investigate if they really works_
73+
> _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_
11274
11375
[⇧ back to top](#table-of-contents)
11476

115-
### Examples
116-
117-
#### Reducer with classic `const types`
77+
### Finished reducer example using `getType` helper on action creators
11878

11979
::example='../../playground/src/redux/counters/reducer.ts'::
12080

12181
[⇧ back to top](#table-of-contents)
12282

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-
14283
---
14384

14485
## Store Configuration
@@ -166,36 +107,13 @@ When creating the store, use rootReducer. This will set-up a **strongly typed St
166107
167108
::example='../../playground/src/store.ts'::
168109

169-
[⇧ back to top](#table-of-contents)
170-
171110
---
172111

173112
## Async Flow
174113

175114
### "redux-observable"
176115

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-
```
116+
::example='../../playground/src/redux/toasts/epics.ts'::
199117

200118
[⇧ back to top](#table-of-contents)
201119

@@ -233,3 +151,43 @@ export const getFilteredTodos = createSelector(
233151
```
234152

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

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+
}

0 commit comments

Comments
 (0)