2
votes

I am trying to write a React Context HOC function so that I can wrap other components with the Context allowing me to send all notifications to a single component. I had to piece together a working solution from a few different guides, mainly this one as none of them quite worked in their original form.

What I ended up with is something like this:

HOC:

import * as React from "react";
import { INotificationContextState, NotificationConsumer } from "./NotificationContext";

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

export default function withNotificationContext<
  P extends { notificationContext?: INotificationContextState },
  R = Omit<P, 'notificationContext'>
  >(
  Component: React.ComponentClass<P> | React.StatelessComponent<P>
  ): React.SFC<R> {
  return function BoundComponent(props: R) {
    return (
      <NotificationConsumer> 
        {value => <Component {...props} notificationContext={value} />} 
      </NotificationConsumer>
    );
  };
}

Context:

 interface IMessage {
    message: string;
    key: number;
}

interface INotificationState {
    open: boolean;
    messageInfo: IMessage;
    timeOut: number;
}
interface INotificationContextState extends INotificationState {
    handleClick(message: string): void;
    handleClose(event: React.MouseEvent<HTMLElement>, reason: any): void;
    handleExited(): void;
}

const NotificationContext = React.createContext<INotificationContextState | null>(
    null
);

class NotificationProvider extends React.Component {
    public state: Readonly<INotificationState> = DEFAULT_STATE;
    private queue: IMessage[] = [];

    constructor(props: INotificationContextState) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
        this.handleClose = this.handleClose.bind(this);
    }
    // rest of the class methods...
}

export const NotificationConsumer = NotificationContext.Consumer;

The HOC file gives me a red underline under the word Component in the line {value => <Component {...props} notificationContext={value} />}

The error reads as follows:

Type 'R & { notificationContext: INotificationContextState | null; }' is not assignable to type 'IntrinsicAttributes & P & { children?: ReactNode; }'. Type 'R & { notificationContext: INotificationContextState | null; }' is not assignable to type 'P'.ts(2322)

The strange thing is that this error does not prevent me from running the app, and furthermore the wrapper function works exactly like it should, and the wrapped component is able to pass notifications to the Notification object.

Part of my trouble is being unfamiliar with the Omit/Pick syntax and trying to mentally work out exactly what the type definition of "P" is in this context. I just have no idea why it generates an error, yet still works.

2

2 Answers

0
votes

I also don't have a working solution for this at the moment. But for now I'm late casting the props passed to Component like so:

<Component {...props as any} notificationContext={value} />

This would still give you types in the render method and will get rid of the error.

0
votes

The problem is the generic R. It defaults to Omit<P, 'notificationContext'>, but it could be explicitly set to anything. You don't have any problems when running the code because this default value is the value that you want, but you don't want it to be generic.

Your HOC should only have one generic value. Here I am using the generic P to refer to the props of the outer component. We take a component that needs notificationContext and some props P and turn it into a component which only requires those props P.

I had to include null as one of the possible values of the notificationContext prop because your context value could be null.

export default function withNotificationContext<P extends {}>(
    Component: React.ComponentType<P & { notificationContext: INotificationContextState | null }>
): React.ComponentType<P> {
    return function BoundComponent(props: P) {
        return (
            <NotificationConsumer>
                {value => <Component {...props} notificationContext={value} />}
            </NotificationConsumer>
        );
    };
}