Skip to content

Instantly share code, notes, and snippets.

@tejacques
Last active May 2, 2022 13:05
Show Gist options
  • Save tejacques/54997ef2d6f672314d53 to your computer and use it in GitHub Desktop.
Save tejacques/54997ef2d6f672314d53 to your computer and use it in GitHub Desktop.
React Higher Order Components in TypeScript
import * as React from 'react';
import { Component } from 'react';
export default function HOCBaseRender<Props, State, ComponentState>(
Comp: new() => Component<Props & State, ComponentState>) {
return class HOCBase extends Component<Props, State> {
render() {
return <Comp {...this.props} {...this.state}/>;
}
}
}
import * as React from 'react';
import { Component } from 'react';
import HOCBaseRender from './HOCBaseRender';
export default function HOCMounted<Props, ComponentState>(
Comp: new() => Component<Props, ComponentState>, onMount: () => void, onUnmount: () => void) {
return class HOCWrapper extends HOCBaseRender<Props, void, ComponentState>(Comp) {
// ... Implementation
componentWillMount() {
onMount.call(this);
}
componentWillUnmount() {
onUnmount.call(this);
}
}
}
import * as React from 'react';
import NameAndAge from './NameAndAge';
import HOCStateToProps from './HOCStateToProps';
export default HOCStateToProps<
{ name: string },
{ age: number },
void>(NameAndAge, () => ({ age: 12 }));
import * as React from 'react';
import { Component } from 'react';
import HOCBaseRender from './HOCBaseRender';
export default function HOCStateToProps<Props, State, ComponentState>(
Comp: new() => Component<Props & State, ComponentState>, getState: () => State) {
return class HOCWrapper extends HOCBaseRender<Props, State, ComponentState>(Comp) {
// ... Implementation
constructor() {
super();
this.state = getState();
}
}
}
import * as React from 'react';
import { Component } from 'react';
export default class NameAndAge extends Component<{ name: string, age: number }, void> {
render() {
return <div>Name: {this.props.name}, Age: {this.props.age}</div>;
}
}
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import HOCNameAndAge from './HOCNameAndAge';
ReactDOM.render(<HOCNameAndAge name='Hello'/>, document.getElementById('root'));
@joonhwan
Copy link

Cool. Learned a lot. BTW is there any code like this in typescript.org ? If not ther must be one!

Is there another good code to learn about HOC concept out there that you know?

@NicholasBoll
Copy link

Your examples worked until I tried to add the declarations: true in the compiler options which will create type definitions files in your output.

For the first example (HOCBaseRender.tsx), the type error I got was TS4060: Return type of exported function has or is using private name 'HOCBase'. The solution is pretty simple - add an explicit return type of React.ComponentClass<P>

import * as React from 'react';
import { Component } from 'react';

export default function HOCBaseRender<Props, State,  ComponentState>(
    Comp: new() => Component<Props & State, ComponentState>): React.ComponentClass<Props & State> {
    return class HOCBase extends Component<Props, State> {
        render() {
            return <Comp {...this.props} {...this.state}/>;
        }
    }
}

I also lost type information with the new() => Component... part in TypeScript 2.1. I created a HOC that had more types implied through generics:

simpleComponentWrap.tsx:

import * as React from 'react'

export default function simpleComponentWrap<P>(
  name: string, Comp: React.ComponentClass<P> | React.StatelessComponent<P>,
): React.ComponentClass<P> {
  return class WrappedComponent extends React.Component<P, {}> {
    render() {
      return <Comp {...this.props} />
    }
  }
}

Button.tsx:

import * as React from 'react'

import simpleComponentWrap from './simpleComponentWrap'

interface Button {
  type: 'button' | 'submit'
  text: string
}

const Button = ({ type, text }: Button) =>
  <button type={type}>{ text }</button>

export default simpleComponentWrap(Button)

No explicit typing is needed when wrapping, but strict type checking is preserved.

@rwieruch
Copy link

If someone is interested in a introduction of HOCs before using them with TypeScript, you could read the gentle introduction to higher order components.

@AlgusDark
Copy link

@NicholasBoll your example throw this error [ts] Spread types may only be created from object types. at line return <Comp {...this.props} />. Any ideas on how to fix that?

@dragosbulugean
Copy link

@AlgusDark - i'm having the same issue. did you find any solutions?

@tak0209
Copy link

tak0209 commented Jun 5, 2017

This works...

export const Logger = (WrapperedComponent: typeof React.Component, name?: string) =>
class extends React.Component<any, any>{
public render() {
console.log((name ? name : '') + ' logging things... @' + new Date().getTime());
return (<WrapperedComponent {...this.props} />);
}
};

@MrKou47
Copy link

MrKou47 commented Jun 8, 2017

@AlgusDark - i'm having the same issue. It looks like the typescript bug

@NicholasBoll
Copy link

@AlgusDark @MrKou47 It is a TypeScript bug tracked here: microsoft/TypeScript#13288. You can fix by doing {...this.props as object}. It is incorrectly casting P.

@kazagkazag
Copy link

kazagkazag commented Aug 30, 2017

This works for me with TS@2.4.2 and React:

// HOC for use case:
// every component connected to some external data source has prop, which has shape: 
// { pending: boolean, data: object, error: object, done: boolean }
// name of that prop is not defined, so UsersList has "users" prop, ImagesList has "images" prop, but
// shape is always the same. I want to display loader, while resource is fetching.

import * as React from "react";
import Loader from "../Loader/Loader"; // spinner!

interface DisplayLoaderOptions {
    resourceName: string; // example: "users" for UsersList, "images" for ImagesList
}

interface ComponentWithResourceProps {
    [resourceName: string]: {
        pending: boolean // otherwise TS will not know about "pending" property
    }
}

function displayLoader({resourceName}: DisplayLoaderOptions) {

    return <OriginalProps extends {},
        Component extends React.ComponentClass<OriginalProps>>(WrappedComponent: Component): Component => {

        class WithLoader extends React.Component<OriginalProps & ComponentWithResourceProps> {
            public render() {
                return this.props[resourceName] && this.props[resourceName].pending === true
                    ? <Loader center/>
                    : <WrappedComponent {...props} />;
            }
        }

        return WithLoader as Component; // otherwise WrappedComponent is not assignable to Component returned from HOC. It smells...
    };
}

export default displayLoader;

Test:

import "jest";
import * as React from "react";
import {mount} from "enzyme";
import displayLoader from "./DisplayLoader";
import Loader from "../Loader/Loader";

describe("Shared - Layout - enhancements - displayLoader", () => {
    test("should display loader when resource is fetching", () => {
        @displayLoader({
            resourceName: "myResource"
        })
        class TestComponent extends React.Component<any, any> {
            public render() {
                return <p>Test</p>
            }
        }

        const testComponentWithLoader = mount(<TestComponent myResource={{pending: true}} />);

        expect(testComponentWithLoader.find("p").length).toBe(0);
        expect(testComponentWithLoader.find(Loader).length).toBe(1);
    });

    test("display component when resource is not fetching", () => {
        @displayLoader({
            resourceName: "resource"
        })
        class TestComponent extends React.Component<any, any> {
            public render() {
                return <p>Test</p>
            }
        }

        const testComponentWithLoader = mount(<TestComponent resource={{pending: false}} />);

        expect(testComponentWithLoader.find("p").length).toBe(1);
        expect(testComponentWithLoader.find(Loader).length).toBe(0);
    });
});

A little bit tricky (return XXX as XX ??).

Any better ideas (I'm starting with TS)?
Works with decorators and component classes for now. I did not test it with stateless components and "no-decorator" approach.

@ciekawy
Copy link

ciekawy commented Sep 4, 2017

I believe that due to microsoft/TypeScript#6559 and microsoft/TypeScript#12215 optimal solution (without casting the result) is not possible yet.

@MuhannedAlogaidi
Copy link

Hi ,
could you please help me to find books or resource on how to learn typescript programming am using now same tech you used also same file ext.
but i am learning through shredded information from diffrent websites

here are sample of code i wrote it

import * as React from 'react';
import 'isomorphic-fetch';

interface IDataTunel {
    name: string;
    desription: string;
    forchild: string;
}
interface IContentTunel {
    empdata: EmployeeContacts;
}
class EmployeeContacts {
    public mobile: string;
    public email: string;
    public others: string;

    constructor() {
        this.mobile = "mobile";
        this.email = "email";
        this.others = "others";
    }
}

export class ListItems extends React.Component<IDataTunel, IContentTunel > {
    public constructor() {
        super();
        this.state = { empdata: null };
    }
    componentWillMount() {
        return (
            this.state = { empdata: new EmployeeContacts() }
        );
    };

   public render() {
        return (
            <div className="List_Items">
                <h1>{this.props.forchild} </h1>
                <span><h4>{this.props.name} with type {this.props.desription}</h4></span>
                <br/>
                <button type="Submitt" id="gd" onClick={() => {this.readdata((document.getElementById("id") as HTMLInputElement).value)}}>Display other information</button>
                <ul>
                    <li>Mobile : {this.state.empdata.mobile} </li>
                    <li>Email : {this.state.empdata.email} </li>
                    <li>Others :{this.state.empdata.others} </li>
                </ul>
            </div>
        );
    }
   public readdata(id): EmployeeContacts {
        fetch('/api/SampleData/GetEmployeesContacts?id=' + id).then(response => response.json() as Promise<EmployeeContacts>).then(data => {
            this.setState({ empdata: data });
        });
        return this.state.empdata;
    };
}
export default ListItems
{
}

@trusktr
Copy link

trusktr commented Dec 21, 2019

The limitations of declaration emit prevent inferred return types from being compilable to declaration files.

I've opened a request to fix issues like these in TypeScript by bringing declaration files to parity with language features. microsoft/TypeScript#35822

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