Skip to content

Commit

Permalink
feat(tools): add resilientProp decorator
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `promisedProp` and `promised` do not keep the last
resolved value on promise update. Use `resilientProp` and `resilient`
to enable this feature.
  • Loading branch information
davidbonnet committed Feb 4, 2019
1 parent e603d23 commit 489abc8
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 12 deletions.
51 changes: 40 additions & 11 deletions src/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,55 +456,60 @@ export function lazyProperty(object, propertyName, valueBuilder) {

export function promisedProp(name) {
/*
Takes the promise from the prop `[name]` and injects prop `[name]` with `{ done, error, value }`.
Replaces the promise at prop `[name]` with `{ done, error, value }`.
Before the promise resolves, `done` is `false`, and becomes `true` afterwards.
If an error occured in the promise, `error` is set to it. Otherwise, the `value` is set to the resolved value.
If a new promise is provided to `[name]`, the previously resolved `value` is kept until the new one resolves.
If the propmise at prop `[name]` changes, `done`, `error`, and `value` are reset and any previous promise is discarded.
*/
return Component =>
class promised extends BaseComponent {
constructor(props) {
super(props)
const promise = props[name]
this.state = this.constructor.getDerivedStateFromProps(props)
this.state = this.constructor.getDerivedStateFromProps(
props,
EMPTY_OBJECT,
)
this.mounted = false
this.attachPromise(promise)
}

attachPromise(promise) {
if (promise == null) {
return
}
return Promise.resolve(promise).then(
value => {
if (!this.mounted || this.state.promise !== promise) {
return
}
this.setState({ resource: { done: true, error: null, value } })
this.setState({ result: { done: true, error: null, value } })
},
error => {
if (!this.mounted || this.state.promise !== promise) {
return
}
this.setState({
resource: { done: true, error, value: this.state.resource.value },
result: { done: true, error, value: null },
})
},
)
}

componentDidMount() {
this.mounted = true
this.attachPromise(this.state.promise)
}

static getDerivedStateFromProps(props, state) {
const promise = props[name]
if (state && state.promise === promise) {
if (promise === state.promise && state !== EMPTY_OBJECT) {
return null
}
return {
promise,
resource: {
result: {
done: false,
error: null,
value: state ? state.resource.value : null,
value: null,
},
}
}
Expand All @@ -523,7 +528,31 @@ export function promisedProp(name) {
render() {
return $(Component, {
...this.props,
[name]: this.state.resource,
[name]: this.state.result,
})
}
}
}

export function resilientProp(name) {
/*
Keeps the last non-`nil` value of prop `[name]`.
*/
return Component =>
class resiliant extends BaseComponent {
constructor(props) {
super(props)
this.state = { [name]: props[name] }
}

static getDerivedStateFromProps(props, state) {
const value = props[name]
return value === state.value || value == null ? null : { value }
}
render() {
return $(Component, {
...this.props,
[name]: this.state.value,
})
}
}
Expand Down
23 changes: 22 additions & 1 deletion src/values.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
editableProp,
cycledProp,
promisedProp,
resilientProp,
EMPTY_OBJECT,
} from './tools'

export const defaultValue = Component =>
Expand All @@ -34,8 +36,16 @@ export const transformable = compose(
hasProp('transformValue'),
Component =>
class transformable extends BaseComponent {
constructor(props) {
super(props)
this.state = this.constructor.getDerivedStateFromProps(
props,
EMPTY_OBJECT,
)
}

static getDerivedStateFromProps({ value, transformValue }, state) {
return state && value === state.value
return value === state.value && state !== EMPTY_OBJECT
? null
: {
transformedValue: transformValue(value, state),
Expand Down Expand Up @@ -142,8 +152,19 @@ export const cyclable = branch(
}),
)

/*
Replaces the promise at prop `value` with `{ done, error, value }`.
Before the promise resolves, `done` is `false`, and becomes `true` afterwards.
If an error occured in the promise, `error` is set to it. Otherwise, the `value` is set to the resolved value.
If the promise at prop `value` changes, `done`, `error`, and `value` are reset and any previous promise is discarded.
*/
export const promised = promisedProp('value')

/*
Keeps the last non-`nil` value of prop `value`.
*/
export const resilient = resilientProp('value')

export const toggledEditing = branch(
/*
Sets the `editing` prop and enables its toggling through the `onToggleEditing()` prop.
Expand Down

0 comments on commit 489abc8

Please sign in to comment.