React with TypeScript
I have been coding with React for over 5 years now. At first when I started I always felt like something was missing. React itself doesn’t have strict type checking and on larger scale projects you wouldn’t know what state shape you are working with or what the mysterious API response returns (unless you check it). It requires more time in general to code some feature (strange as you write more code with TypeScript as opposed to without it). Also occasionally you bump into bugs where you try to use some value, but it does not exist there.
TypeScript solves these problems! I will write a summary of useful code snippets with React + TypeScript.
1. Functional (presentational) component
import React, { MouseEvent } from 'react'interface Props {
onClick(e: MouseEvent<HTMLElement>): void
}const Button: React.FC<Props> = ({ onClick, children }) => (
<button>
{children}
</button>
)
There is a predefined type within @types/react
=> type FC<P>
which is just an alias of interface FunctionalComponent<P>
and it has pre-defined children
and some other things (defaultProps, displayName...), so we don't have to write it every time on our own!
2. Stateful Component
First of all we declare our initialState:
const initialState = { counter: 0 };
Then we use Typescript to infer State type from our implementation.
By doing this we don’t have to maintain types and implementation separately, we have a single source of truth, which is the implementation.
type State = Readonly<typeof initialState>
Also you can notice that State
variable is read-only. We need to be explicit again and define our State type to define state property on the class.
readonly state: State = initialState
Why is this useful/needed ?
We know that we cannot update
state
directly within React like following:
this.state.clicksCount = 55
this.state = { clicksCount: 55 }
This will throw a runtime error, but not during compile time. By explicitly mapping our
type State
to readonly viaReadonly
and setting readonly state within our class component, TS will let us know that we are doing something wrong immediately.
Example:
Final component looks like this:
import React, { Component, MouseEvent } from 'react';const initialState = { clicksCount: 0 };
type State = Readonly<typeof initialState>;class ButtonCounter extends Component<object, State> {
readonly state: State = initialState; render() {
const { clicksCount } = this.state;
return (
<>
<Button onClick={this.handleIncrement}>Add</Button>
<Button onClick={this.handleDecrement}>Decrease</Button>
Counter is: {clicksCount}!
</>
);
} handleIncrement = () => this.setState(incrementCounter);
handleDecrement = () => this.setState(decrementCounter);
}const incrementCounter = (prevState: State) => ({ clicksCount: prevState.clicksCount + 1 });const decrementCounter = (prevState: State) => ({ clicksCount: prevState.clicksCount - 1 });
You can see that we’ve extracted state update functions to pure functions outside of the class. This is a pretty common pattern, as we can test those with ease, without any knowledge of renderer layer. Also because we are using typescript and we mapped State to be explicitly read-only, it will prevent us to do any mutations within those functions as well
const decrementClicksCount = (prevState: State)
=> ({ clicksCount: prevState.clicksCount-- })// Throws complile error:
//
// [ts]
// Cannot assign to 'clicksCount' because it is a constant or a read-only property.
3. Higher order component (enhancers)
Let’s say we want a ButtonCounter
to be enhanced with loading logic to show a spinner while the page is being loaded.
First we need to create HOC that wrap our component with loading logic.
First we define the interface:
interface WithLoadingProps {
loading: boolean;
}
Then we are going to use a generic; P
represents the props of the component that is passed into the HOC. React.ComponentType<P>
is an alias for React.FunctionComponent<P> | React.ClassComponent<P>
, meaning the component that is passed into the HOC can be either a function component or class component.
<P extends object>(Component: React.ComponentType<P>)
Then we are defining a component to return from the HOC, and specifying that the component will include the passed in component’s props (P
) and the HOC’s props (WithLoadingProps
). To combine multiple types we are using a type intersection operator (&
).
class WithLoading extends React.Component<P & WithLoadingProps>
And lastly, we use the loading
prop to conditionally display a loading spinner or our component with its own props passed in:
return loading ? <LoadingSpinner /> : <Component {...props as P} />;
This is the final class implementation:
const withLoading = <P extends object>(Component: React.ComponentType<P>) =>
class WithLoading extends React.Component<P & WithLoadingProps> {
render() {
const { loading, ...props } = this.props;
return loading ? <LoadingSpinner /> : <Component {...props as P} />;
}
};class WithLoading extends React.Component<P & WithLoadingProps>
It can also be a functional component:
const withLoading = <P extends object>(
Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> => ({
loading,
...props
}: WithLoadingProps) =>
loading ? <LoadingSpinner /> : <Component {...props as P} />;
Now we can wrap our either ButtonCounter
(Functional or Classy one) and have a LoadingButtonCounter
as a result. In the future if we decide that we need a different Loading component, we do not need to create the loading logic from the scratch. We just reuse our withLoading
HOC. It saves time, believe me!
4. Higher order component (injectors)
The more common form of HOCs are injectors. It is more difficult to set types for them though. Besides injecting props into a component, in most cases they also remove the injected props when it is wrapped so they can no longer be set from the outside. react-redux’s connect
is an example of an injector HOC, but in this article we will use a simpler example — a HOC that injects a counter value and callbacks to increment and decrement the value:
There are a few major differences here:
export interface InjectedCounterProps {
value: number;
onIncrement(): void;
onDecrement(): void;
}
An interface is being declared for the props that will be injected into the component. It is exported so it can be used by the component that the HOC wraps:
<P extends InjectedCounterProps>(Component: React.ComponentType<P>)
We use a generic here and this time it ensures that the component passed into the HOC includes the props that are going to be injected by it. If it doesn’t — you will receive a compilation error.
class CounterFactory extends React.Component<
Subtract<P, InjectedCounterProps>,
CounterFactoryState
>
The component returned by the HOC uses Subtract
from utility-types package, which will subtract the injected props from the passed in component’s props, meaning that if they are set on the resulting wrapped component you will receive a compilation error:
const WrappedCounter = counterFactory(Counter);
const wrappedCounter = <WrappedCounter value={2} /> // TS error setting value
In most cases, this is the behaviour you would want from an injector.
Thanks for reading! Let me know what you think.