Skip to content

Instantly share code, notes, and snippets.

@vonovak
Last active June 15, 2019 23:47
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vonovak/29c972c6aa9efbb7d63a6853d021fba9 to your computer and use it in GitHub Desktop.
Save vonovak/29c972c6aa9efbb7d63a6853d021fba9 to your computer and use it in GitHub Desktop.
using flowtype with @Inject from 'mobx-react'
import type File from "File";
import type FileService from "FileService";
type FileDetailProps = {
file: File
};
class FileDetail extends React.Component<FileDetailProps> {
render() {
// remove the file prop and flow will complain!
return <FileDownload file={this.props.file} />;
}
}
type InjectedProps = {
fileService: FileService
};
// for older flow versions
function typedInject<Props>(
WrappedComponent: React.ComponentType<InjectedProps & Props>
): React.ComponentType<Props> {
return inject("fileService")(WrappedComponent);
}
// for flow version > 60 (or so)
function typedInject<Props: {}>(
WrappedComponent: React.ComponentType<Props>
): React.ComponentType<$Diff<Props, InjectedProps>> {
return inject("fileService")(WrappedComponent);
}
// NOTE you can spread types from InjectedProps to reuse them
type FileDownloadProps = {
file: File,
...$Exact<InjectedProps>
};
//NOTE using
//@typedInject
//class FileDownload extends ... will NOT work - why? (maybe related to `esproposal.decorators=ignore` in .flowconfig)
const FileDownload = typedInject(
class FileDownload extends React.Component<FileDownloadProps> {
download = () => {
// flow will happily access both file and fileService props!
this.props.fileService.downloadFile(this.props.file);
// flow will complain: [flow] property `foo` (Property not found in object type)
this.props.foo.bar();
};
render() {
return <Button onPress={this.download}>download file!</Button>;
}
}
);
@kuuup-at-work
Copy link

kuuup-at-work commented Dec 5, 2017

You could also try this:

import * as React from 'react';
import { inject, observer } from 'mobx-react';
import { Store } from '../state/Store';

type Props {
    store: Store,
}

@inject('store')
@observer
export class Root extends React.Component<Props> {

    static defaultProps = {
        store: new Store()
    };

    render() {
        return (
            <div>
                <div>{ this.props.store.id }</div>
                <button onClick={ this.props.store.incId }>inc</button>
            </div>
        );
    }
}

@linonetwo
Copy link

If put esproposal.decorators=ignore into flowconfig, then flow type will ignore all decorator. I think that's the cause.

@kuuup-at-work
Copy link

The cause for what?

@Gvozd
Copy link

Gvozd commented Feb 2, 2018

Maybe something like this?

// based on https://github.com/facebook/flow/blob/v0.64.0/lib/react.js#L86
declare class InjectedComponent<Props, InjectedProps, State = void>
  extends Component<Props, State> {
  props: Props & InjectedProps;
  state: State;
}
class FileDownload extends InjectedComponent<FileDownloadProps, InjectedProps> 
}

@Gvozd
Copy link

Gvozd commented Feb 2, 2018

Yes, this worked nice for me
Plus: It's good have one re-usable generic class, instead custom typedInject for every case
Minus: hack with $FlowFixMe

/* @flow */
/* eslint-disable */
import React, {Component, Fragment} from 'react';
import {Provider, inject} from 'mobx-react';


// some hacked generic type
// $FlowFixMe - TODO how define props without "Property `props` is incompatible: InjectedProps. This type is incompatible with Props" ?!
class InjectedComponent<Props, InjectedProps, State = void> extends Component<Props, State> {
    props: Props & InjectedProps;
    state: State;
}

export default class FileDetail extends Component<{
    ...$Exact<FileDetailProps>,
    ...$Exact<InjectedProps>
}> {
    render() {
        return (
            <Provider fileService={this.props.fileService}>
                <Fragment>
                    {/* okay */}
                    <FileDownload file={this.props.file} />
                    <FileDownload file={{name: 'foo'}}/>

                    {/* error... */}
                    <FileDownload file={{fileName: 'foo'}}/>
                    <FileDownload />
                </Fragment>
            </Provider>
        );
    }
}

@inject('fileService')
class FileDownload extends InjectedComponent<FileDetailProps, InjectedProps> {
    download = () => {
        // okay
        this.props.fileService.downloadFile(this.props.file);

        // error...
        this.props.foo.bar();
    };

    render() {
        return <button onClick={this.download}>download file!</button>;
    }
}

// Model types
type File = {
    name: string
};
type FileService = {
    downloadFile: (File) => void
};
// Props types
type FileDetailProps = {
    file: File
};
type InjectedProps = {
    fileService: FileService
};

@coremessage
Copy link

@Gvozd just do

class InjectedComponent<Props, InjectedProps, State = void> extends Component<Props & InjectedProps, State> {

@dgcoffman
Copy link

dgcoffman commented Apr 13, 2018

Here's what I've done.

I import all our stores in the libdef file.

type Stores = {
  user: UserStore,
  ...,
};

declare module 'mobx-react' {
  declare function Provider(props: { children: React.Node }): React.Node;
  declare function observer<T: React$ComponentType<*>>(component: T): T;

  declare function inject<Props>(
    ...storeNames: $Keys<Stores>[] // verify that strings are names of stores
  ): (
    component: React$ComponentType<Props>,
  ) => React$ComponentType<$Diff<Props, Stores>>;

  // We can't know at design time the type of properties on the object returned
  // by mapperFunction, flow can't require specific props
  declare function inject<Props>(
    mapperFunction: Function,
  ): (component: React$ComponentType<Props>) => React$ComponentType<*>;

  declare function onError((Error) => mixed): void;
}

@mmazzarolo
Copy link

mmazzarolo commented Apr 26, 2018

I'm using the following approach in some of my apps:
schermata 2018-04-24 alle 09 49 50

I'm pretty sure your solutions are way smarter than this but I'm I wanted to share my finding, maybe someone else is interested.

P.S.: the Stores type is defined this way:

/* @flow */
import CoreStore from "../stores/Core";
import SessionStore from "../stores/Session";
import ConfigurationStore from "../stores/Configuration";

export type Stores = {
  core: CoreStore,
  session: SessionStore,
  configuration: ConfigurationStore
};

@rhyst
Copy link

rhyst commented Jul 4, 2018

@Gvozd 's method works for me. It is a positive not to have to write extra code in every component.
@coremessage the modification you suggested results in the original flow error about missing injected props again for me. Having a flow ignore comment doesn't seem to bad though.

@alexandersorokin
Copy link

alexandersorokin commented Aug 31, 2018

I prefer explicit way of typing inject. I described it here: facebook/flow#6777 (comment)

The proposed way has several advantages:

  1. It supports covariant (readonly) props.
  2. It supports optional props and defaultProps.
  3. You are not required to mark optional any injected props.
  4. It allows you to inject subtypes instead of props' type;
  5. It allows you to inject only small part of store, not entire store.
  6. It has exhaustive type checks. So you can not inject any props that are not defined in component props. You should pass to component all mandatory props that are not injected, not marked optional and not default. You can not pass prop to component that has been already injected. Also you can not pass any props that is not defined in component props.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment