Skip to content

Instantly share code, notes, and snippets.

@killtheliterate
Created September 14, 2018 03:33
Show Gist options
  • Save killtheliterate/fa5d9c37d69b64c8a51073c6bf8441d1 to your computer and use it in GitHub Desktop.
Save killtheliterate/fa5d9c37d69b64c8a51073c6bf8441d1 to your computer and use it in GitHub Desktop.
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
type Subtract<T, K> = Omit<T, keyof K>
export type EffOptions = {
ErrorComponent?: React.ComponentType<any>
LoadingComponent?: React.ComponentType<any>
log?: (...args: any[]) => void
}
export type EffProps = {
effect: () => Promise<any>
shouldEffectKey: string
}
export type EffState = {
isErred: boolean
isResolved: boolean
prevKey?: string
effResult?: any
}
export type EffInjectedProps = {
effResult?: any // We can't know
}
export function withEffect ({ ErrorComponent, LoadingComponent, log }: EffOptions = {}) {
return function <P> (CC: React.ComponentType<P & EffInjectedProps>) {
const IsEff: React.SFC<Partial<P>> = (props) => LoadingComponent
? (<LoadingComponent {...props} />)
: (<Loader />)
const IsErr: React.SFC<Partial<P>> = (props) => ErrorComponent
? (<ErrorComponent {...props} />)
: (<ErrorMessage />)
return class Eff extends React.Component<Subtract<P, EffInjectedProps> & EffProps, EffState> {
static displayName = `withEffect(${getDisplayName(CC)})`
// @see: https://github.com/Microsoft/TypeScript/pull/13288
// @see: https://github.com/Microsoft/TypeScript/issues/10727
//
// Using the {a, b, ...restProps} form below doesn't enforce that props
// are passed unless the component is called as <jsx />. Since this will
// often be composed with things like `connect`, that's no good. These
// seems "good enough" until the aforementioned PR lands.
static propTypes = {
effect: PropTypes.func.isRequired,
shouldEffectKey: PropTypes.string.isRequired
}
_resolvingKey: string | void = undefined
state = {
effResult: undefined,
isErred: false,
isResolved: false,
prevKey: undefined
}
constructor (props: P & EffProps) {
super(props)
this._effect = this._effect.bind(this)
}
static getDerivedStateFromProps (nextProps: P & EffProps, prevState: EffState) {
if (nextProps.shouldEffectKey !== prevState.prevKey) {
return {
isErred: false,
isResolved: false,
prevKey: nextProps.shouldEffectKey
}
}
return null
}
componentDidMount () {
this._effect(this.props.shouldEffectKey)
}
componentWillUnmount () {
this._resolvingKey = undefined
}
componentDidUpdate () {
if (this.state.isResolved === false) {
this._effect(this.props.shouldEffectKey)
}
}
render () {
const { effect, shouldEffectKey, ...restProps } = this.props as EffProps
if (this.state.isErred) {
return <IsErr {...restProps} />
}
if (!this.state.isResolved) {
return <IsEff {...restProps} />
}
return <CC { ...restProps } effResult={this.state.effResult} />
}
// ---------------------------------------------------------------------
_effect (key: EffProps['shouldEffectKey']) {
if (key === this._resolvingKey) {
return // already resolving
}
this._resolvingKey = key
this.props.effect()
.then(
(result) => {
if (key === this._resolvingKey) {
this.setState(() => ({
effResult: result,
isResolved: true
}))
}
},
(err: Error) => {
if (key === this._resolvingKey) {
if (log) {
log(err)
}
this.setState(() => ({ isErred: true, isResolved: true }))
}
}
)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment