Skip to content

Commit

Permalink
Merge branch 'refactor/iihoc'
Browse files Browse the repository at this point in the history
  • Loading branch information
benadamstyles committed Apr 3, 2018
2 parents c824191 + fd8d333 commit 290449d
Show file tree
Hide file tree
Showing 14 changed files with 628 additions and 337 deletions.
32 changes: 30 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
{
"presets": ["env", "react"],
"plugins": ["transform-flow-strip-types", "transform-class-properties"]
"presets": [
[
"env",
{
"targets": {
"browsers": [
"Chrome >= 36",
"Edge >= 12",
"Firefox >= 6",
"Explorer >= 11",
"Opera >= 23",
"Safari >= 8",
"Android >= 36",
"ChromeAndroid >= 36",
"FirefoxAndroid >= 6",
"ExplorerMobile >= 11",
"OperaMobile >= 23",
"iOS >= 8"
],
"node": "6"
}
}
],
"react"
],
"plugins": [
"transform-flow-strip-types",
"transform-class-properties",
"add-module-exports"
]
}
4 changes: 4 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
extends: mealsup

rules:
react-native/no-unused-styles: 0

overrides:
- files: "**/*.test.js"
rules:
Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ install:
- yarn install
- yarn check --integrity
script:
- yarn run test:all
- yarn test:all
126 changes: 89 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Set Future State
# setFutureState

[![npm version](https://badge.fury.io/js/set-future-state.svg)](https://www.npmjs.com/package/set-future-state)
[![Build Status](https://travis-ci.org/Leeds-eBooks/set-future-state.svg?branch=master)](https://travis-ci.org/Leeds-eBooks/set-future-state)
Expand All @@ -12,11 +12,7 @@ yarn add set-future-state

# The Problem

```
Warning: Can only update a mounted or mounting component.
This usually means you called setState, replaceState,
or forceUpdate on an unmounted component. This is a no-op.
```
> Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
In React, calling `this.setState()` in an `async` function, or in the `.then()` method of a `Promise`, is very common and very useful. But if your component is unmounted before your async/promise resolves, you’ll get the above error in your console. The React blog [suggests](https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html) using cancelable Promises, but as [Aldwin Vlasblom explains](https://medium.com/@avaq/broken-promises-2ae92780f33):

Expand All @@ -26,38 +22,94 @@ Enter [Futures](https://github.com/fluture-js/Fluture/wiki/Comparison-to-Promise

# The Solution

This library has a single default export: the function `withFutureState()`.

<details>
<summary><code>withFutureState()</code> type signature (in <a href="https://flow.org/">flow</a> notation)</summary>

```js
import {ComponentFutureState} from 'set-future-state'

class MyComponent extends ComponentFutureState {
state = {
loading: true,
fetchCount: 0,
data: null,
}

componentDidMount() {
this.setFutureState(
() => fetch('https://www.example.com'),
(data, prevState, props) => ({
data,
loading: false,
fetchCount: prevState.fetchCount + 1,
}),
error => console.error(error)
)
}

render() {
return this.state.loading ? (
<p>Loading . . .</p>
) : (
<p>{JSON.stringify(this.state.data)}</p>
)
}
}
type SetFutureState<P, S> = <E, V>(
self: Component<P, S>,
eventual: Future<E, V> | (() => Promise<V>),
reducer: (value?: V, prevState: S, props: P) => $Shape<S> | null,
onError?: (error: E) => *
) => void

declare export default function withFutureState<P, S>(
factory: (setFutureState: SetFutureState<P, S>) => Class<Component<P, S>>
): Class<Component<P, S>>
```

</details>

### Usage

`withFutureState()` is an [Inheritance Inversion Higher-Order Component](https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#5247). It takes a single argument, a factory function, which must return a React Class Component (i.e. a [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that inherits from `React.Component` or `React.PureComponent`). The factory function receives a single argument, `setFutureState`: your tool for safely updating your component's state in the future.

```js
import {Component} from 'react'
import withFutureState from 'set-future-state'

export default withFutureState(
setFutureState =>
class MyComponent extends Component {
state = {
loading: true,
fetchCount: 0,
data: null,
}

componentDidMount() {
setFutureState(
this,
() => fetch('https://www.example.com'),
(data, prevState, props) => ({
data,
loading: false,
fetchCount: prevState.fetchCount + 1,
}),
error => console.error(error)
)
}

render() {
return this.state.loading ? (
<p>Loading . . .</p>
) : (
<p>{JSON.stringify(this.state.data)}</p>
)
}
}
)
```

# API
`setFutureState()` takes the following 4 arguments:

* **`self`** (required)

Pass `this` as the first argument, so that `setFutureState()` can update your component's state.

* **`eventual`** (required)

The second argument should be either:

* a function that returns a `Promise`. When it resolves, the resolved value will be passed to the **`reducer`**.
* a [`Future`](https://github.com/fluture-js/Fluture).

* **`reducer`** (required)

The third argument should be a function that takes 3 arguments, and returns your updated state. It is called when your **`eventual`** resolves. It works _[exactly like the function form of `setState`](https://reactjs.org/docs/react-component.html#setstate)_: return a partial state object, and it will merge it into your existing state; return `null`, and it will do nothing. The arguments passed to **`reducer`** are:

* `value`: the resolved value from your **`eventual`** (`Promise` or `Future`)
* `prevState`: your component's existing state
* `props`: your component's props

* **`onError`** (optional)

The fourth and final argument is optional: a function that is called if the **`eventual`** (`Promise` or `Future`) rejects. It is called with the rejection reason (ideally an `Error` object).

**IMPORTANT:** If you leave out **`onError`**, your **`reducer`** will be called if the **`eventual`** resolves **AND** if it rejects. This is useful, for example, to remove loading spinners when an ajax call completes, whether or not it was successful.

# Browser Support

TODO...
`setFutureState` is transpiled with [Babel](https://babeljs.io/), to support all browsers that ship native `WeakMap` support. You can see a [list of compatible browser versions on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap#Browser_compatibility).
23 changes: 23 additions & 0 deletions flow-typed/set-future-state/fluture.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ declare class Future<E, V> {
map<M>(f: (value: V) => M): Future<E, M>;
mapRej<M>(f: (error: E) => M): Future<M, V>;

chain<M, F: Future<E, M>>(f: (value: V) => F): F;

fork(onReject: (error: E) => *, onResolve: (value: V) => void): Cancel;
done(callback: Nodeback<E, V>): Cancel;
}
Expand All @@ -20,6 +22,27 @@ declare module 'fluture' {
n: (callback: Nodeback<E, V>) => void
): Future<E, V>

declare export function encaseP<A, E, V>(
p: (a: A) => Promise<V>,
a: A
): Future<E, V>
declare export function encaseP<A, E, V>(
p: (a: A) => Promise<V>
): (a: A) => Future<E, V>

declare export function encaseP2<A, B, E, V>(
p: (a: A, b: B) => Promise<V>,
a: A,
b: B
): Future<E, V>
declare export function encaseP2<A, B, E, V>(
p: (a: A, b: B) => Promise<V>,
a: A
): (b: B) => Future<E, V>
declare export function encaseP2<A, B, E, V>(
p: (a: A, b: B) => Promise<V>
): (a: A) => (b: B) => Future<E, V>

declare export function encaseN<A, E, V>(
n: (a: A, callback: Nodeback<E, V>) => void,
a: A
Expand Down
16 changes: 11 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{
"name": "set-future-state",
"version": "0.1.3",
"version": "0.2.0-rc.9",
"description": "Safely setState in the future",
"main": "lib/set-future-state.js",
"module": "lib/set-future-state.esm.js",
"esnext": "lib/set-future-state.esm.js",
"react-native": "lib/set-future-state.esm.js",
"files": [
"lib"
],
Expand All @@ -12,15 +15,15 @@
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"runkitExample": "const {ComponentFutureState} = require('set-future-state')",
"runkitExample": "const withFutureState = require('set-future-state')",
"scripts": {
"lint": "eslint .",
"pretest:lib": "yarn build",
"test:lib": "jest --silent --config=\"{\\\"moduleNameMapper\\\":{\\\"^./set-future-state$\\\":\\\"<rootDir>/lib/set-future-state\\\"}}\"",
"test": "jest --silent --coverage",
"posttest": "jest-coverage-ratchet",
"test:all": "yarn lint && yarn run flow check && yarn test && yarn run test:lib",
"build": "babel src --out-dir lib --ignore src/set-future-state.test.js,src/flow-tests.js && cp src/set-future-state.js.flow lib",
"build": "babel src --out-dir lib --ignore src/set-future-state.test.js,src/set-future-state-unsupported.test.js,src/flow-tests.js && cp src/set-future-state.js.flow lib && flow-remove-types --pretty src/set-future-state.js > lib/set-future-state.esm.js",
"prepare": "yarn build",
"release": "release-it"
},
Expand All @@ -47,6 +50,7 @@
"babel-cli": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-jest": "^22.4.3",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-flow-strip-types": "^6.22.0",
"babel-preset-env": "^1.6.1",
Expand All @@ -62,6 +66,7 @@
"eslint-plugin-react": "^7.7.0",
"eslint-plugin-react-native": "^3.2.1",
"flow-bin": "^0.69.0",
"flow-remove-types": "^1.2.3",
"flow-typed": "^2.4.0",
"jest": "^22.4.3",
"jest-coverage-ratchet": "^0.2.3",
Expand All @@ -87,12 +92,13 @@
"lines": 100,
"statements": 100,
"functions": 100,
"branches": 91.67
"branches": 85
}
}
},
"dependencies": {
"fluture": "^8.0.2"
"fluture": "^8.0.2",
"recompose": "^0.26.0"
},
"peerDependencies": {
"react": "^16.0.0"
Expand Down
3 changes: 3 additions & 0 deletions src/__snapshots__/set-future-state-unsupported.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`When WeakMap is not available throws 1`] = `"\`setFutureState\` requires WeakMap to be available. Consider including a polyfill such as core-js."`;
8 changes: 4 additions & 4 deletions src/__snapshots__/set-future-state.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`PureComponentFutureState setFutureState cancelling raw setState triggers the React warning 1`] = `
exports[`setFutureState cancelling raw setState triggers the React warning 1`] = `
"Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
Please check the code for the TestWithResetWarnings-1 component."
`;

exports[`PureComponentFutureState setFutureState cancelling raw setState triggers the React warning with async 1`] = `
exports[`setFutureState cancelling raw setState triggers the React warning with async 1`] = `
"Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
Please check the code for the TestWithResetWarnings-2 component."
`;

exports[`PureComponentFutureState setFutureState throws for an incorrect first argument 1`] = `
exports[`setFutureState throws for an incorrect first argument 1`] = `
"The first argument to setFutureState() must be a Future or a Function which returns a Promise.
Please check the code for the Test component."
Please check the code for the WithFutureState(TestBase) component."
`;
62 changes: 62 additions & 0 deletions src/flow-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// @flow

import {Component} from 'react'
import withFutureState from './set-future-state'

type Props = {}

type State = {
num?: number,
str?: string,
loading: boolean,
}

export default withFutureState(
setFutureState =>
class MyComponent extends Component<Props, State> {
state = {
loading: false,
}

method() {
// $FlowExpectError
setFutureState()

// $FlowExpectError
setFutureState(() => Promise.resolve(1), () => ({}))

// $FlowExpectError
setFutureState({}, () => Promise.resolve(1), () => ({}))

setFutureState(
this,
() => Promise.resolve(1),
// $FlowExpectError
() => ({other: 'string'})
)

setFutureState(
this,
() => Promise.resolve(1),
// $FlowExpectError
(num: string) => ({num, loading: true})
)

setFutureState(
this,
() => Promise.resolve(1),
num => ({num, loading: true})
)

setFutureState(
this,
() => Promise.resolve(''),
str => ({str, loading: true})
)
}

render() {
return 'test'
}
}
)
9 changes: 9 additions & 0 deletions src/set-future-state-unsupported.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @flow

describe('When WeakMap is not available', () => {
it('throws', () => {
// eslint-disable-next-line no-global-assign, no-native-reassign
WeakMap = void 0
expect(() => require('./set-future-state')).toThrowErrorMatchingSnapshot()
})
})
Loading

0 comments on commit 290449d

Please sign in to comment.