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

Typescript, react-redux and asyncComponent usage? #62

Open
dawnmist opened this issue Oct 12, 2017 · 4 comments
Open

Typescript, react-redux and asyncComponent usage? #62

dawnmist opened this issue Oct 12, 2017 · 4 comments

Comments

@dawnmist
Copy link

dawnmist commented Oct 12, 2017

Hi,

I'm trying to use asyncComponent to load react-redux connected components for code splitting, but I'm having trouble with the asyncComponent Configuration's resolve typescript definitions. I'm not sure if I'm doing something wrong, or if there is an incompatibility between the return type of react-redux's connect and the resolve return type.

Very cut-down component setup example:
'./components/Page.tsx':

import * as React from 'react';

export interface PagePropsFromState {
  title: string;
}
export interface PagePropsFromDispatch {
  loadPage: (name: string, websocket: WebSocket) => void;
}
export interface PageOwnProps {
  websocket: WebSocket;
}
export type PageProps = PagePropsFromState & PagePropsFromDispatch & PageOwnProps;

interface PageState {
  counter: number;
}

export default class Page extends React.Component<PageProps, PageState> {
  constructor(props: PageProps) {
    super(props);
    this.state = {
      counter: 0
    };
  }

  onClick = () => {
    let counter = this.state.counter + 1;
    if (counter > 5) {
      this.props.loadPage('home', this.props.websocket);
      return;
    }
    this.setState({counter: counter});
  }

  render() : {
    return (
      <span onClick={this.onClick}>
        {this.props.title} clicks: {this.state.counter}
      </span>
    );
  }
}

'./containters/PageContainer.tsx:

import { bindActionCreator, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { loadPage } from '../actions';
import { State } from '../reducers';
import Page, { PageOwnProps, PagePropsFromDispatch, PagePropsFromState } from '../components/Page';

const mapStateToProps = (state: State) => ({
  title: getTitle(state)
});

const mapDispatchToProps = (dispatch: Dispatch<State>) => bindActionCreator(loadPage, dispatch);

const PageContainer = connect<PagePropsFromState, PagePropsFromDispatch, PageOwnProps>(
  mapStateToProps,
  mapDispatchToProps
)(Page);

export default PageContainer;

'./App.tsx':

import * as React from 'react';
import { Store } from 'redux';
import { asyncComponent, AsyncComponentProvider } from 'react-async-component';
import ReconnectingWebSocket from 'reconnectingwebsocket';
import { State } from './reducers';
import HomePage from './containers/HomeContainer';

const AsyncPage = asyncComponent({
  name: 'Page',
  resolve: () => import(/* webpackChunkName: "page" */ './containers/PageContainer')
});

export interface AppProps {
  currentPage: string;
}
interface AppState {}

export default class App extends React.Component<AppProps, AppState> {
  ws: ReconnectingWebSocket;

  constructor(props: AppProps) {
    super(props);
    this.ws = new ReconnectingWebSocket(`${process.env.REACT_APP_SOCKET}`, ['my_app'], {
        reconnectInterval: 10000,
        maxReconnectInterval: 10000,
        reconnectDecay: 1.5
      });
  }

  render() {
    return (
      <AsyncComponentProvider>
        { currentPage === 'home' &&
          <HomePage websocket={this.ws} />
        }
        { currentPage === 'page' &&
          <AsyncPage websocket={this.ws} />
        }
      </AsyncComponentProvider>
    );
  }
}

Hopefully I haven't made any critical typos in the example.

What I am getting is Typescript is rejecting the AsyncPage's resolve function.
Message is:

Types of property 'resolve' are incompatible.
    Type '() => Promise<typeof "./containers/PageContainer">' is not assignable to type '() => Promise<ComponentType<{}>>'.
      Type 'Promise<typeof "./containers/PageContainer">' is not assignable to type 'Promise<ComponentType<{}>>'.
        Type 'typeof "./containers/PageContainer"' is not assignable to type 'ComponentType<{}>'.
          Type 'typeof "./containers/PageContainer"' is not assignable to type 'StatelessComponent<{}>'.
            Type 'typeof "./containers/PageContainer"' provides no match for the signature '(props: { children?: ReactNode; }, context?: any): ReactElement<any> | null'.

What do I need to provide by the way of type information to asyncComponent/Configuration to be able to use this with a connected component?

@dawnmist
Copy link
Author

Got it finally. If I change the definition for resolve to return a React.ReactNode like this:

export interface Configuration<P> {
  resolve: () => Promise<React.ReactNode>;
  LoadingComponent?: (props: P) => JSX.Element;
  ErrorComponent?: (props: P & { error: Error }) => JSX.Element;
  name?: string;
  autoResolveES2015Default?: boolean;
  env?: 'node' | 'browser';
  serverMode?: 'resolve' | 'defer' | 'boundary';
}

and then pass the Own Props definition into the asyncComponent call:

const AsyncPage = asyncComponent<PageOwnProps>({
  name: 'Page',
  resolve: () => import(/* webpackChunkName: "page" */ './containers/PageContainer')
});

it starts working.

@bdoss
Copy link

bdoss commented Oct 13, 2017

I think this is due to the autoResolveES2015Default param of the asyncComponent definition. It defaults to true, so it's hiding that implementation detail from the TypeScript compiler. I've been manually resolving like this:

resolve: () => import('MyComp').then(x => x.default)

It keep the compiler happy so it doesn't think you're trying to do anything silly. The advantage with using ComponentType<T> being that when you import the async component elsewhere for use as a JSX element, the TypeScript compiler can check and give suggestions on props.

Edit: I gave another look over your mods to the type definition and see that with your change to the resolve method you would still gain the ability to get prop checks/suggestions (if you explicitly provide your props Type) when importing the async component, while sacrificing the ability to properly type check the resolve callback itself. This would allow interoperability with the autoResolveES2015Default as you've demonstrated, but albeit with a trade off on a more generic (and I'm not sure semantically correct) definition.

@dawnmist
Copy link
Author

Ah, thank you - that works. I'll close my pull request, and put in a different one to update the docs with a typescript usage example instead :)

@ctrlplusb
Copy link
Owner

@dawnmist - legend thank you for that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants