
Many times when talking about translating React-projects into TypeScript, I often heard that the creation of HoCs (Higher-Order Components - wrapper components) caused the most pain. Today I will show you how to do it painlessly and quite easily. This trick will be useful not only for TS projects, but also for ES6 + projects.
As an example, take the HoC that wraps the standard HTMLInput, and in the first argument onChange, instead of the Event object, passes the actual value of the text field. Consider 2 options for implementing this adapter: as a function, the host component, and as a wrapper.
Many newcomers solve this problem head-on - using a React.cloneElement, they create a clone of the item, passed on as a child, with new Props. But this leads to difficulties in maintaining this code. Let's look at this example to never do this again. Let's start with ES6 code:
')
If we neglect the test for the uniqueness of the child and the transfer of the
onChange
property, then this example can be written even shorter:
Note that the callback for the transfer to the internal component is set outside the wrapper function, this will allow not re-creating the function during each component render cycle. But we are talking about TypeScript, so add some types and get the following component:
import * as React from 'react'; export interface Props { onChange: (value: string) => void; children: JSX.Element; } export const OnChange = ({ onChange, children }: Props) => { const onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => ( onChange(event.target.value) ) const Child = React.Children.only(children); return React.cloneElement(Child, {...children.props, onChange: onChangeHandler}); }
We added a description of Props for the component and typed onChange: in it we indicated that we expect the event argument to be input, which coincides by signature with the event object passed from HTMLInput. In this case, we specified in the external properties that in onChange, the first argument instead of the event object is a string. On this bad example is over, it's time to move on.
Hoc
Now let's look at a good example of writing HoC: a function that returns a new component, wrapping the original one. Thus, the connect function from the react-redux package works. What is needed for this? In simple terms, we need a function that returns an anonymous class, which is a HoC for the component. A key problem with TypeScript is the need to use generics for strong typing of HoCs. But more about that later, let's start with an example on ES6 +.
export const withOnChange = Child => { return class OnChange extends React.Component { onChangeHandler = event => this.props.onChange(event.target.value); render() { return <Child {...this.props} onChange={this.onChangeHandler} />; } } }
The first argument passed to us is the declaration of the component class, which is used to create the component instance. In the render method in the instance of the wrapped component, we pass the modified callback onChange and all other properties unchanged. As in the first example, we brought the onChangeHandler function initialization outside the render method and passed the reference to the function instance to the internal component. In any more or less complex React project, the use of HoCs provides better code portability, since common handlers are put into separate files and connected as needed.
It is worth noting that in this example, the anonymous class can be replaced with the stateless function:
const onChangeHandler = onChange => event => onChange(event.target.value); export const withOnChange = Child => ({ onChange, ...props }) => <Child {...props} onChange={onChangeHandler(onChange)} />
Here we have created a function with a component-class argument that returns a stateless function that accepts the props of this component. A function was created in the onChange handler that creates a new onChangeHandler when passing an event handler from the internal component.
Now back to TypeScript. By performing such actions, we will not be able to take full advantage of strong typing, since by default the passed-in component and the return value will take the type any. When the strict mode is enabled, TS will display an error about the implicit type any in the function argument. Well, let's proceed to typing. First of all, let's declare the onChange properties in the received and sent components:
Now we have clearly indicated which Props the wrapped component should have and which Props are the result of the composition. Now we will declare the component itself:
export function withOnChangeString<T extends OnChangeNative>(Child: React.ComponentType<T>) { . . . }
Here we have indicated that the argument is a component that has the onChange property of a certain signature in the properties, i.e. having native onChange. For the HoC to work, it is necessary to return a React component from it, which already has the same external properties as the component itself, but with a modified onChange. This is done by the expression
OnChangeHoCProps & T
:
export function withOnChangeString<T extends OnChangeNative>(Child: React.ComponentType<T>) { return class extends React.Component<OnChangeHoCProps & T, {}> { . . . } }
Now we have a typed HoC that takes a callback onChange, which expects to receive a string as a parameter, returns the wrapped component and sets onChange to an internal component that gives the Event as an argument.
When debugging code in React DevTools, we may not see the names of the components. The displayName static property is responsible for displaying component names:
static displayName = `withOnChangeString(${Child.displayName || Child.name})`;
We are trying to get a similar property from the internal component and wrap it with the name of our HoC in the form of a string. If there is no such property, then you can use the ES2015 specification, to which all the functions have the name property added, indicating the name of the function itself. However, TypeScript, when compiled in ES5, will generate an error stating that the function does not have this property. To solve this problem, you need to add the following line to tsconfig.json:
"lib": ["dom", "es2015.core", "es5"],
In this line, we told the compiler that we can use the basic set of the ES2015, ES5 specification and API for working with the DOM in the code. The full code of our HoC'a:
export function withOnChangeString<T extends OnChangeNative>(Child: React.ComponentType<T>) { return class extends React.Component<OnChangeHoFProps & T, {}> { static displayName = `withOnChangeString(${Child.displayName || Child.name})`; onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => this.props.onChange(event.target.value); render() { return <Child {...this.props} onChange={this.onChangeHandler} />; } } }
Now our HoC is ready for battle, use the following test to check its operation:
Finally
Today we reviewed the basic techniques for writing HoCs on React. However, in real life it happens that not one, not two, but a whole chain of HoCs is used. In order not to turn the code into noodles, there is a
compose
function, but we'll talk about it next time.
That's all, the source code of the project is available on
GitHub . Subscribe to our blog and stay tuned!