Mostly for fun, I decided to upgrade an old project of mine to current versions of React using React hooks.

Code for the project: https://github.com/ajcrites/times-tables

Live project: https://times-tables.explosionpills.com/

This is a fairly simple React app that displays and animates a times table visualization. It’s broken down into fundamentally three components: The app container that maintains the state, the visualization itself, and the controls for manipulating the visualization.

Update: This article was originally written when React hooks were in alpha, hence the following paragraph. All code in this article has been tested with the latest stable version of React (which includes hooks) that you can install simply with yarn add react react-dom .

My first step was to update React to the version with hooks. This is currently done via yarn add --exact react@next react-dom@next since hooks are still in alpha. Note that the --exact here is recommended because in your package.json if you have "^16.8.0-alpha.1" , yarn/npm may install v16 rather than the alpha version. Using "16.8.0-alpha.1" avoids this potential pitfall.

Using Parcel

I had created this project using Create React App with the now-outdated react-scripts-ts . Some changes that I made caused the app to no longer compile. I have wanted to use parcel for a long time, so I bit the bullet and updated my project to use it.

I was pretty overwhelmed by how handy parcel was. All I had to do was move public/index.html to src/index.html and also update it to include <script src="./index.tsx"> . Parcel does practically everything else with yarn parcel serve src/index.html . Using an analogous yarn parcel build also works for production. I didn’t even need any special TypeScript configuration except for development and linting.

react-scripts also supports TypeScript now, but I haven’t tested it. I would recommend trying out parcel if you haven’t. It’s fast and convenient with almost no configuration and built-in, functional hot reloading. I’m also using parcel-plugin-static-files-copy to copy over some static files at build time.

Using Element Animations

While still experimental, there are now JavaScript APIs that allow you to call .animate on DOM elements to trigger animations. I strongly prefer this to what I was doing before — setting up CSS animations and updating styles in timeouts and using transitions. Using the .animate API is cleaner, simpler, and more straightforward.

The before code using CSS transitions may seem a bit simpler, but note that you have to handle the post-animation transition separately. You could also set state using something like the transitionend event, but I would find that to be almost as wonky as using the timeout.

The code using .animate may seem a bit more complex at first, but that’s just because we have the additional animation settings for duration and direction. I prefer the explicit single set of animation keyframes that you can choose to animate forward and back. Waiting for the animations to finish is also explicit. This is also superior in cases where you may have to Promise.all to wait for multiple animations to complete; otherwise you would have to do something like manage multiple transitionend , or choose the longest timeout to wait for.

Note that some or all of the web animation API is not available in all browsers, and it’s still an experimental technology. The web-animations-js polyfill library did exactly what I needed via import 'web-animations-js/web-animations-next-lite.min' . The library has other polyfill files depending upon your specific needs. I highly recommend it.

React Hooks

The component hierarchy of my project is structured like so:

<TimesTableApp>

<TimesTableVisualization />

<TimesTableControls />

<About />

</TimesTableApp>

The “controls” and “visualizations” also have some children for the UI for controls and the times table itself, respectively. It’s tough to think that I could get much reuse out of any of the components I’m using. It’s pretty much just HTML. Therefore, I didn’t put a lot of thought into the component hierarchy except to break things apart a bit and to try to keep like things together.

The application state I refer to includes three values:

times table

point count

line color

You can read about how this time table works on the site itself, but the “times table” is the number for the times table displayed (the 2 times table, the 3 times table, the 4.2 times table, etc.), “point count” is how many points are around the circle on the visualization for the times table to use, and “line color” is the color of the lines showing the multiplication. Here’s a gif of the visualization with 50 points animating from the 2 to the 10 times table .1 at a time. The line color changes every .5 times tables.

This could have been done using component composition — the visualization piece and the controls for the times table number, point number, and line color could all be at the top level. The older version of the app drills these down one level instead — TimesTableApp passes these values via its state and passes methods to manipulate the times table to the controls that are used by the inputs/buttons, etc.

My decision to upgrade to hooks was mostly experimental, but I also did some additional cleanup along the way. I’ll review each of the changes I went through step-by-step.

State and Refs

The simplest component to update was the “About” component. This is mostly just static information that has no dependency on app state. The only interesting thing is that it’s a modal that animates in and out using the web animations I showed above.

useRef with React hooks is really not any different than React.createRef for my needs here. Note that you can use useRef to maintain any mutable value for the component in lieu of this , but it can also still be used for the ref attribute for a component.

The more notable change here is that About has moved from a class component to a functional component. Hooks are for functional components. Otherwise, this contentRef is used in the same way except that we don’t use this. anymore.

We also need to maintain our state, show from earlier. This is merely used to determine whether or not the content is actually showing.

The example above includes the useRef from before for clarity. The addition is useState . The state hook may seem a bit unusual to the uninitiated — especially if you’re used to using this.state , but I will try to explain how it works.

useState returns an array with two values. The first is the value of the state property for the current render cycle. The second is a function that allows you to update this state value. Since we only have one value in this case, we don’t need an object like we do with state / setState . Instead, show is the boolean value. When we want to update it, we call setShow , a function that we could name whatever we wanted. I stuck with conventions.

If this is the first time you’ve ever seen useState at work, you may be wondering:

How does show get updated?

Remember that About is a component function. It is called each time it receives updates. Hooks update the component’s state — calling setShow triggers an update with the new show value. When this component function is called again, useState will return the new value from the updated state, and the component will be rendered accordingly. The argument to useState is an initial value used on the initial render.

Calling setShow updates show in the same way that this.setState({ show }) updates this.state.show . Hopefully, this is clear. If not, you might want to try updating some simple, stateful components in a project of yours to use useState instead to see how it works.

Effects and State Management

The next step in my update was to update the TimesTable visualization itself. This is a fairly simple component that does some math and draws to a canvas. The drawing is resized to match the window size on resize, and it redraws any time the times table number, point count, or line colors changes. These are all props to the TimesTable visualization.

There are several steps to complete the hooks update:

Low hanging fruit: useRef instead of createRef The lifecycle hooks ( component... methods) need to be replaced with useEffect useState must be used to trigger redrawing as forceUpdate does.

I found useEffect to be a little bit difficult to understand since it’s not a one-to-one transition from the lifecycle hooks. The useEffect hook will typically run on both mount and for any update.

useEffect takes a callback that handles your side-effect logic. You can optionally return a function from this callback that can tear down your side effect. “Side effects” can be broadly thought of as effects that run in a component outside of the component’s state. The 'resize' event on the browser window is an ideal example of a side effect.

This means three things for us:

An effect has to set the initial width/height. An effect has to add the 'resize' event listener on the window. An effect has to return a function to remove this event listener.

In order for the resize event to work properly, it has to update some component state to trigger a render… Otherwise, the canvas won’t be redrawn since resize doesn’t actually update any state or props. We can use an arbitrary useState for this. It may seem a bit odd, but you can think of this as using forceUpdate .

The first step will be to update for useRef .

I’ve left off most of the functionality for now. This simply sets the canvas ref.

<canvas ref={this.canvasRef} is changed to <canvas ref={canvasRef} . Otherwise this is handled the same way as before.

One thing astute readers may notice is that we don’t have something like const canvas = canvasRef.current at the top level (component function’s scope). Instead, we do this inside redraw . The reason for this is that if we do const canvas = canvasRef.current , canvas never gets re-evaluated inside the redraw method. It’s a closure around the initial canvas value and thus will be set to whatever the initial value was. In our case: null .

Instead, we need to use canvasRef.current any time we need to access whatever the canvasRef is for the current render cycle in a given scope. Note that const canvas = canvasRef.current works inside of our redraw function. This is because .current is called whenever redraw is called.

We can use useState to force an update to redraw the canvas when its width/height changes. If we don’t do this, the canvas is not redrawn because no state or props are updated. In an earlier version, I had kept track of the width/height in state for this effect. That works too, but I find this approach simpler.

The argument to forceUpdate above is required by TypeScript, but it can be anything. Calling the set function returned by useState will trigger a render regardless. You could also name this function whatever you want — it doesn’t have to be forceUpdate .

This leads into our use of useEffects which will make our initial call to redraw to trigger the initial draw with our canvas dimensions and set the resize events. The canvas ref will already be set to the <canvas> when the useEffect callback is called.

This took a bit for me to wrap my head around, but the first useEffect is called during the initial render and will then trigger another render because of redraw() . However, we have to wait for the canvas to be drawn to get access to the ref so we can set its dimensions, so this second render is appropriate.

The second argument to useEffect is used as a comparator — if none of the values in this array change, the useEffect callback is not called again. If you use [] , there are no values to compare to and the callback is only called once (on the initial render). There’s no special treatment for [] , it just won’t run the effects again since no comparator values have changed. This is desirable in our case since we only want to do an initial resize for the canvas, and we don’t need to add/remove the window listener multiple times as long as the visualization is displayed.

As strange as it may sound, in an earlier version of my update, the comparison == ultimately done by useEffects for comparing to old values took too long and caused flickering. In the actual project, I omitted the conditional check. It was actually more performant to render continuously than to do this check.

Using Context

This is something that I could have done without hooks, but there is a convenient useContext hook that I can take advantage of here.

Rather than have TimesTableApp explicitly pass the application state data between the sibling visualization and controls, I thought it would be simpler to have a context that would allow the visualization/controls to retrieve and update state properties as needed. This does simplify things quite a bit, but as suggested in React documentation this creates a coupling of the visualization and controls with the context. Since I neither plan to nor see how I could make these components reusable across projects, I think this is a fine compromise.

TimesTableApp still needs to manage the context, so it needs methods that will update the corresponding state properties in addition to the properties themselves.

The {} as any is used to avoid redundancy for having to set initial values in the context as well as the TimesTableApp that manages the top level context. Using initial values, { pointCount: 0, ... } etc. would have also been fine.

For my next step, I wanted to update the controls and visualization to use the context. Rather than update the TimesTableApp to use hooks, I started out sticking with this.state and passing down context instead of the individual state values (times table, point count, and line color).

The visualization and controls can now consume these values. The times table controls have a “play” button that increments the times table. We can update the times table with the setTimesTable context method.

The useRef above is used to keep track of the interval timer. Remember that useRef can be used to keep track of any mutable value, and the interval timer will change any time we play again.

Hopefully the above code looks sensible, but we have a bit of a problem. If you remember what I said about const canvas = canvasRef.current , closures will use whatever value was evaluated when the closure was created. This means that timesTable inside of the setInterval callback will always be the initial value ( 2 ), and the times table will never increment beyond 2.1 .

Instead, we need a way to get access to what the timesTable value is in context when we need to read it. Using useContext anywhere other than the component function scope is not allowed, so we can’t use it inside of setInterval . Also, doing something like const ctx = useContext() and then ctx.timesTable does not work either since ctx is a new object on each render rather than a reference to an initially created object.

We can take a hint from React’s setState and allow our state updating functions to take a callback that provides the current context value. We can update the existing changeValue function in TimesTableApp to do this:

This ensures that we’ll have the correct times table value whenever the setInterval callback runs regardless of where it was set. For example, our controls can also have an input with onChange={({ target: { value } } => setTimesTable(+value)} , so setting a value rather than using a callback still works.

We can also update the visualization to use context rather than props. The change here is minimal, and since they both come from the same source (application state) it’s arguable whether this is necessary or desirable. However, I’ve done it for consistency.

Simply const props = useContext(TimesTableContext) should work fine, and you can also pass the props explicitly. I think this is mostly down to preference.

Top Level State Management

The final change is to update TimesTableApp to use hooks. This is fairly simple since we can move from this.state to useState

This actually isn’t a simplification… other than some stylistic things this is exactly what I changed TimesTableApp to. You’ll notice that the changeValue function factory is no longer needed. Instead, the set* functions from useState conveniently cover these needs for us since you can also pass a callback to the set* functions that provides the current value. That is, we can still use setTimesTable inside of the controls component without any changes.

My current application changes both the times table and line color while playing. This leads to nested callbacks for updating state since line color depends on both current context values. I think that this is a bit wonky, and I would prefer a mechanism for managing multiple state values although that may simply be a single useState with an object of values rather than individual useState s… but that also makes updating individual values more complex.

Conclusion

Parcel, web-animations-js , and React hooks are all great. It had been a long time since I had updated this project, so some changes that I made such as moving to a React context didn’t require hooks to get involved. This makes it a bit more difficult to see whether some of the arguable improvements to my code came strictly from hooks or from another paradigm I could have used with React classes.

I’m reasonably sold on hooks, although I don’t see substantial advantages over using React classes yet except for the potential to create reusable hooks. The React team has argued that because this in JavaScript in general is difficult to grasp, and that not everyone who may want to use React comes from an object-oriented background with respect to state management, hooks will lower the barrier of entry for some developers. This may be true, but I think that they still bring their own challenges such as requiring a better understanding of function scope. I think that use of hooks could quickly become unwieldy as well… You don’t have an enforced single-state object and lifecycles, but you can manage state, lifecycles, and scope values in a variety of ways. What makes const val = useRef() better than this.val ?

I know that hooks have been made to be largely backwards compatible, so that may put some limitations on what they can do. I don’t see hooks as solving any new problems, although I think that’s liable to change once they are more widely adopted. I look forward to seeing improved and sensible state management patterns using hooks since I think this is their greatest weakness right now.

Also, why are comparison operators apparently so slow in JavaScript?