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 typenew () => Component<ThemedButtonProps, ThemedButtonState, any>
.
What am I missing?
Context provides a way to pass data or state through the component tree without having to pass props down manually through each nested component.
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.
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.
You got it right for the most part, just with a few missing pieces:
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.
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>
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With