Simple Steps to a Typescript Class Decorator
Tags: typescript, react, context, tutorial
Published: 2018-08-15 14:01:20

---
title: Simple Steps to a Typescript Class Decorator
published: true
description: 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 articles I've found to be 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.
tags: Typescript,React,Context,Tutorial
---

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!):
    {% link https://dev.to/sreisner/understanding-the-react-context-api-through-building-a-shared-snackbar-for-in-app-notifications-8j2 %}
- A slightly deeper dive: [Introducing the React Context API by Leigh Halliday](https://www.leighhalliday.com/introducing-react-context-api)
- And of course, the well written: [Context on the official React documentation](https://reactjs.org/docs/context.html)

## 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:

```tsx
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](https://www.typescriptlang.org/docs/handbook/generics.html)

With that implemented, you could use your decorator on classes that need to be modified with an `@`:
```tsx
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.

```tsx
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:

```tsx
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](https://reactjs.org/docs/context.html#accessing-context-in-lifecycle-methods) 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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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](https://parceljs.org/):

{% github phillipmalboeuf/simple-steps-to-a-typescript-class-decorator %}