Skip to content

improved redux section text and examples #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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