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

type annotation for higher order react component #6559

Closed
bhavinkamani opened this issue Jan 21, 2016 · 10 comments
Closed

type annotation for higher order react component #6559

bhavinkamani opened this issue Jan 21, 2016 · 10 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@bhavinkamani
Copy link

I am trying to figure out type annotation for higher order react components. Here's my code

class A extends React.Component<any, any> {
  render() {
    return <div>Hello World</div>
  }
}

const HigherOrderComponent = (component:any) => {
  return class extends component {
    render() {
      return super.render();
    }
  };
};

export default HigherOrderComponent(A);

typescript compiler throws following error
Error:(23, 24) TS2507: Type 'any' is not a constructor function type

I tried couple of annotation such as <any, any>, <T extends React.Component<any, any>>. None of them is passing through compilation.

@frankwallis
Copy link
Contributor

See #5887 for an example of this. There is currently an issue with the approach outlined there but it is fixed in 1.8.0 nightly.

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Jan 21, 2016
@RyanCavanaugh
Copy link
Member

What you want is this:

const HigherOrderComponent = (component: typeof React.Component) => {
  return class extends component<any, any> {
    render() {
      return super.render();
    }
  };
};

@bhavinkamani
Copy link
Author

This worked and is very readable. Thanks Ryan.

@flushentitypacket
Copy link

@RyanCavanaugh Doesn't that lose typing on the returned class?

@kitsonk
Copy link
Contributor

kitsonk commented Jul 5, 2017

No...

class Component<T, U> {
  render() { }
}
const React = {
  Component
}

const HigherOrderComponent = (component: typeof React.Component) => {
  return class extends component<any, any> {
    render() {
      return super.render();
    }

    foo = 'bar';
  };
};

const NewClass = HigherOrderComponent(React.Component);

const p = new NewClass();

p.foo; // string

@flushentitypacket
Copy link

flushentitypacket commented Jul 5, 2017

@kitsonk I should have been more specific--doesn't it lose React Component props typing? (No one is instantiating React Components via new)

I think this is an issue for folks using JSX

@kitsonk
Copy link
Contributor

kitsonk commented Jul 5, 2017

The JSX tag is mapped to a constructor. It shouldn't lose its types, being instaniated programatically or by some JSX tag.

@frankwallis
Copy link
Contributor

Well it does because the class being returned specifies the props to be type any:

 return class extends component<any, any> {

If you put the correct types in here then the props should end up properly typed. However, having tried it, it does seem difficult to use the 'inheritance approach' with base components which are not generic. I was able to get it working using the 'composition approach' however:

/* higher-order component */
export interface HighlightedProps {
    isHighlighted?: boolean;
}

export function Highlighted<T>(InputTemplate: React.ComponentClass<T>) {
    return class extends React.Component<T & HighlightedProps> {
        render() {
            let className = this.props.isHighlighted ? "highlighted" : "";
            return (
                <div className={className}>
                    <InputTemplate {...this.props} />
                </div>
            );
        }
    }
}

/* some basic components */
interface MyInputProps {
    inputValue: string;
}
class MyInput extends React.Component<MyInputProps, {}> { };

interface MyLinkProps {
    linkAddress: string;
}
class MyLink extends React.Component<MyLinkProps, {}> { };

/* wrapped components */
const HighlightedInput = Highlighted(MyInput);
const HighlightedLink = Highlighted(MyLink);

/* usage example */
export class Form extends React.Component<{}, {}> {
    render() {
        return (
            <div>
                <HighlightedInput inputValue={"inputValue"} isHighlighted={false} />
                <HighlightedLink linkAddress={"/home"} isHighlighted={true} />
            </div>
        );
    }
}

@flushentitypacket
Copy link

flushentitypacket commented Jul 21, 2017

@frankwallis That looks good, but for some reason I get Spead types may only be created from object types on {...this.props}, which seems to be tracked by this issue:
#10727

I haven't been able to figure out a workaround

@gabeweaver
Copy link

this is how i've approached it until the new spread proposal goes live:

this wraps my routes entry component:

import * as React from 'react'

import connectState from 'hoc/connectState'
import { verifySocialSession, VerifySocialSession } from 'hoc/withAuth'
import { compose } from 'lib/helpers'

export interface InitializeProps {
  initialized: boolean
}

interface InjectedProps extends VerifySocialSession {
  storedSession: boolean | LastAction
}

interface State {
  initialized: boolean
}

const initializeRoutesWrapper = <OP extends {}>(
  WrappedComponent: React.SFC<OP & InitializeProps>
) => {
  type Result = OP & InjectedProps
  class InitializeRoutes extends React.Component<Result, State> {
    constructor (props: Result) {
      super(props)

      this.state = {
        initialized: false
      }
    }

    public componentWillReceiveProps (next) {
      const uninitialized = !this.state.initialized
      const rehydratedSession = next.storedSession
      if (uninitialized && rehydratedSession) {
        this.verifySocial(next.storedSession.payload.session)
      }
    }

    /**
     *   Use the token from the rehydrated state to login to the server with loginSocial.
     *   If there is an error authenticating, it will redirect to the redirectOnError param.
     */
    public verifySocial = (session: Session) => {
      if (session && session.sessionType === 'social') {
        this.props.verifySocialSession(session.token)
          .then(() => this.initialize())
      }
    }

    public initialize = () => this.setState({ initialized: true })

    public render () {
      return <WrappedComponent initialized={true} {...this.props} />
    }
  }

  const enhance = compose(
    connectState(
      (selectors: Selectors) => ({
        storedSession: selectors.actionRehydrate
      })
    ),
    verifySocialSession({
      redirectOnError: '/login'
    })
  )

  return enhance(InitializeRoutes)
}

export default initializeRoutesWrapper

Here I'm wrapping my entry with a withSubRoutes HOC, which wraps the routes with a layout. I know that I will want to spread some props to my wrapped layout component, so I create a helper function to build the props that i will spread in the next HOC under the layout key. I create a type called LP since i'm not creating any static props within the component itself. If I were, i'd just extend them with whatever other interfaces i've imported.

import * as React from 'react'

import withSubRoutes, { RouteProps } from 'hoc/withSubRoutes'

import initialize, { InitializeProps } from './initialize'
import EntryLayout from './layout'

/** LayoutProps Shorthand */
type LP = InitializeProps

const AppEntry = withSubRoutes<LP>(EntryLayout)

const layout = (props: LP) => ({
  ...props
})

const Routes: React.SFC<RouteProps & LP> = ({ initialized, routes, store }) =>
  <AppEntry layout={layout({ initialized })} routes={routes} store={store} />

export default initialize(Routes)

Then in my withSubRoutes, i'm able to spread the layout prop to the wrapped layout:

import * as React from 'react'

import NotFound from 'components/NotFound'
import { Route, Switch } from 'lib/router'
import { Store } from 'lib/types'

import { getDisplayName } from '../helpers'

export interface RouteProps {
  routes: RouteConfig[],
  store: Store<{}>
}

export interface SubRoutes<LayoutProps> extends RouteProps {
  layout: LayoutProps,
}

/**
 *  LP = LayoutProps
 *
 *  interface LP extends LayoutPropsInterfaceFromParent {
 *    staticLayoutProp: string
 *  }
 *
 *  const MakeRoutes = withSubRoutes<LP>(Layout)
 *  const layout = (props: LP) => ({ ...props })
 *
 *  const RenderRoutes = ({ propA, propB, propC }) => (
 *    <MakeRoutes propA={ propA } propB={ propB } layout={ layout({ propC, staticLayoutProp: 'red' }) } />
 *  )
 *
 */

const composedMatchSubRoutes = <LP extends {}>(
  WrappedComponent: React.SFC<LP>
) => {
  const MatchRoutes: React.SFC<SubRoutes<LP>> = ({ routes, store, layout }) => {
    return (
      <WrappedComponent {...layout}>
        <Switch>
          {routes.map(({
            routeComponent: RouteComponent,
            routes: subRoutes,
            ...route
          }) => (
            <Route
              key={route.id}
              {...route}
              children={({ ...routerProps }) => (
                <RouteComponent {...routerProps} store={store} routes={subRoutes} />
              )}
            />
          ))}
          <Route path='*' render={({ location }) => <NotFound location={location} />} />
        </Switch>
      </WrappedComponent>
    )
  }

  MatchRoutes.displayName = getDisplayName(WrappedComponent, 'subRoutes')

  return MatchRoutes
}

export default composedMatchSubRoutes

It's certainly not perfect but it does work :)

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants