Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bdec1e5
Updated tests to fix build Closed #95 (#133)
piotrwitek Feb 3, 2019
857d9fd
Adds info import module decleration (#134)
arshadkazmi42 Feb 4, 2019
23ffca5
Updated Recipes section Closed #137
piotrwitek Feb 12, 2019
8f51601
Updated TOC
piotrwitek Feb 12, 2019
af7d940
Updated deps (#139)
piotrwitek Feb 12, 2019
c06d37a
Added example for using context in class component (#141)
binoy14 Feb 14, 2019
33d96b6
Testing epics (#144)
piotrwitek Feb 22, 2019
8c796de
updated deps
piotrwitek Mar 7, 2019
1259b21
added redux-thunk types overload
piotrwitek Mar 7, 2019
d0a05b2
Updated legacy patterns
piotrwitek Mar 7, 2019
5d03b97
ThunkActionType prototype
piotrwitek Mar 7, 2019
342a6ad
fix spelling mistake (#152)
SCKelemen Apr 6, 2019
0ecf245
Updated docs and and example of integration with redux-thunk Resolved…
piotrwitek Apr 6, 2019
c8d35fe
Merge branch 'redux-thunk'
piotrwitek Apr 6, 2019
b5cefb4
Added hyperlinks for better UX
piotrwitek Apr 6, 2019
34fc808
Small updates
piotrwitek Apr 13, 2019
e732b43
Updated playground project and added integration with react-redux-typ…
piotrwitek Apr 13, 2019
3f978be
Added new solution to Connect with `react-redux` to solve validation …
piotrwitek Apr 13, 2019
acd9b20
Added ESLint config section (#158)
piotrwitek Apr 14, 2019
93fc2b8
Refactored playground to use create-react-app (#159)
piotrwitek Apr 14, 2019
cf8a03c
Capitalize file to fix import on POSIX systems (#160)
lordi Apr 15, 2019
05ddc8f
Added prettier to common npm scripts
piotrwitek Apr 18, 2019
0ff885d
Added doctoc
piotrwitek Apr 21, 2019
01bc1df
Updated readme
piotrwitek Apr 21, 2019
0c2afc9
Updated readme with new section about createReducer API
piotrwitek Apr 22, 2019
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
Prev Previous commit
Next Next commit
Testing epics (piotrwitek#144)
* Added reference example for testing of epics

* updated hoc typings
  • Loading branch information
piotrwitek authored Feb 22, 2019
commit 33d96b6e7249ee3d0c1195685f3b1bfa4d397e23
252 changes: 159 additions & 93 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ Issues can be funded by anyone and the money will be transparently distributed t
- [Context](#context) 🌟 __NEW__
- [Hooks](#hooks) 🌟 __NEW__
- [Redux - Typing Patterns](#redux---typing-patterns)
- [Store Configuration](#store-configuration)
- [Action Creators](#action-creators)
- [Reducers](#reducers)
- [State with Type-level Immutability](#state-with-type-level-immutability)
- [Typing reducer](#typing-reducer)
- [Testing reducer](#testing-reducer)
- [Store Configuration](#store-configuration)
- [Async Flow](#async-flow)
- [Async Flow with `redux-observable`](#async-flow-with-redux-observable)
- [Typing Epics](#typing-epics)
- [Testing Epics](#testing-epics) 🌟 __NEW__
- [Selectors](#selectors)
- [Typing connect](#typing-connect)
- [Tools](#tools)
Expand Down Expand Up @@ -457,29 +459,28 @@ Adds state to a stateless counter
import * as React from 'react';
import { Subtract } from 'utility-types';

// These props will be subtracted from original component type
// These props will be subtracted from base component props
interface InjectedProps {
count: number;
onIncrement: () => any;
}

export const withState = <WrappedProps extends InjectedProps>(
WrappedComponent: React.ComponentType<WrappedProps>
export const withState = <BaseProps extends InjectedProps>(
BaseComponent: React.ComponentType<BaseProps>
) => {
// These props will be added to original component type
type HocProps = Subtract<WrappedProps, InjectedProps> & {
// here you can extend hoc props
type HocProps = Subtract<BaseProps, InjectedProps> & {
// here you can extend hoc with new props
initialCount?: number;
};
type HocState = {
readonly count: number;
};

return class WithState extends React.Component<HocProps, HocState> {
return class Hoc extends React.Component<HocProps, HocState> {
// Enhance component name for debugging and React-Dev-Tools
static displayName = `withState(${WrappedComponent.name})`;
static displayName = `withState(${BaseComponent.name})`;
// reference to original wrapped component
static readonly WrappedComponent = WrappedComponent;
static readonly WrappedComponent = BaseComponent;

readonly state: HocState = {
count: Number(this.props.initialCount) || 0,
Expand All @@ -490,14 +491,14 @@ export const withState = <WrappedProps extends InjectedProps>(
};

render() {
const { ...restProps } = this.props as {};
const { ...restProps } = this.props as any;
const { count } = this.state;

return (
<WrappedComponent
{...restProps}
<BaseComponent
count={count} // injected
onIncrement={this.handleIncrement} // injected
{...restProps}
/>
);
}
Expand Down Expand Up @@ -531,22 +532,26 @@ import { Subtract } from 'utility-types';

const MISSING_ERROR = 'Error was swallowed during propagation.';

// These props will be subtracted from base component props
interface InjectedProps {
onReset: () => any;
}

export const withErrorBoundary = <WrappedProps extends InjectedProps>(
WrappedComponent: React.ComponentType<WrappedProps>
export const withErrorBoundary = <BaseProps extends InjectedProps>(
BaseComponent: React.ComponentType<BaseProps>
) => {
type HocProps = Subtract<WrappedProps, InjectedProps> & {
// here you can extend hoc props
type HocProps = Subtract<BaseProps, InjectedProps> & {
// here you can extend hoc with new props
};
type HocState = {
readonly error: Error | null | undefined;
};

return class WithErrorBoundary extends React.Component<HocProps, HocState> {
static displayName = `withErrorBoundary(${WrappedComponent.name})`;
return class Hoc extends React.Component<HocProps, HocState> {
// Enhance component name for debugging and React-Dev-Tools
static displayName = `withErrorBoundary(${BaseComponent.name})`;
// reference to original wrapped component
static readonly WrappedComponent = BaseComponent;

readonly state: HocState = {
error: undefined,
Expand All @@ -558,7 +563,7 @@ export const withErrorBoundary = <WrappedProps extends InjectedProps>(
}

logErrorToCloud = (error: Error | null, info: object) => {
// TODO: send error report to cloud
// TODO: send error report to service provider
};

handleReset = () => {
Expand All @@ -573,9 +578,9 @@ export const withErrorBoundary = <WrappedProps extends InjectedProps>(

if (error) {
return (
<WrappedComponent
{...restProps}
<BaseComponent
onReset={this.handleReset} // injected
{...restProps}
/>
);
}
Expand Down Expand Up @@ -979,12 +984,78 @@ export default function ThemeToggleButton(props: Props) {

# Redux - Typing Patterns

## Store Configuration

### Create Global RootState and RootAction Types

#### `RootState` - type representing root state-tree
Can be imported in connected components to provide type-safety to Redux `connect` function

#### `RootAction` - type representing union type of all action objects
Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics

```tsx
declare module 'MyTypes' {
import { StateType, ActionType } from 'typesafe-actions';
export type Store = StateType<typeof import('./index').default>;
export type RootAction = ActionType<typeof import('./root-action').default>;
export type RootState = StateType<typeof import('./root-reducer').default>;
}

```

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

### Create Store

When creating a store instance we don't need to provide any additional types. It will set-up a **type-safe Store instance** using type inference.
> The resulting store instance methods like `getState` or `dispatch` will be type checked and will expose all type errors

```tsx
import { RootAction, RootState, Services } from 'MyTypes';
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';

import { composeEnhancers } from './utils';
import rootReducer from './root-reducer';
import rootEpic from './root-epic';
import services from '../services';

export const epicMiddleware = createEpicMiddleware<
RootAction,
RootAction,
RootState,
Services
>({
dependencies: services,
});

// configure middlewares
const middlewares = [epicMiddleware];
// compose enhancers
const enhancer = composeEnhancers(applyMiddleware(...middlewares));

// rehydrate state on app start
const initialState = {};

// create store
const store = createStore(rootReducer, initialState, enhancer);

epicMiddleware.run(rootEpic);

// export store singleton instance
export default store;

```

---

## Action Creators

> We'll be using a battle-tested library [![NPM Downloads](https://img.shields.io/npm/dm/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)
that automates and simplify maintenace of **type annotations in Redux Architectures** [`typesafe-actions`](https://github.com/piotrwitek/typesafe-actions#typesafe-actions)

### You should read [The Mighty Tutorial](https://github.com/piotrwitek/typesafe-actions#behold-the-mighty-tutorial) to learn it all the easy way!
### For more examples and in-depth tutorial you should check [The Mighty Tutorial](https://github.com/piotrwitek/typesafe-actions#behold-the-mighty-tutorial)!

A solution below is using a simple factory function to automate the creation of type-safe action creators. The goal is to decrease maintenance effort and reduce code repetition of type annotations for actions and creators. The result is completely typesafe action-creators and their actions.

Expand Down Expand Up @@ -1100,6 +1171,7 @@ state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated
[⇧ back to top](#table-of-contents)

### Typing reducer

> to understand following section make sure to learn about [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html), [Control flow analysis](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#control-flow-based-type-analysis) and [Tagged union types](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#tagged-union-types)

```tsx
Expand Down Expand Up @@ -1202,77 +1274,11 @@ describe('Todos Stories', () => {

---

## Store Configuration
## Async Flow with `redux-observable`

### Create Global RootState and RootAction Types
### For more examples and in-depth tutorial you should check [The Mighty Tutorial](https://github.com/piotrwitek/typesafe-actions#behold-the-mighty-tutorial)!

#### `RootState` - type representing root state-tree
Can be imported in connected components to provide type-safety to Redux `connect` function

#### `RootAction` - type representing union type of all action objects
Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics

```tsx
declare module 'MyTypes' {
import { StateType, ActionType } from 'typesafe-actions';
export type Store = StateType<typeof import('./index').default>;
export type RootAction = ActionType<typeof import('./root-action').default>;
export type RootState = StateType<typeof import('./root-reducer').default>;
}

```

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

### Create Store

When creating a store instance we don't need to provide any additional types. It will set-up a **type-safe Store instance** using type inference.
> The resulting store instance methods like `getState` or `dispatch` will be type checked and will expose all type errors

```tsx
import { RootAction, RootState, Services } from 'MyTypes';
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';

import { composeEnhancers } from './utils';
import rootReducer from './root-reducer';
import rootEpic from './root-epic';
import services from '../services';

export const epicMiddleware = createEpicMiddleware<
RootAction,
RootAction,
RootState,
Services
>({
dependencies: services,
});

// configure middlewares
const middlewares = [epicMiddleware];
// compose enhancers
const enhancer = composeEnhancers(applyMiddleware(...middlewares));

// rehydrate state on app start
const initialState = {};

// create store
const store = createStore(rootReducer, initialState, enhancer);

epicMiddleware.run(rootEpic);

// export store singleton instance
export default store;

```

---

## Async Flow

### "redux-observable"

### For more examples and in-depth explanation you should read [The Mighty Tutorial](https://github.com/piotrwitek/typesafe-actions#behold-the-mighty-tutorial) to learn it all the easy way!
### Typing epics

```tsx
import { RootAction, RootState, Services } from 'MyTypes';
Expand Down Expand Up @@ -1302,6 +1308,66 @@ export const logAddAction: Epic<RootAction, RootAction, RootState, Services> = (

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

### Testing epics

```tsx
import { StateObservable, ActionsObservable } from 'redux-observable';
import { RootState, Services, RootAction } from 'MyTypes';
import { Subject } from 'rxjs';

import { add } from './actions';
import { logAddAction } from './epics';

// Simple typesafe mock of all the services, you dont't need to mock anything else
// It is decoupled and reusable for all your tests, just put it in a separate file
const services = {
logger: {
log: jest.fn<Services['logger']['log']>(),
},
localStorage: {
loadState: jest.fn<Services['localStorage']['loadState']>(),
saveState: jest.fn<Services['localStorage']['saveState']>(),
},
};

describe('Todos Epics', () => {
let state$: StateObservable<RootState>;

beforeEach(() => {
state$ = new StateObservable<RootState>(
new Subject<RootState>(),
undefined as any
);
});

describe('logging todos actions', () => {
beforeEach(() => {
services.logger.log.mockClear();
});

it('should call the logger service when adding a new todo', done => {
const addTodoAction = add('new todo');
const action$ = ActionsObservable.of(addTodoAction);

logAddAction(action$, state$, services)
.toPromise()
.then((outputAction: RootAction) => {
expect(services.logger.log).toHaveBeenCalledTimes(1);
expect(services.logger.log).toHaveBeenCalledWith(
'action type must be equal: todos/ADD === todos/ADD'
);
// expect output undefined because we're using "ignoreElements" in epic
expect(outputAction).toEqual(undefined);
done();
});
});
});
});

```

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

---

## Selectors
Expand Down
Loading