Ultimate React Component Patterns with Typescript 2.8
Stateful, Stateless, Default Props, Render Callbacks, Component Injection, Generic Components, High Order Components, Controlled Components
This blogpost was inspired by React Component Patterns post
If you know me, you already know that I don’t write javascript without types, so yeah I’m really into Typescript, since version 0.9. Beside typed JS I really love React, and when React + Typescript are combined, I just feel like in heaven . Full type safety within whole app and VDOM templates it’s just marvelous and joy to work with.
So what is this post all about? Well, there are various articles about React Component patterns on the internet, but none describes how to apply those patterns with Typescript. Also upcoming version of TS 2.8 brings new exciting features to the table, like conditional types, new predefined conditional types within standard library, homomorphic mapped types modifiers…, which allows us to create easily common react patterns in type safe way…
This post is gonna be quite long, so please just sit back and relax, while you master Ultimate React Component Patterns with Typescript!
all patterns/examples use typescript 2.8 and strict mode
Start
First off we need to install typescript and tslib helpers so our emitted code is smaller
yarn add -D typescript@next
# tslib will be leveraged only for features that are not natively supported by your compile target
yarn add tslib
With that we can initialize our typescript config:
# this will create tsconfig.json within our project with sane compiler defaults
yarn tsc --init
Now let’s install react, react-dom and their type definitions.
# install react
yarn add react react-dom
# install type definitions
yarn add -D @types/{react,react-dom}
Great! Now let’s hop into those component patterns, shall we ?
Stateless Component
You guess it, those are components without state ( they are also called presentational ). Most of the time they are just pure functions. Let’s create contrived Button stateless compoennt with Typescript.
Like in vanilla JS we need to import React which allows us to use JSX
import React from 'react'
const Button = ({ onClick: handleClick, children }) => (
<button onClick={handleClick}>{children}</button>
)
Although tsc compiler will now emit errors! We need to be explicit and tell the component/function what is the type of our props. Let’s define our props:
import React, { MouseEvent, ReactNode } from 'react'
type Props = {
onClick(e: MouseEvent<HTMLElement>): void
children?: ReactNode
}
const Button = ({ onClick: handleClick, children }: Props) => (
<button onClick={handleClick}>{children}</button>
)
We get rid of all errors! great! But we can do even better!
There is a predefined type within @types/react
=> type SFC<P>
which is just an alias of interface StatelessComponent<P>
and it has pre-defined children
and other things (defaultProps, displayName...), so we don't have to write it every time on our own!
So the final stateless component looks like this:
Stateless Component
Stateful Component
Let’s create stateful counter component which will leverage our Button
First of we need define initialState
const initialState = { clicksCount: 0 }
Now we will use Typescript to infer State type from our implementation.
By doing this we don’t have to maintain types and implementation separately, we have only source of truth, which is the implementation. nice !
type State = Readonly<typeof initialState>
Also note that type is explicitly mapped to have all properties read-only. We need to be explicit again and define use 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 = 2 this.state = { clicksCount: 2 }
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:
Compile time State type safety
Whole Container/Stateful component implementation:
Our Container doesn’t have any Props API so we need to type 1st generic argument of Component
as object
( because props
is always an object {}
in React) and use State
type as a 2nd generic argument.
Stateful Component
You’ve may noticed that we’ve extracted state update functions to pure functions outside the class. This is a 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-- }
)
// Will throw following complile error:
//
// [ts]
// Cannot assign to 'clicksCount' because it is a constant or a read-only property.
Cool right ?
Default Props
Let’s extend our Button component with an color prop of type string.
type Props = {
onClick(e: MouseEvent<HTMLElement>): void
color: string
}
If we wanna define defaultProps we can do it via Button.defaultProps = {...}
on our component.
By doing that we need to change our Props type definition to mark props that are default as optional.
So something like this ( notice the ?
operator )
type Props = {
onClick(e: MouseEvent<HTMLElement>): void
color?: string
}
and our Component looks like this:
const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
)
While this works for this simple example, there is one gotcha. Because we are in strict mode, optional props are union of type undefined | string
for our color
.
Let’s say we would like to do something with that particular prop, TS would throw an error because it doesn’t know, that it is defined by Component.defaultProps
React construct.
Default Props issue
To satisfy TS compiler we can use 3 techniques:
- use Bang operator to explicitly tell compiler that this won’t be
undefined
within our render, although it is optional, like this:<button onClick={handleClick!}>{children}</button>
- use conditional statements/ternary operator to make compiler understand that some particular prop is not undefined:
<button onClick={handleClick ? handleClick: undefined}>{children}</button>
- create reusable
withDefaultProps
High order function, which will update our props type definition and will set our default props. This is the most clean solution, IMHO
We can implement our High order function very easily ( thanks to new TS 2.8 conditional predefined mapped types ):
withDefaultProps High order function generic helper
Now we can use our withDefaultProps
High order function to define our default props, this will also solve our previous problem:
Define default props on our Button component
Or directly inline ( note that we need to explicitly provide original Button Props type, as TS is not able to infer argument types from function):
Define default props on inline with Component implementation
Now Button Props are defined correctly for consumption, defaultProps are reflected and marked as optional within our type definition but stays required within implementation !
{
onClick(e: MouseEvent<HTMLElement>): void
color?: string
}
And usage remains the same:
render(){
return (
<ButtonWithDefaultProps
onClick={this.handleIncrement}
>
Increment
</ButtonWithDefaultProps>
)
}
And yes this works also for Components defined via class
( also note, that thanks to structural origin of classes in TS, we don't have to specify explicitly our Prop
generic type).
It looks like this:
Define default props on inline class with Component implementation
And again usage remains the same:
render(){
return (
<ButtonViaClass
onClick={this.handleIncrement}
>
Increment
</ButtonViaClass>
)
}
Let’s say you need to build an expandable menu component that shows some children content when user clicks on it. We will implement this behaviour via various React Component Patterns.
Render Callbacks/Render Props pattern
The best way to make a component logic reusable is by turning your component children into a function or leveraging render
prop API — that’s why Render Callbacks are also called Function as Child Components.
Let’s implement a Toggleable
component with render props functionality:
Toggleable component with RenderProps/Children as a function pattern
Huh quite loot is happening in there right?
Let’s take a closer look to each important part of our implementation:
const initialState = { show: false }
type State = Readonly<typeof initialState>
- here we are declaring our state as in previous examples, nothing new
Now we need to define our component props ( note that we are using Partial mapped type, as we know that all props are gonna be optional, instead of annotating every prop manually by ?
operator ):
type Props = Partial<{
children: RenderCallback
render: RenderCallback
}>
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element
type ToggleableComponentProps = {
show: State['show']
toggle: Toggleable['toggle']
}
We wanna support both function as a child and render prop function, so both need to be optional. To makes things DRY , we’re creating RenderCallback
type of our render function definition:
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element
What may look strange to our readers eye, is our last type alias, the
type ToggleableComponentProps
!
type ToggleableComponentProps = {
show: State['show']
toggle: Toggleable['toggle']
}
Again we are using the power of typescript and lookup types , so we don’t have to repeat ourselves when defining types:
show: State['show']
we are creating ourshow
prop type by leveraging existing type definition within our statetoggle: Toggleable['toggle']
we are leveraging type inference and structural nature of classes within TS by getting the type from our method implementation! nice and indeed powerful!
The rest of the implementation is straightforward, standard render props/children as a function pattern:
export class Toggleable extends Component<Props, State> {
// ...
render() {
const { children, render } = this.props
const renderProps = { show: this.state.show, toggle: this.toggle }
if (render) {
return render(renderProps)
}
return isFunction(children) ? children(renderProps) : null
}
// ...
}
Now we can pass a function as children to Toggleable component:
Toggleable component with children as a function
or we can pass a function to render prop:
Toggleable component with render prop
Thanks to Typescript we got also intellisense at our disposal and proper type checking of our render prop arguments:
Complete soundness of our Toggleable component. Thank you Typescript !
If we want to reuse it (for multiple menus or so), we could simply create a new component that uses Toggleable logic:
ToogleableMenu component created with Toggleable
Our brand new ToggleableMenu component is ready to be used within Menu component:
Menu Component
And it works as expected:
Demo with ToggleableMenu components
This approach is really useful when we want to change the rendered content itself regardless of state manipulation: as you can see, we’ve moved our render logic to our ToggleableMenu children function, but kept the state logic to our Toggleable component!
Component Injection
To make our component even more flexible, we can introduce Component Injection pattern.
What is Component Injection pattern? If you’re familiar with React-Router, you are using this pattern when defining route definition via :
<Route path="/foo" component={MyView} />
So instead of passing a function via render/children props, we are “injecting” Component via component
prop. For this to work, we can refactor our inline render prop function to a reusable stateless component:
import { ToggleableComponentProps } from './toggleable'
type MenuItemProps = { title: string }
const MenuItem: SFC<MenuItemProps & ToggleableComponentProps> = ({
title,
toggle,
show,
children,
}) => (
<>
<div onClick={toggle}>
<h1>{title}</h1>
</div>
{show ? children : null}
</>
)
With that we can refactor our ToggleableMenu
with render prop to:
type Props = { title: string }
const ToggleableMenu: SFC<Props> = ({ title, children }) => (
<Toggleable
render={({ show, toggle }) => (
<MenuItem show={show} toggle={toggle} title={title}>
{children}
</MenuItem>
)}
/>
)
Now with that done, let’s define our new API — component
prop.
We need update our props API.
children
can be now function or ReactNode ( when component prop is used)component
is our new API which accepts component that needs to implementToggleableComponentProps
on it's props and it needs to be generic and set toany
, so if arbitrary component that implements other properties thanToggleableComponentProps
will pass TS validaionprops
we are introducing props property for passing down arbitrary props, this is a common pattern. It is defined as index type with any type, so we are loosing here strict type safety...
// We need create defaultProps with our arbitrary prop type -> props which is gonna be empty object by default
const defaultProps = { props: {} as { [name: string]: any } }
type Props = Partial<
{
children: RenderCallback | ReactNode
render: RenderCallback
component: ComponentType<ToggleableComponentProps<any>>
} & DefaultProps
>
type DefaultProps = typeof defaultProps
Next we need to add new props API to our ToggleableComponentProps
, so consumer will be allowed to use props
prop on <Toggleable props={...}/>
:
export type ToggleableComponentProps<P extends object = object> = {
show: State['show']
toggle: Toggleable['toggle']
} & P
Now we need to update our render
method
render() {
const {
component: InjectedComponent,
children,
render,
props
} = this.props
const renderProps = {
show: this.state.show, toggle: this.toggle
}
// when component prop api is used children is ReactNode not a function
if (InjectedComponent) {
return (
<InjectedComponent {...props} {...renderProps}>
{children}
</InjectedComponent>
)
}
if (render) {
return render(renderProps)
}
// children as a function comes last
return isFunction(children) ? children(renderProps) : null
}
Whole implementation of Toogleable component with Render Props, Children as a Function, Component Injection with arbitrary props support:
Whole implementation of Toogleable component with Render Props, Children as a Function, Component Injection with arbitrary props support
Our final ToggleableMenuViaComponentInjection
component which leverages component
prop looks like this:
ToggleableMenu with component injection pattern
NOTE though, that we have no type safety within our arbitrary custom props
prop, because it's defined as indexed object map { [name: string]: any }
We can pass anything to our props prop
We can now use our ToggleableMenuViaComponentInjection
for menu rendering as before
export class Menu extends Component {
render() {
return (
<>
<ToggleableMenuViaComponentInjection title="First Menu">
Some content
</ToggleableMenuViaComponentInjection>
<ToggleableMenuViaComponentInjection title="Second Menu">
Another content
</ToggleableMenuViaComponentInjection>
<ToggleableMenuViaComponentInjection title="Third Menu">
More content
</ToggleableMenuViaComponentInjection>
</>
)
}
}
Generic Components
When we implemented “component injection pattern” we lost strict type safety on arbitrary props handled via props
. How can we fixes this? You guess it right! We can write our Toggleable
component as a generic Components!
First we need to make our props generic. We are using default generic parameters so we don’t have to provide it explicitly when we don’t want to ( for render props/children as a function).
type Props<P extends object = object> = Partial<
{
children: RenderCallback | ReactNode
render: RenderCallback
component: ComponentType<ToggleableComponentProps<P>>
} & DefaultProps<P>
>
We also need to update our ToggleableComponentProps
to be generic. Oh wait it already is . So no changes on this front.
What needs to be changed though is definition of type DefaultProps
as it is impossible to get generic type definition from implementation, we need to refactor it to old fashioned way type definition -> implementaion
type DefaultProps<P extends object = object> = { props: P }
const defaultProps: DefaultProps = { props: {} }
Almost done!
Now let’s make our component class generic. Again we are using default props so we don’t have to specify generic arguments when component injection is not used!
export class Toggleable<T = {}> extends Component<Props<T>, State> {}
That’s it ! That’s it??? Hmm, how can we leverage this generic type within our JSX?
Bad news, we can’t …
We need to introduce ofType
generic component factory pattern
export class Toggleable<T extends object = object> extends Component<Props<T>, State> {
static ofType<T extends object>() {
return Toggleable as Constructor<Toggleable<T>>
}
}
Whole implementation of Toogleable component with Render Props, Children as a Function, Component Injection with generic props support:
Whole implementation of Toogleable component with Render Props, Children as a Function, Component Injection with generic props support:
Now with static ofType
factory method, we can create our properly typed generic component
ToggleableMenu with component injection pattern and Generics
And everything will work as before, this time with proper type safety for our props={}
prop. High five !
Type safe arbitrary props prop thanks to generics !
High Order Components
Because we already created our Toggleable
component with render callback functionality, implementing HOC will be easy peasy. ( That's also one of the great advantages of render callback pattern, we can leverage it for HOC implementation)
Let’s implement our HOC:
We need to create:
- displayName ( so we get nice debugging within devtools)
- WrappedComponent ( so we can access original component — useful for testing )
- leverage
hoistNonReactStatics
fromhoist-non-react-statics
npm package
withToogleable Hoc implemented via Togglable
Now we can create our Toogleable menu item via HOC as well, with correct type safety of props!
ToggleableMenu implemented via HoC
And everything works and is covered by types as well ! yay!
Proper type annotation for our ToggleableMenu created with HoC
Controlled Components
We are in the finale ! Let’s say we wanna make our Toggleable
highly configurable by controlling it from parent. This is very powerful pattern indeed. Let's do this.
What I mean by Controlled Component ? I wanna controll if content is shown directly for all my ToggleableMenu
components from within Menu
component.
Our Menu component that can control ToggleableMenu components via props
We need to update our implementation of ToggleableMenu components like following:
Implementation of our ToggleableMenu via various patterns
Now with those updated we can add state to our Menu and propagate it down to ToggleableMenu
Stateful Menu component
Let’s update our Toggleable
one last time for ultimate power and flexibility
To make our Toggleable controlled component we need to do following:
- add
show
to ourProps
API - update default props ( because show is optional)
- update initial Component.state to be set from Props.show, because now the source of truth for setting our state may come via props from parent.
- componentWillReceiveProps Life cycle hook to properly update state from public props
1 & 2:
const initialState = { show: false }
const defaultProps: DefaultProps = { ...initialState, props: {} }
type State = Readonly<typeof initialState>
type DefaultProps<P extends object = object> = { props: P } & Pick<State, 'show'>
3 & 4:
export class Toggleable<T = {}> extends Component<Props<T>, State> {
static readonly defaultProps: Props = defaultProps
// Bang operator used, I know I know ...
state: State = { show: this.props.show! }
componentWillReceiveProps(nextProps: Props<T>) {
const currentProps = this.props
if (nextProps.show !== currentProps.show) {
this.setState({ show: Boolean(nextProps.show) })
}
}
}
Final Toggleable Component with support for all patterns ( Render Props/Children as Function/Component Injection/Generic types/Controllable )
Final withToggleable HoC via Toggleable
Just slight update -> we need to propagate show
prop value within our HoC and update our OwnProps
API
withToggleable Hoc with controllable functionality
Summary
Implementing proper type safe components with React and Typescript can be tricky. With new functionality added to Typescript 2.8, we have almost everything at our disposal to write type safe components by adhering to common React component patterns.
In this super long post ( sorry about that !) we learned how to implement components with various patterns in strict type safe way thanks to Typescript.
Most powerfull pattern overall is indeed Render Props, which allows us to implement other common patterns like Component Injection or HOC without much additional churn.
All demos can be found at my Github repo for this post.
Also it is very important to realise, that type safety within templates like demonstrated in this article, is possible only within libraries that use VDOM/JSX
- Angular templates with Language service provide type safety, but soundness fails on simple constructs like checking within ngFor etc…
- Vue has nothing like Angular implemented yet for templates, so their templates and data binding are just magical strings ( but this may change in the future. Although you can use VDOM for templates it’s cumbersome to use because various types of props definition ( “snabdom takes the blame…” ) )
As always ping me here or on twitter( my handle @martin_hotell) if you have any questions, beside that, happy type checking folks! Cheers!