Skip to content

Commit

Permalink
improved redux section text and examples (#36)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
piotrwitek authored Dec 28, 2017
1 parent 3d66461 commit e234bd5
Show file tree
Hide file tree
Showing 38 changed files with 517 additions and 592 deletions.
366 changes: 179 additions & 187 deletions README.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions docs/markdown/1_react.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ Adds error handling using componentDidCatch to any component
## Redux Connected Components
### Caveat with `bindActionCreators`
**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.**
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.
> 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).
> 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:
```
const mapDispatchToProps = (dispatch: Dispatch) => ({
onIncrement: () => dispatch(actions.increment()),
});
```
#### - redux connected counter
::example='../../playground/src/connected/sfc-counter-connected.tsx'::
Expand Down
179 changes: 68 additions & 111 deletions docs/markdown/2_redux.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,30 @@

## Action Creators

### KISS Style
This pattern is focused on a KISS principle - to stay clear of complex proprietary abstractions and follow simple and familiar JavaScript const based types:
> Using Typesafe Action Creators helpers for Redux [`typesafe-actions`](https://github.com/piotrwitek/typesafe-actions)
Advantages:
- simple "const" based types
- familiar to standard JS usage

Disadvantages:
- significant amount of boilerplate and duplication
- necessary to export both action types and action creators to re-use in other places, e.g. `redux-saga` or `redux-observable`
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.
> 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`).
All that without losing type-safety! Please check this very short [Tutorial](https://github.com/piotrwitek/typesafe-actions#tutorial)

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

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

### DRY Style
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".

Advantages:
- using factory function to automate creation of type-safe action creators
- less boilerplate and code repetition than KISS Style
- getType helper to obtain action creator type (this makes using "type constants" unnecessary)

```ts
import { createAction, getType } from 'react-redux-typescript';

// Action Creators
export const actionCreators = {
incrementCounter: createAction('INCREMENT_COUNTER'),
showNotification: createAction('SHOW_NOTIFICATION',
(message: string, severity: Severity = 'default') => ({
type: 'SHOW_NOTIFICATION', payload: { message, severity },
})
),
};

// Usage
store.dispatch(actionCreators.incrementCounter(4)); // Error: Expected 0 arguments, but got 1.
store.dispatch(actionCreators.incrementCounter()); // OK: { type: "INCREMENT_COUNTER" }
getType(actionCreators.incrementCounter) === "INCREMENT_COUNTER" // true

store.dispatch(actionCreators.showNotification()); // Error: Supplied parameters do not match any signature of call target.
store.dispatch(actionCreators.showNotification('Hello!')); // OK: { type: "SHOW_NOTIFICATION", payload: { message: 'Hello!', severity: 'default' } }
store.dispatch(actionCreators.showNotification('Hello!', 'info')); // OK: { type: "SHOW_NOTIFICATION", payload: { message: 'Hello!', severity: 'info' } }
getType(actionCreators.showNotification) === "SHOW_NOTIFICATION" // true
```

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

---

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

### Tutorial
Declare reducer `State` type definition with readonly modifier for `type level` immutability
### State with Type-level Immutability
Declare reducer `State` type with `readonly` modifier to get "type level" immutability
```ts
export type State = {
readonly counter: number,
};
```

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

#### Caveat: Readonly does not provide recursive immutability on objects
> 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.
#### Caveat: Readonly does not provide a recursive immutability on objects
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.

Check the example below:
```ts
export type State = {
readonly counterContainer: {
readonly readonlyCounter: number,
mutableCounter: number,
readonly containerObject: {
readonly immutableProp: number,
mutableProp: number,
}
};

state.counterContainer = { mutableCounter: 1 }; // Error, cannot be mutated
state.counterContainer.readonlyCounter = 1; // Error, cannot be mutated
state.containerObject = { mutableProp: 1 }; // Error, cannot be mutated
state.containerObject.immutableProp = 1; // Error, cannot be mutated

state.counterContainer.mutableCounter = 1; // No error, can be mutated
state.containerObject.mutableProp = 1; // OK! No error, can be mutated
```

> 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.
#### Best-practices for nested immutability
> use `Readonly` or `ReadonlyArray` [Mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
```ts
export type State = Readonly<{
countersCollection: ReadonlyArray<Readonly<{
readonlyCounter1: number,
readonlyCounter2: number,
counterPairs: ReadonlyArray<Readonly<{
immutableCounter1: number,
immutableCounter2: number,
}>>,
}>;

state.countersCollection[0] = { readonlyCounter1: 1, readonlyCounter2: 1 }; // Error, cannot be mutated
state.countersCollection[0].readonlyCounter1 = 1; // Error, cannot be mutated
state.countersCollection[0].readonlyCounter2 = 1; // Error, cannot be mutated
state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // Error, cannot be mutated
state.counterPairs[0].immutableCounter1 = 1; // Error, cannot be mutated
state.counterPairs[0].immutableCounter2 = 1; // Error, cannot be mutated
```

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

### Examples

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

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

#### Reducer with getType helper from `react-redux-typescript`
```ts
import { getType } from 'react-redux-typescript';

export const reducer: Reducer<State> = (state = 0, action: RootAction) => {
switch (action.type) {
case getType(actionCreators.increment):
return state + 1;

case getType(actionCreators.decrement):
return state - 1;

default: return state;
}
};
```

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

---

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

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

---

## Async Flow

### "redux-observable"

```ts
// import rxjs operators somewhere...
import { combineEpics, Epic } from 'redux-observable';

import { RootAction, RootState } from '@src/redux';
import { saveState } from '@src/services/local-storage-service';

const SAVING_DELAY = 1000;

// persist state in local storage every 1s
const saveStateInLocalStorage: Epic<RootAction, RootState> = (action$, store) => action$
.debounceTime(SAVING_DELAY)
.do((action: RootAction) => {
// handle side-effects
saveState(store.getState());
})
.ignoreElements();

export const epics = combineEpics(
saveStateInLocalStorage,
);
```
::example='../../playground/src/redux/toasts/epics.ts'::

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

Expand Down Expand Up @@ -233,3 +150,43 @@ export const getFilteredTodos = createSelector(
```

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

---

### Action Creators - Alternative Pattern
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:

Advantages:
- familiar to standard JS "const" based approach

Disadvantages:
- significant amount of boilerplate and duplication
- more complex compared to `createAction` helper library
- necessary to export both action types and action creators to re-use in other places, e.g. `redux-saga` or `redux-observable`

```tsx
export const INCREMENT = 'INCREMENT';
export const ADD = 'ADD';

export type Actions = {
INCREMENT: {
type: typeof INCREMENT,
},
ADD: {
type: typeof ADD,
payload: number,
},
};

export const actions = {
increment: (): Actions[typeof INCREMENT] => ({
type: INCREMENT,
}),
add: (amount: number): Actions[typeof ADD] => ({
type: ADD,
payload: amount,
}),
};
```

[⇧ back to top](#table-of-contents)
2 changes: 1 addition & 1 deletion docs/markdown/4_extras.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"strict": true,
"pretty": true,
"removeComments": true,
"sourceMap": true
Expand Down
14 changes: 8 additions & 6 deletions docs/markdown/_toc.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
- [Higher-Order Components](#higher-order-components) 📝 __UPDATED__
- [Redux Connected Components](#redux-connected-components)
- [Redux](#redux)
- [Action Creators](#action-creators)
- [Reducers](#reducers)
- [Store Configuration](#store-configuration)
- [Async Flow](#async-flow) _("redux-observable")_
- [Selectors](#selectors) _("reselect")_
- [Action Creators](#action-creators) 📝 __UPDATED__
- [Reducers](#reducers) 📝 __UPDATED__
- [State with Type-level Immutability](#state-with-type-level-immutability)
- [Reducer Example](#reducer-example)
- [Store Configuration](#store-configuration) 📝 __UPDATED__
- [Async Flow](#async-flow) 📝 __UPDATED__
- [Selectors](#selectors)
- [Tools](#tools)
- [Living Style Guide](#living-style-guide) _("react-styleguidist")_ 🌟 __NEW__
- [Living Style Guide](#living-style-guide) 🌟 __NEW__
- [Extras](#extras)
- [tsconfig.json](#tsconfigjson)
- [tslint.json](#tslintjson)
Expand Down
3 changes: 3 additions & 0 deletions playground/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
3 changes: 2 additions & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
"redux": "3.7.2",
"redux-observable": "0.17.0",
"reselect": "3.0.1",
"rxjs": "5.5.5",
"rxjs": "5.5.6",
"tslib": "1.8.1",
"typesafe-actions": "1.0.0-rc.1",
"uuid": "3.1.0"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions playground/src/api/models.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface ITodoModel {
id: string,
text: string,
completed: false,
id: string;
text: string;
completed: false;
}
2 changes: 1 addition & 1 deletion playground/src/api/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const resolveWithDelay = <T>(value: T, time: number = 1000) => new Promise(
(resolve) => setTimeout(() => resolve(value), time),
(resolve) => setTimeout(() => resolve(value), time)
);

export const rangeQueryString = (count: number, pageNumber?: number) =>
Expand Down
4 changes: 2 additions & 2 deletions playground/src/components/generic-list.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';

export interface GenericListProps<T> {
items: T[],
itemRenderer: (item: T) => JSX.Element,
items: T[];
itemRenderer: (item: T) => JSX.Element;
}

export class GenericList<T> extends React.Component<GenericListProps<T>, {}> {
Expand Down
6 changes: 3 additions & 3 deletions playground/src/components/sfc-counter.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react';

export interface SFCCounterProps {
label: string,
count: number,
onIncrement: () => any,
label: string;
count: number;
onIncrement: () => any;
}

export const SFCCounter: React.SFC<SFCCounterProps> = (props) => {
Expand Down
4 changes: 2 additions & 2 deletions playground/src/components/sfc-spread-attributes.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';

export interface SFCSpreadAttributesProps {
className?: string,
style?: React.CSSProperties,
className?: string;
style?: React.CSSProperties;
}

export const SFCSpreadAttributes: React.SFC<SFCSpreadAttributesProps> = (props) => {
Expand Down
Loading

0 comments on commit e234bd5

Please sign in to comment.