I've been struggling with the implementation of a class decorator for my React component classes. I needed it to be able to consume a Context within the lifecycle methods of my components. If you're unsure as to how the React 16.3 Context API works, I've compiled a list of the articles I've found to be the most accessible on the subject. And if you're familiar with the API, please read on to explore the different pieces of a Typescript class decorator.
A list of articles on the React Context API
- An approachable article with a clear use case (and snacks!):
- A slightly deeper dive: Introducing the React Context API by Leigh Halliday
- And of course, the well written: Context on the official React documentation
Now, a Typescript Class Decorator
A class decorator is used to modify the constructor function of a class. In other words you can conveniently mess with the instantiation phase of a class instance. So a class decorator in Typescript (that does nothing) may have this structure:
export function decorator<C>(ClassDefinition: C): C {
return ClassDefinition
}
Where C
is the generic type of the ClassDefinition. Flip through the Generics docs if you haven't encountered them before: Generics on the official React documentation
With that implemented, you could use your decorator on classes that need to be modified with an @
:
import { decorator } from './decorator'
@decorator
export class Cat {}
Next, a React Component Class Decorator
So our goal is to modify a React Component class to make it consume a context, right. First, let's rename a few things and add the React.ComponentClass
type definition.
import * as React from 'react'
export function withContext<C extends React.ComponentClass>(Component: C): C {
return Component
}
Now Typescript recognizes C
as a ComponentClass.
You: but DUDE where's our Consumer? Me: we're getting there! Let's instantiate our context first.
Initializing a Context
Going forward, we'll use a "pet store" as our example use case with a "cart" and the functionality to add cats to the cart. So, our context will have an array of cats called cart
and a function called addPetToCart
. Creating a context in Typescript requires you to provide default values including their type definitions. Here then is our context initialization:
import * as React from 'react'
export const StoreContext = React.createContext({
cart: [] as {catId: number}[],
addPetToCart: (catId: number)=> function(): void {}
})
Writing our Context Decorator
Okay, we've finally been writing some React. And the React docs are telling us that the best way to consume a context is to implement an HOC and pass it to the child component in its props. So following our StoreContext
initialization we may write a StoreContext.Consumer
stateless component as our decorator, as so:
import * as React from 'react'
export const StoreContext = React.createContext({
cart: [] as {catId: number}[],
addPetToCart: (catId: number)=> function(): void {}
})
export function withStoreContext<C extends React.ComponentClass>(Component: C): C {
return (props => <StoreContext.Consumer>
{context => <Component {...props} context={context} />}
</StoreContext.Consumer>) as any as C
}
Reading this, you might be yelling at me: what is this as any as C
garbage!? The issue is that yes a class decorator is supposed to return a class definition, and Typescript is not ok with us trying to return a function instead. But React on the other hand accepts both ComponentClass
s and StatelessComponents
interchangeably. Therefor I'm satisfied with force casting this SFC
function to our C
class definition.
Writing our Provider Component Class
Let's set aside our decorator for now and let's move on to write our StoreContext.Provider
component. Here is where we may implement the functionalities of our pet store: put the cart
in a component's state and define an addPetToCart
function. As so:
import * as React from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import { StoreContext } from './context'
import { Pets } from './pets'
interface StoreProps {}
interface StoreState {
cart: {catId: number}[]
}
export class Store extends React.Component<StoreProps, StoreState> {
constructor(props: StoreProps) {
super(props)
this.state = {
cart: []
}
}
public addPetToCart(catId: number): void {
this.setState({ cart: [...this.state.cart, { catId }] })
}
public render() {
return <StoreContext.Provider value={{
cart: this.state.cart,
addPetToCart: this.addPetToCart.bind(this)
}}>
<BrowserRouter>
<Switch>
<Route path='/' component={Homepage} />
</Switch>
</BrowserRouter>
</StoreContext.Provider>
}
}
You'll notice the existence of a BrowserRouter
. I've simply found that having components deep in Route
s is a common use case for the application of the Context API. We're getting somewhere ladies and gentlemen!
Writing our Consumer Component Classes
For our final lap, we'll have a Homepage
component that renders in our cats
collection and a Cart
component. Since the Cat
component needs access to the addPetToCart
function from the store context and the Cart
will read the cart
array also from the store context, we give them both the withStoreContext
decorator:
import * as React from 'react'
import { RouteComponentProps } from 'react-router-dom'
import { withStoreContext } from './context'
interface HomepageProps extends RouteComponentProps<any> {}
interface HomepageState {
cats: CatProps[]
}
export class Pets extends React.Component<HomepageProps, HomepageState> {
constructor(props: HomepageProps) {
super(props)
this.state = {
cats: [{id: 1, name: 'Garfield'}, {id: 2, name: 'Mufasa'}]
}
}
public render() {
return <>
{this.state.cats.map(cat => <Cat key={cat.id} {...cat} />)}
<Cart />
</>
}
}
interface StoreContextProps {
context?: {
cart: {catId: number}[],
addPetToCart: (catId: number)=> void
}
}
interface CatProps extends StoreContextProps {
id: number,
name: string
}
interface CatState {}
@withStoreContext
export class Cat extends React.Component<CatProps, CatState> {
constructor(props: CatProps) {
super(props)
this.state = {}
}
public render() {
return <div>
<strong>{this.props.name}</strong>
<button onClick={()=> this.props.context.addPetToCart(this.props.id)}>Add to Cart</button>
</div>
}
}
interface CartProps extends StoreContextProps {}
interface CartState {}
@withStoreContext
export class Cart extends React.Component<CartProps, CartState> {
constructor(props: CartProps) {
super(props)
this.state = {}
}
public render() {
return <ol>
{this.props.context.cart.map((item, index)=> <li key={index}>{item.catId}</li>)}
</ol>
}
}
You'll find that it's useful to define a StoreContextProps
interface for our consumer components. This next step will be our last!
Let's Render into the DOM
All that is left is to ReactDOM.render
into a document element:
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Store } from './store'
ReactDOM.render(<Store />, document.getElementById('store'))
Questions & Comments
Thanks for reading this and don't hesitate if you have any questions or comments! And here's the complete repo running on Parcel:
Simple Steps to a Typescript Class Decorator
I've been struggling with the implementation of a class decorator for my React component classes. I needed it to be able to consume a Context within the lifecycle methods of my component. If you're unsure as to how the React 16.3 Context API works, I've compiled a list of the articles I've found to be the most accessible on the subject. And if you're familiar with the API, please read on to explore the different pieces of a Typescript class decorator.
Read the full article on dev.to!
Top comments (0)