Open mulholo opened 5 years ago
Currently decorators are not supported by Flow and 'mobx-react' is not well-typed.
To generate typings for 'mobx-react' I use following inject function:
import * as React from 'react';
import {inject as untypedInject, Provider as UntypedProvider} from 'mobx-react';
import type {Store1, Store2} from './stores';
type Stores = $ReadOnly<{
store1: Store1,
store2: Store2,
}>;
type MakeMixed = <-V>(V) => mixed;
export function inject<
TProps: {},
TWrappedComponentType: React.ComponentType<TProps>,
TInjectedProps: $Shape<
& {...TProps} // Strip covariance modifiers ($Shape doesn't support it)
& TProps // Improve error messages about injecting invalid props for $Shape
>,
>(propsSelector: (Stores) => TInjectedProps): (TWrappedComponentType) =>
React.ComponentType<
$Diff<
$Exact<
React.ElementConfig<TWrappedComponentType> // Support defaultProps
>,
$ObjMap<
$Exact<TInjectedProps>,
MakeMixed // Support injecting subtypes
>
>
> {
return wrappedComponentType => untypedInject(propsSelector)(wrappedComponentType);
}
export const Provider: React.ComponentType<Stores> = UntypedProvider;
To use it:
import {Provider} from './typedInject';
import type {Store1, Store2} from './stores';
const store1: Store1 = ...; const store2: STore2 = ...; const stores = {store1, store2};
ReactDOM.render( <Provider {...stores}>
, document.getElementById('root'));
2. Import 'inject' function/decorator from module above instead of importing it from 'mobx-react'. Currently you can not use 'inject' function as decorator if you inject any mandatory props. You can use it as decorator only if you inject default props and optional props.
import * as React from 'react'; import {observer} from 'mobx-react'; import type {Store1, Store2} from './stores'; import {inject} from './typedInject';
type Props = { +store1: Store1, +default: string, +mandatory: string, +optional: string | void, };
@observer
class Component extends React.Component
// You can inject 'store1' from Mobx Stores to 'store1' in Props. // You can inject any declared props, including optional and default ones. So you can inject 'default' prop. // You can inject any subtype instead of required type. So you can inject store or variable with literal type 'message' to 'optional' prop. // You can not inject any extra props. Flow will complain about it. So you can not inject anything to 'extra' prop. const InjectedComponent = inject(({store1}) => ({store1})) (Component);
// To use it as decorator for your Component you can use syntax @inject(({store2}) => ({optional: store2}))
export default InjectedComponent;
3. Just use it:
Thanks for this @alexandersorokin ! It's been extremely helpful. Can you take a look at my typedInject that takes in store names as arguments? It doesn't seem to be throwing any errors when it should be. Thanks!
export function inject<
TProps: {},
TWrappedComponentType: React.ComponentType<TProps>,
TInjectedProps: $Shape<
{ ...TProps } & TProps, // Strip covariance modifiers ($Shape doesn't support it) // Improve error messages about injecting invalid props for $Shape
>,
>(
...storeNames: $Keys<Stores>[] // takes in strings that are names of stores as arguments
): TWrappedComponentType => React.ComponentType<
$Diff<
$Exact<
React.ElementConfig<TWrappedComponentType>, // Support defaultProps
>,
$ObjMap<
$Exact<TInjectedProps>,
MakeMixed, // Support injecting subtypes
>,
>,
> {
return wrappedComponentType =>
untypedInject(...storeNames)(wrappedComponentType);
}
@melissafzhang, it does not throwing any errors because storeNames
argument does not have any connection to TInjectedProps
.
To make it works you should do some sort of black magic. Let's do it. Firstly, you need create several files.
createObjectFromArray.js
with content below. It's helper function for Pick
.
// @flow
const createObjectFromArray = (keys: $ReadOnlyArray<*>) => keys.reduce((object, key) => { object[key] = undefined; return object; }, {});
export default createObjectFromArray;
You should do not merge/move content of this file with/to other files. It will broke everything.
2. Create `Pick.js` with content below. This utility type allows select some field of object and filter other.
// @flow
import createObjectFromArray from './createObjectFromArray';
export type Pick<TSource: $Subtype<{}>, Keys: $ReadOnlyArray<$Keys
Thanks @alexandersorokin! How do you recommend typing the observer
function from mobx-react
?
We frequently have this pattern: export default inject('user')(observer(Component));
However, this causes the inject
typing to break.
Thanks again!
Here's my first pass at it
// @flow
import { observer as untypedObserver } from 'mobx-react';
type observer = <T: React.ComponentType<*>>(component: T) => T;
const typedObserver: observer = component => {
return untypedObserver(component);
};
export default typedObserver;
@melissafzhang why do you need typing of the observer
?
observer
does not change type of wrapped component. So you can use it as decorator freely.
Also you can try something like this:
declare module "mobx-react" {
import type {ComponentType} from "react";
declare module.exports: {
observer<T: ComponentType<*>>(component: T) => T;
}
}
Or you can even modify proposed above TypedInject with:
export default class TypedInject<TStores: {}> {
Provider: React.ComponentType<$ReadOnly<TStores>> = Provider;
inject:
& MapStoresToProps<TStores>
& SelectStores<TStores>
= (...selector) => component => inject(...selector)(observer(component));
}
Thanks again @alexandersorokin! We want to allow passing in unknown props since we use the withRouter
hoc from react-router
which injects multiple props which might not be used. I removed the $Exact
typings but still getting the error that the other injected props are undefined.
Cannot call withRouter because:
• undefined property history [1] is incompatible with RouterHistory [2].
• undefined property match [1] is incompatible with Match [3].
Also I'm not sure if this is the intended behavior below. Here's a simplified example
type Props = {|
myStore: MyStoreType
|}
export const InjectedComponent: React.ComponentType<Props> = inject(
'myStore',
)(Component);
This throws the flow error;
Cannot assign `inject(...)(...)` to `InjectedComponent` because property `myStore` is missing in `Props` [1] but exists in `Props` [2] in type argument `P` [3]
This is a good workaround reference: https://gist.github.com/vonovak/29c972c6aa9efbb7d63a6853d021fba9
Does the Flow team plan to add decorator support anytime soon?
I have been using MobX's provider which allows you to pass a store to a component without passing it down through each layer of props. Usually, therefore, it is possible to have a component which has no props passed in by its parent, yet has props passed in by
@inject
.However, Flow counts this (perfectly fine) behaviour as an error, as shown here:
Would it be possible to get the two libraries to play nicely together?
(Also, I suspect, but am not sure, that this might also affect React's context functionality.)
An example component using
@inject
, for reference: