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 1 commit
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
Next Next commit
improved redux section text and examples
updated tslint to strict for 2.6 and future new strict features
  • Loading branch information
piotrwitek committed Dec 28, 2017
commit 5837dc63b94a8dbf2407de7824e2f7017c3ad030
78 changes: 77 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
### Goals
- 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
- Minimize amount of manually writing type declarations by leveraging [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html)
- 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
- 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

### Playground Project
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.
Expand Down Expand Up @@ -618,6 +618,82 @@ export default () => (

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

## Higher-Order Components
- function that takes a component and returns a new component
- a new component will infer Props interface from wrapped Component extended with Props of HOC
- will filter out props specific to HOC, and the rest will be passed through to wrapped component

### Basic HOC Examples

#### - withState
> enhance stateless counter with state

```tsx
import * as React from 'react';
import { Omit } from 'react-redux-typescript';

interface RequiredProps {
count: number,
onIncrement: () => any,
}

type Props<T extends RequiredProps> = Omit<T, keyof RequiredProps>;

interface State {
count: number,
}

export function withState<WrappedComponentProps extends RequiredProps>(
WrappedComponent: React.ComponentType<WrappedComponentProps>,
) {
const HOC = class extends React.Component<Props<WrappedComponentProps>, State> {

state: State = {
count: 0,
};

handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};

render() {
const { handleIncrement } = this;
const { count } = this.state;

return (
<WrappedComponent
count={count}
onIncrement={handleIncrement}
/>
);
}
};

return HOC;
}

```

<details><summary>SHOW USAGE</summary><p>

```tsx
import * as React from 'react';

import { withState } from '@src/hoc';
import { SFCCounter } from '@src/components';

const SFCCounterWithState =
withState(SFCCounter);

export default (
({ children }) => (
<SFCCounterWithState label={'SFCCounterWithState'} />
)
) as React.SFC<{}>;

```
</p></details>

---

# Redux
Expand Down
13 changes: 13 additions & 0 deletions docs/markdown/1_react_redux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### bindActionCreators caveat
**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.**

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.

> 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)

> 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:
```
const mapDispatchToProps = (dispatch: Dispatch) => ({
onIncrement: () => dispatch(actions.increment()),
});
```
166 changes: 62 additions & 104 deletions docs/markdown/2_redux.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,32 @@

## 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 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 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.
> 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)

::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 for "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 a compiler error
```ts
export const initialState: State = {
counter: 0,
Expand All @@ -76,69 +36,50 @@ 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 on "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,
readonly immutableCounter: number,
mutableCounter: number,
}
};

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

state.counterContainer.mutableCounter = 1; // 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

```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`
### Finished reducer example using `getType` helper on action creators

::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 +107,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 +151,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
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"
}
Loading