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 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:
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.
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:
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:
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:
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*asReactfrom'react'import{RouteComponentProps}from'react-router-dom'import{withStoreContext}from'./context'interfaceHomepagePropsextendsRouteComponentProps<any>{}interfaceHomepageState{cats:CatProps[]}exportclassPetsextendsReact.Component<HomepageProps,HomepageState>{constructor(props:HomepageProps){super(props)this.state={cats:[{id:1,name:'Garfield'},{id:2,name:'Mufasa'}]}}publicrender(){return<>{this.state.cats.map(cat=><Catkey={cat.id}{...cat}/>)}<Cart/></>}}interfaceStoreContextProps{context?:{cart:{catId:number}[],addPetToCart:(catId:number)=>void}}interfaceCatPropsextendsStoreContextProps{id:number,name:string}interfaceCatState{}@withStoreContextexportclassCatextendsReact.Component<CatProps,CatState>{constructor(props:CatProps){super(props)this.state={}}publicrender(){return<div><strong>{this.props.name}</strong><buttononClick={()=>this.props.context.addPetToCart(this.props.id)}>Add to Cart</button></div>}}interfaceCartPropsextendsStoreContextProps{}interfaceCartState{}@withStoreContextexportclassCartextendsReact.Component<CartProps,CartState>{constructor(props:CartProps){super(props)this.state={}}publicrender(){return<ol>{this.props.context.cart.map((item,index)=><likey={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:
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.