TypeScript and React: Hooks

Hooks have been announced at React Conf 2018. Check out this page for more
details. I think they’re pretty awesome. Probably game-changing! Hooks heave formerly “stateless” functional components to
… basically everything traditional class components can be. With a much cleaner API!

Just quickly after their release in React 16.7., React typings in DefinitelyTyped got an update as well. Check out how you
can use hooks with TypeScript!

useState

useState is probably one you are going to use a lot. Instead of using this.state from class components, you can access the
current state of a component instance, and initialise it, with one single function call. Our desire for strong typing is that
values we initially set, get per component update, and set through events, always have the same type. With the provided
typings, this works without any additional TypeScript:

// import useState next to FunctionComponentimportReact,{FunctionComponent,useState}from'react';// our components props accept a number for the initial valueconstCounter:FunctionComponent<{initial?:number}>=({initial=0})=>{// since we pass a number here, clicks is going to be a number.// setClicks is a function that accepts either a number or a function returning// a numberconst[clicks,setClicks]=useState(initial);return<><p>Clicks:{clicks}</p>
<buttononClick={()=>setClicks(clicks+1)}>+</button>
<buttononClick={()=>setClicks(clicks-1)}>+</button>
</>
}

And that’s it. Your code works with out any extra type annotations, but still typechecks.

useEffect

useEffect is here for all side effects. Adding event listeners, changing things in the document, fetching data.
Everything you would use component lifecycle methods for (componentDidUpdate, componentDidMount, componentWillUnmount)
The method signature is pretty straightforward. It accepts two parameters:

A function that is called without any parameters. This is the side-effect you want to call.

An array of values of type any. This parameter is optional. If you don’t provide it, the function provided is called
every time the component update. If you do, React will check if those values did change, and triggers the function only
if there’s a difference.

You don’t need to provide any extra typings. TypeScript will check that the method signature of the function
you provide is correct. This function also has a return value (for cleanups). And TypeScript will check that you provide
a correct function as well:

useContext

useContext allows you to access context properties from anywhere in your components. Much like the Context.Consumer
does in class components. Type inference works brilliantly here, you don’t need to use any TypeScript specific language
features to get everything done:

useRef

useRef is nice as you can set references directly in your function components. However, this was the first time I found
hooks together with TypeScript a bit tricky! When you are in strict mode, TypeScript might complain:

importReact,{useRef}from'react';functionTextInputWithFocusButton(){// it's common to initialise refs with nullconstinputEl=useRef(null);constonButtonClick=()=>{// ⚡️ TypeScript in strict mode will complain here, // because inputEl can be null!inputEl.current.focus();};return(<><inputref={inputEl}type="text"/><buttononClick={onButtonClick}>Focustheinput</button>
</>
);}

Here’s what bugs us:

usually we initialise refs with null. This is because we set it later in our JSX calls

with the initial value of a ref being null, inputEl might be null. TypeScript complains that you should do a strict
null check.

That’s not the only thing. Since TypeScript doesn’t know which element we want to refer to, things like current and
focus() will also probably be null. So our strict null checks are pretty elaborate. We can make this a ton easier
for us and for TypeScript, when we know which type of element we want to ref. This also helps us to not mix up element types
in the end:

functionTextInputWithFocusButton(){// initialise with null, but tell TypeScript we are looking for an HTMLInputElementconstinputEl=useRef<HTMLInputElement>(null);constonButtonClick=()=>{// strict null checks need us to check if inputEl and current exist.// but once current exists, it is of type HTMLInputElement, thus it// has the method focus! ✅if(inputEl&&inputEl.current){inputEl.current.focus();}};return(<>{/* in addition, inputEl only can be used with input elements. Yay! */}<inputref={inputEl}type="text"/><buttononClick={onButtonClick}>Focustheinput</button>
</>
);}

A bit more type safety for all of us ❤️

useMemo - useCallback

You know from useEffect that you can influence the execution of certain functions by passing some parameters to it.
React checks if those parameters have changed, and will execute this function only if there’s a difference.

useMemo does something similar. Let’s say you have computation heavy methods, and only want to run them when their parameters
change, not every time the component updates. useMemo returns a memoized result, and executes the callback function only
when parameters change.

To use that with TypeScript, we want to make sure that the return type from useMemo is the same as the return type from
the callback:

/** * returns the occurence of if each shade of the * red color component. Needs to browse through every pixel * of an image for that. */functiongetHistogram(image:ImageData):number[]{// details not really necessary for us right now 😎...returnhistogram;}functionHistogram(){.../* * We don't want to run this method all the time, that's why we save * the histogram and only update it if imageData (from a state or somewhere) * changes. * * If you provide correct return types for your function or type inference is * strong enough, your memoized value has the same type. * In that case, our histogram is an array of numbers */consthistogram=useMemo(()=>getHistogram(imageData),[imageData]);}

The React typings are pretty good at that, so you don’t have to do much else.

useCallback is very similar. In fact, it’s a shortcut that can be expressed with useMemo as well. But it returns a
callback function, not a value. Typings work similar:

The key here is: Get your typings right. The React typings do the rest.

useReducer

Now this is something, isn’t it? The core of Redux and similar state management libraries baked into a hook. Sweet and
easy to use. The typings are also pretty straightforward, but let’s look at everything step by step. We take the example
from the website, and try to make it type safe.

useReducer accepts a reducer function and an initial state. The reducer function switches the action.type
property and selects the respective action. Nothing new. It’s just that right now, everything is of type any.
We can change that.

The useReducer typings are nice as you don’t have to change anything in the usage of useReducer, but can control
everything via type inference from the reducer function. Let’s start by making the actions more type safe. Here’s what we
want to avoid:

listening to actions that are not reset, increment or decrement

Making sure that the type property is set.

For that, we create an ActionType type definition. We use union types to make sure that type can only be of
reset, increment or decrement.

typeActionType={type:'reset'|'decrement'|'increment'}constinitialState={count:0};// We only need to set the type here ...functionreducer(state,action:ActionType){switch(action.type){// ... to make sure that we don't have any other strings here ...case'reset':returninitialState;case'increment':return{count:state.count+1};case'decrement':return{count:state.count-1};default:returnstate;}}functionCounter({initialCount=0}){const[state,dispatch]=useReducer(reducer,{count:initialCount});return(<>Count:{state.count}{/* and can dispatch certain events here */}<buttononClick={()=>dispatch({type:'reset'})}>Reset</button>
<buttononClick={()=>dispatch({type:'increment'})}>+</button>
<buttononClick={()=>dispatch({type:'decrement'})}>-</button>
</>
);}

That’s not much to do, to make our actions type safe. If you want to add another action, do it at your type declaration.
It’s the same with the state. The useReducer typings infer state types from the reducer function:

typeStateType={counte:number}functionreducer(state:StateType,action:ActionType){...}functionCounter({initialCount=0}){// ⚡️ Compile error! Strings are not compatible with numbersconst[state,dispatch]=useReducer(reducer,{count:'whoops, a string'});...// ✅ All goodconst[state,dispatch]=useReducer(reducer,{count:initialCount});...}

I think hooks are exciting. I also think that TypeScript’s great generics and type inference features are a perfect
match to make your hooks type safe, without doing too much. That’s TypeScript’s greatest strength: Being as little
invasive as possible, while getting the most out of it.