DEV Community

Phillip Malboeuf
Phillip Malboeuf

Posted on

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

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
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 {}
})
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 ComponentClasss 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>
  }
}
Enter fullscreen mode Exit fullscreen mode

You'll notice the existence of a BrowserRouter. I've simply found that having components deep in Routes 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>
  }
}

Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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)