Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] How to get typesafe injection #256

Closed
noveyak opened this issue May 17, 2017 · 75 comments

Comments

Projects
None yet
@noveyak
Copy link

commented May 17, 2017

I've looked at
https://github.com/mobxjs/mobx-react#strongly-typing-inject

but am not sure if there is a better way. I generally pick out a lot of things to inject. Imagine

@inject((stores) => {
  return {
    storeA: stores.storeA,
    storeB: stores.storeB,
    storeC: stores.storeC,
    somethingOnStoreD: stores.storeD.something
})
MyComponent extends React.Component<IMyComponentProps>

But if I render this component from a parent component, then I can't render it without marking each one of the stores or props as optional in IMyComponentProps or pass in values for each of those props. This is documented but makes it very difficult to actually use the props in the component.

Basically anytime I use something from storeA, storeB, or somethingOnStoreD - I first have to check for the presence of those or else typescript will complain that it is potentially undefined.

I think part of the problem is typescript currently doesn't allow class mutations from decorators. But even when I use inject as a function, the tsd file specifies inject returns the original target which expects the props declared in IMyComponentProps
export function inject<T, P>(storesToProps : IStoresToProps<T, P>): (<TFunction extends IReactComponent<T | P>>(target: TFunction) => (TFunction & IWrappedComponent<T>)); // decorator

If I were to change it the tsd to

export function inject<T, P>(storesToProps : IStoresToProps<T, P>): (<TFunction extends IReactComponent<T | P>>(target: TFunction) => (IReactComponent<P> & IWrappedComponent<T>)); // decorator

and inject with

interface IPropsFromStore {
  storeA: storeA;
  storeB: storeB;
  ...
}
IPropsFromParent {
}
inject<IPropsFromStore, IPropsFromParent>((stores) => {
  return {
    storeA: stores.storeA,
    storeB: stores.storeB,
    storeC: stores.storeC,
    somethingOnStoreD: stores.storeD.something
})(
MyComponent extends React.Component<IPropsFromStore & IPropsFromParent>)

This allows me to use the function to return a component that no longer requires all the props that will be inserted with injection. It also will validate that the inject function is returning the props that I am expecting and also allows MyComponent to use props from injection directly and force checks on props from parent. The problem is this declaration will no longer allow inject to be used as a decorator since its no longer clear that inject returns a signature compatible with the original constructor. I kind of wish this functionality somehow existed because I could be willing to forego decorators to have a better type-checking experience but not sure if its possible.

Sorry this was a bit long, do you know of a better way to solve these problems?

Thanks!

@jamiewinder

This comment has been minimized.

Copy link
Member

commented May 18, 2017

Not sure if you're aware, but you can tell TypeScript that you know something is defined by using the following syntax (note the ! on the property access):

const { storeA } = props;
storeA!.something;

See here. Makes this a lot simpler. There is the same problem in a lot of a React libraries that inject props (including redux, react-dnd). It's certainly a TypeScript-specific issue.

@mweststrate

This comment has been minimized.

Copy link
Member

commented May 25, 2017

The current declaration of React components don't allow this behavior. The only way to properly fix it is to define React.Component with three generic params <PApi, PInside, S>, so that the PApi could define fields as being optional, while PInside is used inside the components, and can mark the same fields as non-optional.

E.g.: MyComponen extends React.Component<{ store?: StoreType }, { store: StoreType }, {}>. That is imho the only proper way to fix this; make a PR on the React component typings. Since in the latest TS versions Generics can have default arguments, this doesn't need to be as breaking as it would have been in the past, if it is possible to express: ReactComponent<P={}, S={}, PInner = P>.

Hope that makes sense. Not much can be done about it from mobx-react side of things.

@davezuko

This comment has been minimized.

Copy link

commented Jun 25, 2017

Not sure if it's of any help, but I just started picking up MobX and decided to go with a custom inject/observer wrapper to achieve this. The type definitions could arguably be a bit stronger, but for an initial pass it seems to work quite well:

export function connect<MappedProps> (
  mapStoreToProps: (store: Store) => MappedProps
) {
  return function<WrappedProps> (
    WrappedComponent: React.ComponentClass<WrappedProps> | React.StatelessComponent<WrappedProps>,
  ) {
    const ConnectedComponent = inject(mapStoreToProps)(
      observer(WrappedComponent)
    ) as React.ComponentClass<Partial<WrappedProps>>
    return ConnectedComponent
  }
}

// use as:
export default connect((store) => ({
  session: store.session,
}))(MyComponent)

With this, MyComponent does not have to treat the props as if they were optional, and any component rendering the connected (injected) MyComponent does not have to supply any of its props. Clearly what we'd really want is for the connected component to expose only the subset of props that weren't selected via inject, but my limited TypeScript skills have hindered me from accomplishing that thus far.

@DeDuckProject

This comment has been minimized.

Copy link

commented Jul 4, 2017

Any news about this?
@amir-arad and I tried a different implementation:

interface MyComponentInternalProps {
    myStore: MyStore;
}

export interface MyComponentExternalProps {
    myStore?: MyStore;
}

export interface MyComponentState {

}

@inject(STORE)
@observer
class MyComponentInternal extends React.Component<MyComponentInternalProps, MyComponentState> {

    render() {
        return (
            <div>
                Hi
            </div>
        );
    }
}

class JSXInterface extends React.Component<MyComponentExternalProps, MyComponentState>{}
export const MyComponent = MyComponentInternal as typeof JSXInterface;

It works, but its messy.

@beshanoe

This comment has been minimized.

Copy link

commented Jul 5, 2017

There is an article on this topic, yet another way to deal with this https://medium.com/@prashaantt/strongly-typing-injected-react-props-635a6828acaf
But for me the question is whether it is a good practice to use a bang! operator like
this.props.todoStore!.fetchData() or to use the approach from article like this.injected.todoStore.fetchData()
Which of these is more readable and maintainable, what do you think guys?

@amir-arad

This comment has been minimized.

Copy link

commented Jul 5, 2017

I'd add that the bang nullifies any existing type inference.
see: microsoft/TypeScript#16945

@mweststrate

This comment has been minimized.

Copy link
Member

commented Jul 6, 2017

I think the problem is basically that the React typings are too limited. If somebody would be willing to improve the react typings this could be easily addressed. Basically a component needs 3 generic arguments, instead of 2:

React.Component<PropsExternal, PropsInternal, State> where PropsInternal specifies which properties are available inside the component's methods. The PropsExternal would specificy the interface to the outside world.

In that case you could have a component with the following signature: class Component<{ todoStore?: TodoStore }, { todoStore: TodoStore }, {}> { ...}

Probably the inject signature could even be used to leverage Partials and construct the external properties from the internal ones. Nope, type inference starts with the fn, not the components

I am wondering btw whether

export function inject<T, P>(storesToProps : IStoresToProps<T, Partial<P>>): (<TFunction extends IReactComponent<T | P>>(target: TFunction) => (TFunction & IWrappedComponent<T>)); // decorator
Could help (adding partial to this def)

@amir-arad

This comment has been minimized.

Copy link

commented Jul 6, 2017

I think the root of the problem is in how they implemented JSX support. currently it just uses the props field's type for type checking.
we need a marker type like JSXAble<T>, so every class that implements it can be used as JSX with arguments of type T. However I don't know how it will play out with duck-typings. maybe using a hidden symbol field with type T.

anyhow, I have a different approach using this type:

type MyProps<P, T> = {props:Readonly<{ children?: ReactNode }> & Readonly<P>} & T;

so you can use it to cast this for the methods like so:

interface ExtProps{
    store?:{foo:boolean};
}
interface IntProps{
    store:{foo:boolean};
}
class Me extends React.Component<ExtProps>{
    private _:MyProps<IntProps, this> = this as any;
    componentDidUpdate(){
        this._.props.store.foo; // <-- this._.props is of type IntProps so store is not optional
    }
}

I don't like having runtime footprints for type workarounds, but I guess that's a choice.

@KatSick

This comment has been minimized.

Copy link

commented Jul 14, 2017

so there is no workaround for now ?

@mweststrate

This comment has been minimized.

Copy link
Member

commented Jul 14, 2017

@mattiamanzati

This comment has been minimized.

Copy link

commented Jul 14, 2017

Please everyone running TypeScript >= 2.4.1, try this out :)

import * as React from "react";
import { inject } from "mobx-react";
import { ObjectOmit } from "typelevel-ts"; // Thanks @gcanti, we <3 you all!

declare module "mobx-react" {
  export function inject<D>(
    mapStoreToProps: (store: any) => D
  ): <A extends D>(
    component: React.ComponentType<A>
  ) => React.SFC<ObjectOmit<A, keyof D> & Partial<D>>;
}

const MyComp = ({ label, count }: { label: string; count: number }) =>
  <p>
    {label} x {count} times!
  </p>;
const MyCompConnected = inject((store: any) => ({ count: 1 }))(MyComp);

export const el1 = <MyCompConnected label="hello" />;
export const el2 = <MyCompConnected labels="hello" />; // error! :D
export const el3 = <MyCompConnected label="Hello world!" count={2} />;

Thanks to @gcanti for his help and work who made this possible! ;)

@amir-arad

This comment has been minimized.

Copy link

commented Jul 16, 2017

@mattiamanzati I'll try it out first chance I get

@Bnaya

This comment has been minimized.

Copy link

commented Jul 25, 2017

I made this component:

import { inject, IReactComponent } from "mobx-react";

function mobxInject<newProps, oldProps extends newProps>(
  stores: string[],
  wrappedComponent: IReactComponent<oldProps>,
) {
  return inject.apply(null, stores)(wrappedComponent) as IReactComponent<newProps>;
}

export default mobxInject;

...
function CompInternal(props: {store:IStore; passedProps: string}) {
}

const Comp = mobxInject<{passedProps: string}, {store:IStore; passedProps: string}>(["store"], CompInternal);
@mweststrate

This comment has been minimized.

Copy link
Member

commented Sep 5, 2017

Also note that an @inject decorator for fields could quite nicely fit the typing problem, like done here: https://github.com/ascoders/dob-react

@kuuup-at-work

This comment has been minimized.

Copy link

commented Sep 12, 2017

Same problem with flowtype.

I create my stores as singletons and import them into my components without injecting.

@zhahaoyu

This comment has been minimized.

Copy link

commented Sep 18, 2017

Can we do something like in the react-redux type definition by explicitly defining the types of InjectedProps and OwnProps?

@zhahaoyu

This comment has been minimized.

Copy link

commented Sep 18, 2017

The following type definition makes more sense to me, but somehow this fails to be used as a decorator.
Reasons:

  1. The wrapped component should accept both the injected props T as well as props P passed by parent, therefore I changed | to &
  2. The new component generated from the HOC should only expose the props P, and T is no longer visible to the outside because they have been injected by the mapper function, so instead of using TFunction, we use TNewFunction
export function inject<T, P>(storesToProps: IStoresToProps<T, P>): <TFunction extends IReactComponent<T & P>, TNewFunction extends IReactComponent<P>>(
  target: TFunction
) => TNewFunction & IWrappedComponent<P>;

I made the following simple wrapper of inject which seems to do the work.

export function mobxInject<T, P>(storesToProps: IStoresToProps<T, P>): (
  <TFunction extends IReactComponent<T & P>, TNewFunction extends IReactComponent<P>>(
    target: TFunction
  ) => TNewFunction & IWrappedComponent<P>
) {
  return inject(storesToProps) as 
    <TFunction extends IReactComponent<T & P>, TNewFunction extends IReactComponent<P>>(
      target: TFunction
    ) => TNewFunction & IWrappedComponent<P>;
}
@RafalFilipek

This comment has been minimized.

Copy link
Contributor

commented Sep 26, 2017

@mattiamanzati any idea how to convert your definition to work as decorator?
Thank you!

@mattiamanzati

This comment has been minimized.

Copy link

commented Sep 26, 2017

@RafalFilipek In TypeScript decorators can't change class type definition, so that is impossible unfortunately :)

@tomitrescak

This comment has been minimized.

Copy link

commented Oct 5, 2017

Hi, I do not understand why you return:

export function inject<T, P>(
    storesToProps: IStoresToProps<T, P>
): (<TFunction extends IReactComponent<T | P>>(
    target: TFunction
) => TFunction & IWrappedComponent<P>) // WHY TFunction?

What makes more sense to me is:

export function inject<T, P>(
    storesToProps: IStoresToProps<T, P>
): (<TFunction extends IReactComponent<T | P>>(
    target: TFunction
) => IReactComponent<P>) // only the container props will be required !!

The problem is that if component has compulsory paramaters which are fulfiled by container, using your definition also the container will have those compulsory parameters which is incorrect.

The problem of my "fix" is that it does not work with decorator.

So, what is the reasoning for returning also the component parameters? And can I somehow fix that to work with decorators? Thanks

@geekflyer

This comment has been minimized.

Copy link

commented Oct 9, 2017

Based on @mattiamanzati's proposal and using a type from @gcanti's typelevel lib, I created a utility type that can be used to get pretty typesafe injection when using the inject(<storeName>)(MyComponent) variant.
Using this utility type, the type system will mark the injected property optional in the wrapped component, but also it will make sure that the component you're injecting to has a property which is type compatible with the injected store.

this is the utility type:

import { ObjectDiff } from 'typelevel-ts';

export type TypedInject<Stores> = <StoreKeyToInject extends keyof Stores>(
  ...storeKeysToInject: StoreKeyToInject[]
) => <ExpectedProps extends Pick<Stores, StoreKeyToInject>>(
  component: React.ComponentType<ExpectedProps>
) => React.ComponentType<ObjectDiff<ExpectedProps, Pick<Stores, StoreKeyToInject>>>;

a short example on how to use it would be like this:

const typedInject = inject as TypedInject<Stores>;
const InjectedComponent = typedInject('myStoreName')(MyComponent);

The long story of the idea is the following:

I'll just make the assumption that in most applications you'll only have a single <Provider ... /> at the entry point of your app.
Now let's suppose you're injecting 3 stores into the provider.
E.g.

index.tsx

class Store1 {...};
class Store2 {...};
class Store3 {...};

const stores = {
  store1: new Store1(),
  store2: new Store2(),
  store3: new Store3()
};

react.render(<Provider {...stores}> <App /> </Provider>);

We know at the design time the name and the shape of the stores that are getting injected, and we can extract the type in typescript via:

export type Stores = typeof stores; we can just add that type to the index.tsx and then import it from anywhere.

Now somewhere deep in your component hierarchy you want to inject one of those stores in a typesafe manner. You can do this in the following way

MyComponent.tsx

import React from 'react';
import { inject } from 'mobx-react';
import { Store1, Stores } from '../index.ts';


@observer
export class MyComponent extends Component<{ store1: Store1, store3: {incompatibleShape: string} }> {
  render() {
    <div>{this.props.store1.foo}></div>
  }
}

export default typedInject('store1')(MyComponent); // will work and make `store1` an optional prop on the wrapped component

export default typedInject('store2')(MyComponent); // will error because Component does not expect `store2` as prop
export default typedInject('store3')(MyComponent); // will error because store3 from `Stores` is not type compatible with the `store3` prop of the component
export default typedInject('store4')(MyComponent); // will error because store4 does not exist on `Stores`

Since I make the assumption that you're only having a single provider and a defined set of stores to inject, you actually can create the typedInject variable in a central file (like index.tsx) and just import it from anywhere without always casting the untyped inject in multiple places.

E.g. final result may look like:

index.tsx

import { TypedInject } from '../utils/types';

export class Store1 {...};
export class Store2 {...};
export class Store3 {...};

const stores = {
  store1: new Store1(),
  store2: new Store2(),
  store3: new Store3()
};

const typedInject = inject as TypedInject<typeof stores>;

react.render(<Provider {...stores}> <App /> </Provider>);

MyComponent.tsx

import React from 'react';
import { Store1, typedInject } from '../index.ts';

@observer
export class MyComponent extends Component<{ store1: Store1 }> {
  render() {
    <div>{this.props.store1.foo}></div>
  }
}

export default typedInject('store1')(MyComponent);

This way you're getting relatively typesafe injection E2E without too much boilerplate.

P.S.: In a real app you probably shouldn't put all the store definitions, types and typedInject into index.tsx but somewhere else (maybe your stores directory).

@ddbradshaw

This comment has been minimized.

Copy link

commented Oct 11, 2017

This gentleman suggests a workaround by extending the main props interface: https://medium.com/@prashaantt/strongly-typing-injected-react-props-635a6828acaf

interface MyComponentProps {
  name: string;
  countryCode?: string;
}

interface InjectedProps extends MyComponentProps {
  userStore: UserStore;
  router: InjectedRouter;
}
@inject("userStore")
@withRouter
@observer
class MyComponent extends React.Component<MyComponentProps, {}> {
  get injected() {
    return this.props as InjectedProps;
  }

  render() {
    const { name, countryCode } = this.props;
    const { userStore, router } = this.injected;
    ...
  }
}
@shadow-identity

This comment has been minimized.

Copy link

commented Oct 20, 2017

@geekflyer Thank you for this solution, but I've got
TypeError: Object(...) is not a function at export default typedInject('journal')(CalendarButton);
(full component here https://github.com/shadow-identity/therapy_journal/blob/e14f731a0a50b7b64efd1e6ab16e29b46181a7d9/src/CalendarButton.tsx)
What's wrong here?

@geekflyer

This comment has been minimized.

Copy link

commented Oct 20, 2017

@shadow-identity do you get this as runtime or typescript compiler error?

@shadow-identity

This comment has been minimized.

Copy link

commented Oct 22, 2017

@geekflyer at runtime.

@Obiwarn

This comment has been minimized.

Copy link

commented Oct 31, 2018

I read through the whole thread and I am a bit confused.
So the issue is closed but the problem not solved?
Is optional props and ! bang still the "official" workaround?
Also I don't understand @FredyC's answer (but it looks great).
Could someone help out?

@FredyC

This comment has been minimized.

Copy link
Contributor

commented Oct 31, 2018

@Obiwarn You haven't met React hooks yet? 😮 Well, then you have a lot to read :)

Here is the issue to figure out the future of this package #588

@Albert-Gao

This comment has been minimized.

Copy link

commented Nov 1, 2018

@FredyC Yes, by using the Hook, you can typing the injected store quite easily, but isn't it making the component not pure, now the component knows which store to use. And then every component could connect to any store easily with things like useStore or useInject.

But yeah, looks much more cleaner than the traditional 2 layers mapping

@FredyC

This comment has been minimized.

Copy link
Contributor

commented Nov 1, 2018

@Albert-Gao It's not any cleaner than using HOC around the component, imo. Obviously, if you want to eg. write tests for a purely UI component, you need to separate it and have a hook in a separate component that renders that UI component. However, personally, I think it's much easier to just wrap that component to Provider directly in tests.

I believe that what this topic was mostly about hiding injected props from the outside while being able to access it from the inside. That's exactly what hook is doing without messing with props typing.

@JoshMoreno

This comment has been minimized.

Copy link

commented Nov 20, 2018

@mattiamanzati and @onlyann - Do one of you happen to have an updated version of your custom inject typings? I can't get it working with typescript 3.1.6. Tried a handful of typelevel-ts versions too.

@FredyC

This comment has been minimized.

Copy link
Contributor

commented Nov 20, 2018

@JoshMoreno Have you seen my #256 (comment) ? I assume that you don't have a problem of using at least React 16.3 since you have latest TS, so the sweetness of Context is at your disposal and you can leave this craziness of typing inject properly behind.

@JoshMoreno

This comment has been minimized.

Copy link

commented Nov 20, 2018

I did and it looks very tempting. But from the docs...

They’re currently in React v16.7.0-alpha and being discussed in an open RFC

So they're not officially released and still being in alpha makes me a bit cautious. I suppose having a single typed wrapper function (like in your example) could make future breaking changes an easy fix though. Have you been using this without issue in the wild?

@FredyC

This comment has been minimized.

Copy link
Contributor

commented Nov 20, 2018

@JoshMoreno No, the Context is stable since 16.3, what are you citing is hooks and you can do just fine without those and still get pretty good typing capabilities.

@JoshMoreno

This comment has been minimized.

Copy link

commented Nov 20, 2018

@FredyC 🤔 You used useContext in your example. I don't see an alternate version that isn't using a new "React Hook". useContext is not part of the new context api of 16.3, but createContext is.

Am I missing something?

In React's source code, it has the following jsdoc tags for useContext

/**
 * @version experimental
 * @see https://reactjs.org/docs/hooks-reference.html#usecontext
 */
@FredyC

This comment has been minimized.

Copy link
Contributor

commented Nov 20, 2018

@JoshMoreno Ok, I see your confusion now. You can use context without hooks, although it's slightly less convenient. It kinda depends on your needs. Personally, I have a single store that has everything so typing is super simple. If you have different needs, you need to tailor it to those.

import { createContext } from 'react'

export const { Consumer } = createContext<TStores>(createStores())

// in some other file
export function MyComponent() {
  return <Consumer>{stores => {
    // do whatever you want here, stores would be fully typed to specified TStores
    return stores.friend.name
  }}</Consumer>
}

You can find everything you need in the React docs. I am sorry, but I don't want to spend time writing up something more elaborate without know what you need.

@onlyann

This comment has been minimized.

Copy link

commented Nov 21, 2018

@JoshMoreno

My version has become as below and is TypeScript 3.1.6 compliant:

declare module "mobx-react" {
  export function inject<D extends object>(
    mapStoreToProps: (store: IRootStore) => D
  ): <A extends D>(
    component: React.ComponentType<A> | React.SFC<A>
  ) => React.SFC<Omit<A, keyof D> & Partial<D>> & IWrappedComponent<A>;
}

Omit is defined as

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
@kylecorbelli

This comment has been minimized.

Copy link

commented Dec 25, 2018

We can get type safety on both the injected props and the "remainder" passed-in props by using the non-decorator syntax for inject and forcing a type assertion on the wrapped component.

It doesn't feel great but since using the non-null assertion operator (!) is also kind of a hack I figured I'd offer up an alternative hack. 😁

import { observable } from 'mobx'
import { inject, observer, Provider } from 'mobx-react'
import * as React from 'react'

class PersonStore {
    @observable public name: string = 'Rick Sanchez'
}

interface AllStores {
    personStore: PersonStore
}

interface InjectedProps {
    name: string
}

interface PassedProps {
    age: number
}

@observer
class ExamplePresentational extends React.Component<PassedProps & InjectedProps> {
    public render () {
        const { age, name } = this.props
        return (
            <div>
                <h3>Name: {name}</h3>
                <h4>Age: {age}</h4>
            </div>
        )
    }
}

const ExampleContainer = inject<AllStores, PassedProps, InjectedProps, {}>(({ personStore }: AllStores) => ({
    name: personStore.name,
}))(ExamplePresentational as unknown as React.ComponentClass<PassedProps>)

class Sandbox extends React.Component {
    public render () {
        return (
            <Provider personStore={new PersonStore()}>
                <ExampleContainer age={70} />
            </Provider>
        )
    }
}
@FredyC

This comment has been minimized.

Copy link
Contributor

commented Dec 25, 2018

@kylecorbelli Why would you be shooting yourself in the foot with such hassle when it's much easier to just use Context API? Unless you are with older React and cannot upgrade. In that case nevermind.

DIY inject if you have embraced React Hooks.

@JulianG

This comment has been minimized.

Copy link

commented Feb 2, 2019

I think there's a simpler way:
https://gist.github.com/JulianG/18af9b9ff582764a87639d61d4587da1#a-slightly-better-solution

interface InjectedProps {
  bananaStore: BananaStore; // 👍 no question mark here, and no interface inheritance
}

interface BananaProps {
  label: string;
}

@inject('bananaStore')
@observer
class BananaComponent extends Component<BananaProps> {

  get injected(): InjectedProps {
    return this.props as BananaProps & InjectedProps;
  }

  render() {
    const bananas = this.injected.bananaStore.bananas; // 👍 no exclamation mark here
    return <p>{this.props.label}:{bananas}</p>
  }

}
@JiiB

This comment has been minimized.

Copy link

commented Feb 5, 2019

This solution works great for me:

type AppProps = {
  myMobxStore?: IMyMobxStore; // injected
  title: string; // passed as <App title="my title">
};

@inject("myMobxStore")
@observer
class App extends Component<AppProps> {
  render() {
    const { myMobxStore, title }: AppProps = this.props;

    if (!myMobxStore) return null;

    const language = myMobxStore.language;
    return (
      <div>
        <h1>{title}</h1>
      </div>
    );
  }
}

You can savely return null because you can be sure that myMobxStore is always present as long as you inject it

@ivanslf

This comment has been minimized.

Copy link

commented Feb 25, 2019

I ended up using this approach, based on that heavily-mentioned workaround:

import { Component } from "react";
import { inject } from "mobx-react";
import { RootStore } from "../stores"; // App's root store as a singleton

export class ConnectedComponent<T, S, X = {}> extends Component<T, X> {
  public get stores() {
    return (this.props as any) as S;
  }
}

export const connect = (...args: Array<keyof RootStore>) => inject(...args);

Example:

import * as React from "react";
import { ConnectedComponent, connect } from "../utils";
import { UserStore } from "../stores";

interface IProps {
  onClose: () => void;
}

interface IStores {
  userStore: UserStore;
}

@connect("userStore") // Autocomplete works here ✔️ ❤️
export class UserProfile extends ConnectedComponent<IProps, IStores> {
  public render() {
    return (
      <UserDetails
        data={this.stores.userStore.user} // Also here ✔️
        onClose={this.props.onClose}      // and here ✔️ :)
      />
    )
  }
}

This way, stores and regular props are properly isolated. It is also possible to set native IState interface as the third parameter -- this is obviously discouraged, though.

@kylecorbelli

This comment has been minimized.

Copy link

commented Feb 25, 2019

Full disclosure, I've just been using a little hand-rolled render-prop component with the Context API:

import { Observer } from 'mobx-react'
import { Store } from '../store'
import { store } from '../store/initial-store'

const StoreContext = React.createContext(store)

interface Props {
  children(store: Store): JSX.Element | null
}

export const WithStore: React.FunctionComponent<Props> = ({ children }) =>
  <StoreContext.Consumer>
    {store => (
      <Observer>
        {() => children(store)}
      </Observer>
    )}
  </StoreContext.Consumer>

so that later I can write something along the lines of:

import { WithStore } from '../../contexts/WithStore'
import { AddBook as AddBookPresentational } from './AddBook'

export const AddBook: React.FunctionComponent = () =>
  <WithStore>
    {store => (
      <AddBookPresentational
        addBook={store.booksStore.addBook}
      />
    )}
  </WithStore>

100% type-safe, no type casting with as any, no non-null assertions with !, no null checks with if (this.props.store) {...}.

I'd go for a similar approach with hooks if your project supports at least v16.8.0.

@orzarchi

This comment has been minimized.

Copy link

commented Apr 3, 2019

Another HOC solution, based on the one posted above by @onlyann:

// Somewhere else: create a type with all of your stores. "bootstrap" here is a function that creates my stores.
export type StoresType = ReturnType<typeof bootstrap>;

export type Subtract<T, K> = Omit<T, keyof K>;

export const withStores = <TStoreProps extends keyof StoresType>(...stores: TStoreProps[]) =>
  <TProps extends Pick<StoresType, TStoreProps>>(component: React.ComponentType<TProps>) => {
    return (inject(...stores)(component) as any) as
      React.FC<Subtract<TProps, Pick<StoresType, TStoreProps>> &
      Partial<Pick<StoresType, TStoreProps>>> &
      IWrappedComponent<TProps>;
  };

Usage:

@observer
class SomeComponent extends Component<{customerStore: CustomerStore}> {
...
}

export default withStores('customerStore')(SomeComponent));

You get nice typings and IDE autocomplete, but still use the string version of inject to reduce verbosity.

@onlyann

This comment has been minimized.

Copy link

commented Apr 4, 2019

Another HOC solution, based on the one posted above by @onlyann:

// Somewhere else: create a type with all of your stores. "bootstrap" here is a function that creates my stores.
export type StoresType = ReturnType<typeof bootstrap>;

export type Subtract<T, K> = Omit<T, keyof K>;

export const withStores = <TStoreProps extends keyof StoresType>(...stores: TStoreProps[]) =>
  <TProps extends Pick<StoresType, TStoreProps>>(component: React.ComponentType<TProps>) => {
    return (inject(...stores)(component) as any) as
      React.FC<Subtract<TProps, Pick<StoresType, TStoreProps>> &
      Partial<Pick<StoresType, TStoreProps>>> &
      IWrappedComponent<TProps>;
  };

Usage:

@observer
class SomeComponent extends Component<{customerStore: CustomerStore}> {
...
}

export default withStores('customerStore')(SomeComponent));

You get nice typings and IDE autocomplete, but still use the string version of inject to reduce verbosity.

Nice.

Say that StoresType looks like:

{
  customerStore: CustomerStore;
  authStore: AuthStore
}

Does it identify that the following is incorrect:

export default withStores('authStore')(SomeComponent));
@orzarchi

This comment has been minimized.

Copy link

commented Apr 4, 2019

@onlyann Why is that example incorrect? You are requesting one of your stores, there shouldn't be any problem. If you would've misspelled 'authStore', typescript would've complained.

@onlyann

This comment has been minimized.

Copy link

commented Apr 4, 2019

In the example above, SomeComponent only has customerStore prop. From what I see (and I could be mistaken as I haven’t tried it), Typescript won’t complain but SomeComponent will likely fail at runtime given its customerStore prop in undefined.
It’d be great to be able to catch it early.

@orzarchi

This comment has been minimized.

Copy link

commented Apr 4, 2019

@onlyann oh, for some reason I didn't realize you were referring to my SomeComponent example.
So the answer is yes, that will be marked by typescript as incorrect.
edit: let's separate to two cases:

  1. Component expects customerStore as props, and authStore is injected: will be caught.
  2. Component expects customerStore and authStore as props, and only authStore is injected - will unfortunately not be caught.
@vkrol

This comment has been minimized.

Copy link
Contributor

commented May 27, 2019

We use custom mobx-react/inject typings based on react-redux/connect typings from @types/react-redux package. We do not have to use optional properties or custom inject wrappers. Everything works like a charm. The only significant downside: inject cannot be used as decorator, but it doesn't bother us.

You can see an example of these typings here: https://github.com/appulate/strict-mobx-react-inject-typings-example/blob/master/mobx-react.d.ts.
You can see an example of usage here: https://github.com/appulate/strict-mobx-react-inject-typings-example/blob/master/index.tsx.
Link to the repository that you can clone and play with: https://github.com/appulate/strict-mobx-react-inject-typings-example.

@dested

This comment has been minimized.

Copy link

commented Jun 9, 2019

Coming in late to the game but I was able to solve this and still use inject as a decorator with all the regular typings by simply splicing in some defaultProps on the components that inject. Example:

export type MainStoreProps = {mainStore: MainStore};
export const mainStoreDefaultProps = {mainStore: (null as unknown) as MainStore};

interface Props extends MainStoreProps{}

@inject(MainStoreName)
@observer
export class SomeComp extends Component<Props> {
  static defaultProps = mainStoreDefaultProps;
}

This allows me to have this.props.mainStore as not optional, but also doesn't require me to explicitly provide it when using SomeComp.

Hope this helps someone!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.