Introduction
These notes should help in better understanding TypeScript
and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.
More Notes on TypeScript
Notes on TypeScript: Pick, Exclude and Higher Order Components
Render Props
Render Props
is a popular pattern for enhancing a React Component with additional functionality. It can be interchanged with a higher order component, and choosing the render props pattern or a higher order component is a matter of flavour and depends on the specific use case.
To get a better understanding of the topic, let's build a component that uses a render prop. In the previous "Notes on TypeScript" we built a component that provided an Input
component with onChange
and value
properties.
We can rewrite this higher order component to a render prop implementation.
class OnChange extends React.Component {
state = {
value: this.props.initialValue
};
onChange = event => {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
this.setState({ value });
};
render() {
return this.props.render({
value: this.state.value,
onChange: this.onChange
});
}
}
Using the refactored OnChange
inside your React application:
<OnChange
initialValue="hello"
render={onChangeProps => <Input {...props} {...onChangeProps} />}
/>
We can reuse most of the previously defined types.
type InputProps = {
name: string,
type: string
};
type OnChangeProps = {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
value: string
};
type ExpandedOnChangeProps = {
initialValue: string | boolean,
render: (onChangeProps: onChangeProps) => JSX.Element
};
type OnChangeState = {
value: string
};
Our Input
component has not changed, we can also reuse that component for this example.
const Input = ({ value, onChange, type, name }: InputProps & OnChangeProps) => (
<input type={type} name={name} value={value} onChange={onChange} />
);
So now that we have everything in place, let's see how OnChange
would be typed.
Interestingly, there's not very much we need to do, to type the onChange
component.
class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState> {
state = {
value: this.props.initialValue
};
onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const target = event.target;
this.setState({ value: target.value });
};
render() {
return this.props.render({
value: this.state.value,
onChange: this.onChange
});
}
}
Compared to the higher order component implementation, we only need to define props and state for OnChange
, in this case using the already existing ExpandedOnChangeProps
and OnChangeState
and defining the class component as follows:
class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState>
.
We might want to reuse the functionality in multiple places inside our application. By defining a new component, f.e. ControlledInput
, we can combine our OnChange
and Input
and let developers define the initialValue
as well as name
and type
.
type ControlledInputProps = InputProps & { initialValue: string };
const ControlledInput = ({ initialValue, ...props }: ControlledInputProps) => (
<OnChange
initialValue={initialValue}
render={onChangeProps => <Input {...props} {...onChangeProps} />}
/>
);
Now ControlledInput
can be used inside another component and TypeScript will complain when either name
, type
or initialValue
is missing.
<ControlledInput initialValue="testdrive" type="text" name="test" />
Advanced
We might want to enable to either pass the render callback via render or children prop. This requires us to make some changes to our OnChange
component. If we recall, our ExpandedOnChangeProps
has the following shape:
type ExpandedOnChangeProps = {
initialValue: string | boolean,
render: (onChangeProps: onChangeProps) => JSX.Element
};
One way to enable passing callbacks as children prop is to change the definition to the following:
type ExpandedOnChangeProps = {
initialValue: string,
render?: (onChangeProps: onChangeProps) => JSX.Element,
children?: (onChangeProps: onChangeProps) => JSX.Element
};
But the above definition has problems, as both or none of the variants could be provided now. What we actually want is to ensure that one of these properties is defined, which is possible defining an explicit RenderProp
type:
type RenderProp =
| { render: (onChangeProps: OnChangeProps) => JSX.Element }
| { children: (onChangeProps: OnChangeProps) => JSX.Element };
Which means we can rewrite our ExpandedOnChangeProps
definition to:
type ExpandedOnChangeProps = {
initialValue: string
} & RenderProp;
Finally we need to update the render function to handle both possible cases:
class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState> {
state = {
value: this.props.initialValue
};
onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const target = event.target;
this.setState({ value: target.value });
};
render() {
if ("render" in this.props) {
return this.props.render({
value: this.state.value,
onChange: this.onChange
});
}
if ("children" in this.props) {
return this.props.children({
value: this.state.value,
onChange: this.onChange
});
}
throw new Error("A children or render prop has to be defined");
}
}
By using "render" in this.props
, we can check if render
is defined else check if a children
property is defined. In case neither properties are defined we throw an error.
Our previously defined ControlledInput
could be rewritten to:
const ControlledInput = ({
initialValue,
...props
}: InputProps & { initialValue: string }) => (
<OnChange initialValue={initialValue}>
{onChangeProps => <Input {...props} {...onChangeProps} />}
</OnChange>
);
We should have a basic understanding of how render props can be typed with TypeScript now.
If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif
Top comments (2)
Thanks for the article A. Sharif.
It seems like this is a part of a series.
Would you be able to mark series posts in the meta data so it's easy to navigate?
Thanks for the info Sung Kim!