Skip to content

Instantly share code, notes, and snippets.

@shuhei
Last active March 12, 2017 08:08
Show Gist options
  • Save shuhei/23194bf79d2655577d4772c1a28239bf to your computer and use it in GitHub Desktop.
Save shuhei/23194bf79d2655577d4772c1a28239bf to your computer and use it in GitHub Desktop.
WIP: Typing HOCs with recompose

WIP: Typing HOCs with recompose

It's hard to keep prop types of underlying components of HOCs. Even the official documentation is not working well. It's even harder to have multiple HOCs typed and maintain with peer developers. Here comes recompose, the HOC utility module. If we have recompose properly typed, we can omit typing of our own HOCs taking advantage of recompose and flowtype's type inference.

recompose still doesn't have official flowtype definition, but there are some efforts:

This gist is an effort to make it happen based on the PRs above.

Goals

  • Keep types of underlying component's props
  • Allow underlying component to ignore additional props by HOC
  • No additional type annotations on top of recompose

Implementation details

  • In a union type, a specific type should come before more general types.
    • NG: T | Fn1<A, B>
    • OK: Fn1<A, B> | T
  • HOCs that provide additional props may return HOC<$Shape<A & B>, B>. In this way, the underlying component can ignore some unnecessary intermediate props.
    • For example:
      const withOpen = withValue(false, (open, setOpen) => ({ open, setOpen }));
      const withToggle = withProps(({ open, setOpen }) => ({ toggle: () => setOpen(!open) }));
      
      // `Something` doesn't need `setOpen`!
      function Something({ open, toggle }) { /* ... */ }
      const enhancedSomething = compose(withOpen, withToggle)(Something);
  • suppress_comment suppresses also next next line in addition to next line. So put a new line after the suppression target line.
    • For example:

      // .flowconfig
      [options]
      suppress_comment=^ $ExpectError$
      // or
      suppress_comment=\\(.\\|\n\\)*\\$ExpectError
      
      // $ExpectError
      'foo'.bar();
      'foo'.bar(); // This line's error is also suppressed!
      
      // $ExpectError
      'foo'.bar();
      
      'foo'.bar(); // This line's error is not suppressed!

Reference

declare module 'recompose' {
declare type FunctionComponent<A> = (props: A) => ?React$Element<any>;
declare type ClassComponent<D, A, S> = Class<React$Component<D, A, S>>;
declare type Component<A> = FunctionComponent<A> | ClassComponent<any, A, any>;
declare type Fn1<A, B> = (a: A) => B;
declare type HOC<A, B> = Fn1<Component<A>, Component<B>>;
declare type SCU<A> = (props: A, nextProps: A) => boolean;
declare function id<A>(a: A): A;
declare function compose<A, B, C, D, E>(de: Fn1<D, E>, cd: Fn1<C, D>, bc: Fn1<B, C>, ab: Fn1<A, B>): Fn1<A, E>;
declare function compose<A, B, C, D>(cd: Fn1<C, D>, bc: Fn1<B, C>, ab: Fn1<A, B>): Fn1<A, D>;
declare function compose<A, B, C>(bc: Fn1<B, C>, ab: Fn1<A, B>): Fn1<A, C>;
declare function compose<A, B>(ab: Fn1<A, B>): Fn1<A, B>;
declare function compose<A>(): id<A>;
declare function mapProps<A, B>(
propsMapper: (ownerProps: B) => A
): HOC<A, B>;
declare function withProps<A, B>(
createProps: Fn1<B, A> | A
): HOC<$Shape<A & B>, B>;
declare function withPropsOnChange<A, B>(
shouldMapOrKeys: Array<$Keys<B>> | SCU<B>,
createProps: (ownerProps: B) => A
): HOC<A, B>;
declare function withHandlers<B, A: { [key: string]: (props: B) => Function }>(
handlerCreators: A
): HOC<$Shape<A & B>, B>
declare function defaultProps<A, D: $Shape<A>, B: $Diff<A, D>>(
props: D
): HOC<A, B>;
declare function renameProp<A, B>(
oldName: $Keys<A>,
newName: $Keys<B>
): HOC<A, B>;
declare function renameProps<A, B>(
nameMap: { [key: $Keys<A>]: $Keys<B> }
): HOC<A, B>;
declare function flattenProp<A, B>(
propName: $Keys<B>
): HOC<A, B>;
declare function withState<A, B, T>(
stateName: string,
stateUpdaterName: string,
initialState: T | (props: B) => T
): HOC<A, B>;
// FIXME: The following doesn't work because `B & { [SN]: T } & { [SUN]: (_: T) => void }`
// means an object with keys that are keys of B *and* SN *and* SUN, which doesn't make sense.
// declare function withState<A, B, T, SN: string, SUN: string>(
// stateName: SN,
// stateUpdaterName: SUN,
// initialState: T & (props: B) => T,
// ): HOC<{ [SN]: T } & { [SUN]: (_: T) => void } & B, B>;
declare function withReducer<A, B, Action, State>(
stateName: string,
dispatchName: string,
reducer: (state: State, action: Action) => State,
initialState: State
): HOC<A, B>;
declare function branch<A, B>(
test: (ownerProps: B) => boolean,
left: HOC<A, B>,
right: HOC<A, B>
): HOC<A, B>;
declare function renderComponent<A>(C: Component<A> | string): HOC<A, A>;
declare function renderNothing<A>(C: Component<A>): HOC<A, {}>;
declare function shouldUpdate<A>(
test: SCU<A>
): HOC<A, A>;
declare function pure<A>(C: Component<A>): FunctionComponent<A>;
declare function onlyUpdateForKeys<A>(propKeys: Array<$Keys<A>>): HOC<A, A>;
declare function withContext<A, B>(
childContextTypes: Object,
getChildContext: (props: Object) => Object
): HOC<A, B>;
declare function getContext<A, B, C: Object>(
contextTypes: C
): HOC<A & { [ key: $Keys<C> ]: any }, B>;
declare function lifecycle<A>(
spec: Object,
): HOC<A, A>;
declare function toClass<A>(): HOC<A, A>;
declare function setStatic<A>(
key: string,
value: any
): HOC<A, A>;
declare function setDisplayName<A>(
displayName: string
): HOC<A, A>;
declare function getDisplayName<A>(C: Component<A>): string;
declare function wrapDisplayName<A>(C: Component<A>, wrapperName: string): string;
declare function shallowEqual(a: Object, b: Object): boolean;
declare function isClassComponent(value: any): boolean;
declare type ReactNode = React$Element<any> | Array<React$Element<any>>;
declare function createEagerElement<A>(
type: Component<A> | string,
props: ?A,
children?: ?ReactNode
): React$Element<any>;
declare function createEagerFactory<A>(
type: Component<A> | string,
): (
props: ?A,
children?: ?ReactNode
) => React$Element<any>;
declare function createSink<A>(callback: (props: A) => void): Component<A>;
declare function componentFromProp<A>(propName: string): Component<A>;
declare function nest<A>(
...Components: Array<Component<any> | string>
): Component<A>
declare function hoistStatics<A, B, H: HOC<A, B>>(hoc: H): H;
}
import { Component } from 'react'
import createHelper from 'recompose/createHelper'
import { createEagerFactory } from 'recompose'
// https://github.com/acdlite/recompose/pull/241#issuecomment-269765711
const withValueImpl = (initialState, mapStateProps) => BaseComponent => {
const factory = createEagerFactory(BaseComponent)
return class extends Component {
state = {
stateValue: typeof initialState === 'function'
? initialState(this.props)
: initialState
};
updateStateValue = (updateFn, callback) => (
this.setState(({ stateValue }) => ({
stateValue: typeof updateFn === 'function'
? updateFn(stateValue)
: updateFn
}), callback)
);
render() {
return factory({
...this.props,
...mapStateProps(this.state.stateValue, this.updateStateValue),
});
}
}
};
const withValue = createHelper(withValueImpl, 'withValue');
export default withValue;
// @flow
import type { Fn1, HOC } from 'recompose';
declare export function withValue<A, B, T>(
initialState: Fn1<B, T> | T,
mapStateProps: (state: T, setState: Fn1<T, void>) => A
): HOC<A & B, B>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment