Sitemap
Level Up Coding

Coding tutorials and news. The developer homepage gitconnected.com && skilled.dev && levelup.dev

Follow publication

Ultimate React Component Patterns with Typescript 2.8

15 min readFeb 28, 2018

--

Start

yarn add -D typescript@next
# tslib will be leveraged only for features that are not natively supported by your compile target
yarn add tslib
# this will create tsconfig.json within our project with sane compiler defaults
yarn tsc --init
yarn add react react-dom
yarn add -D @types/{react,react-dom}

Stateless Component

import React from 'react'const Button = ({ onClick: handleClick, children }) => (
<button onClick={handleClick}>{children}</button>
)
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>
)
Zoom image will be displayed
Stateless Component

Stateful Component

const initialState = { clicksCount: 0 }
type State = Readonly<typeof initialState>
readonly state: State = initialState
this.state.clicksCount = 2 
this.state = { clicksCount: 2 }
Zoom image will be displayed
Compile time State type safety
Zoom image will be displayed
Stateful Component
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.

Default Props

type Props = { 
onClick(e: MouseEvent<HTMLElement>): void
color: string
}
type Props = { 
onClick(e: MouseEvent<HTMLElement>): void
color?: string
}
const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
)
Zoom image will be displayed
Default Props issue
Zoom image will be displayed
withDefaultProps High order function generic helper
Zoom image will be displayed
Define default props on our Button component
Zoom image will be displayed
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
}
Zoom image will be displayed
render(){
return (
<ButtonWithDefaultProps
onClick={this.handleIncrement}
>
Increment
</ButtonWithDefaultProps>
)
}
Zoom image will be displayed
Define default props on inline class with Component implementation
render(){
return (
<ButtonViaClass
onClick={this.handleIncrement}
>
Increment
</ButtonViaClass>
)
}

Render Callbacks/Render Props pattern

Zoom image will be displayed
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>
type Props = Partial<{
children: RenderCallback
render: RenderCallback
}>
type RenderCallback = (args: ToggleableComponentProps) => JSX.Elementtype ToggleableComponentProps = {
show: State['show']
toggle: Toggleable['toggle']
}
type ToggleableComponentProps = { 
show: State['show']
toggle: Toggleable['toggle']
}
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
}
// ...
}
Zoom image will be displayed
Toggleable component with children as a function
Zoom image will be displayed
Toggleable component with render prop
Complete soundness of our Toggleable component. Thank you Typescript !
Zoom image will be displayed
ToogleableMenu component created with Toggleable
Zoom image will be displayed
Menu Component
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

<Route path="/foo" component={MyView} />
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}
</>
)
type Props = { title: string }
const ToggleableMenu: SFC<Props> = ({ title, children }) => (
<Toggleable
render={({ show, toggle }) => (
<MenuItem show={show} toggle={toggle} title={title}>
{children}
</MenuItem>
)}
/>
)
// 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
export type ToggleableComponentProps<P extends object = object> = {
show: State['show']
toggle: Toggleable['toggle']
} & P
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
}
Zoom image will be displayed
Whole implementation of Toogleable component with Render Props, Children as a Function, Component Injection with arbitrary props support
Zoom image will be displayed
ToggleableMenu with component injection pattern
Zoom image will be displayed
We can pass anything to our props prop :(
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

type Props<P extends object = object> = Partial<
{
children: RenderCallback | ReactNode
render: RenderCallback
component: ComponentType<ToggleableComponentProps<P>>
} & DefaultProps<P>
>
type DefaultProps<P extends object = object> = { props: P }
const defaultProps: DefaultProps = { props: {} }

Almost done!

export class Toggleable<T = {}> extends Component<Props<T>, State> {}
export class Toggleable<T extends object = object> extends Component<Props<T>, State> {
static ofType<T extends object>() {
return Toggleable as Constructor<Toggleable<T>>
}
}
Zoom image will be displayed
Whole implementation of Toogleable component with Render Props, Children as a Function, Component Injection with generic props support:
Zoom image will be displayed
ToggleableMenu with component injection pattern and Generics
Zoom image will be displayed
Type safe arbitrary props prop thanks to generics !

High Order Components

Zoom image will be displayed
withToogleable Hoc implemented via Togglable
Zoom image will be displayed
ToggleableMenu implemented via HoC

And everything works and is covered by types as well ! yay!

Zoom image will be displayed
Proper type annotation for our ToggleableMenu created with HoC

Controlled Components

Our Menu component that can control ToggleableMenu components via props
Zoom image will be displayed
Implementation of our ToggleableMenu via various patterns
Zoom image will be displayed
Stateful Menu component
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'>
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 )

Zoom image will be displayed

Final withToggleable HoC via Toggleable

Zoom image will be displayed
withToggleable Hoc with controllable functionality

Summary

--

--

Level Up Coding
Level Up Coding
Martin Hochel
Martin Hochel

Written by Martin Hochel

Principal Engineer | Google Dev Expert/Microsoft MVP | @ngPartyCz founder | Speaker | Trainer. I 🛹, 🏄‍♂️, 🏂, wake, ⚾️& #oss #js #ts

Responses (33)