Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React/TypeScript: Consuming context via HOC

I'm trying to implement the example Consuming Context with a HOC from the React documentation (React 16.3) in TypeScript (2.8) and failing miserably. For reference, the code from React's manual:

const ThemeContext = React.createContext('light');

// This function takes a component...
export function withTheme(Component) {
  // ...and returns another component...
  return function ThemedComponent(props) {
    // ... and renders the wrapped component with the context theme!
    // Notice that we pass through any additional props as well
    return (
      <ThemeContext.Consumer>
        {theme => <Component {...props} theme={theme} />}
      </ThemeContext.Consumer>
    );
  };
}

The best I could come up with:

export interface ThemeAwareProps {
  theme: string;
}

const ThemeContext = React.createContext('light');

export function withTheme<P extends ThemeAwareProps, S>(Component: new() => React.Component<P, S>) {
  return function ThemedComponent(props: P) {
    return (
      <ThemeContext.Consumer>
        {theme => <Component {...props} theme={theme} />}
      </ThemeContext.Consumer>
    );
  };
}

class App extends React.Component {

  public render() {
    return (
      <ThemeContext.Provider value={'dark'}>
        <ThemedButton/>
      </ThemeContext.Provider>
    );
  }
}

ThemedButton.tsx:

interface ThemedButtonProps extends ThemeAwareProps {
}

interface ThemedButtonState{
}

class ThemedButton extends React.Component<ThemedButtonProps, ThemedButtonState> {

  constructor(props: ThemedButtonProps) {
    super(props);
  }


  public render() {
    return (
      <button className={this.props.theme}/>
    )
  }
}

export default withTheme(ThemedButton);

The problem is the last line (export default withTheme(ThemedButton)). The TypeScript compiler complains that

Argument of type typeof ThemedButton is not assignable to parameter of type new () => Component<ThemedButtonProps, ThemedButtonState, any>.

What am I missing?

like image 514
aha Avatar asked May 30 '18 19:05

aha


People also ask

Why useContext is used in react?

Context provides a way to pass data or state through the component tree without having to pass props down manually through each nested component.

Is context API same as useContext?

The useContext is the React hook, used in context API to consume the context state or object. There are two options for getting the context object. We can get the context object from Context Consumer or useContext Hook. UseContext Hook is an exquisite, more excellent way to get the context object with less code.

Is react context bad for performance?

Context API is a nice feature, but, since every context update always re-renders every consumer of this context, may cause performance problems if not used carefully.


1 Answers

You got it right for the most part, just with a few missing pieces:

  1. For Component, use React.ComponentType<Props>, which correctly accepts class components and functional components. I figure using new () => ... alone doesn't work here because the signatures didn't fully match up.

  2. To exclude the props from ThemedButton while using it, you'll have to use some magical-looking syntax:

function ThemedComponent(props: Pick<P, Exclude<keyof P, keyof ThemeAwareProps>>)

Here's what this does:

  • Exclude<keyof P, keyof ThemeAwareProps> means "get the keys of P, then take away the keys that are in ThemeAwareProps"
  • Pick<P, ...> then says, "from P, return an object type with only these properties"

Combining these gives us a component that accepts all the props that ThemedButton does, minus the theme prop, so that we can do <ThemedButton /> without errors.

Here's the full HOC:

function withTheme<P extends ThemeAwareProps>(Component: React.ComponentType<P>) {
  return function ThemedComponent(props: Pick<P, Exclude<keyof P, keyof ThemeAwareProps>>) {
    return (
      <ThemeContext.Consumer>
        {(theme) => <Component {...props} theme={theme} />}
      </ThemeContext.Consumer>
    )
  }
}

And finally, a good blog post on the subject, from which I gleamed most of this information from. It also includes a way to shorten the Pick<...> stuff with an Omit type, if you prefer.


EDIT: The behavior of rest/spread has changed in 3.2, and this bug came up as an unfortunate side effect, causing the type of props to get erased when merged with other props. A currently working workaround is to cast props as P:

    return (
      <ThemeContext.Consumer>
        {(theme) => <Component {...props as P} theme={theme} />}
      </ThemeContext.Consumer>
    )
like image 177
kingdaro Avatar answered Sep 19 '22 10:09

kingdaro