What We’ve Learned Using React Hooks

Meshify
Meshify
Published in
7 min readJul 19, 2019

As web developers, why do we try to live on the bleeding edge of tech by constantly upgrading the packages that we use? Ideally, development would be contained to a tidy and foreseeable timeframe. Build a thing once, do it right and you should be done! Web developers can dream of such a world but this sits in stark contrast to the reality of the web development ecosystem.

Javascript frameworks and packages are constantly evolving to incorporate new features, bug-fixes, and security improvements over time. Keeping up with these features prevents obsolescence and *hopefully* reduces the complexity of your codebase. React is a quintessential example of a framework that is constantly evolving, releasing new features and fixes. React rarely makes breaking changes to their code — look at the Context API if you don’t believe me — so when the React team announced its plans to deprecate the class component and its lifecycle methods, developers should trust that upgrading has serious benefits.

On February 4th, 2019, the React team released hooks as React version 16.8. Around that time a fellow coworker and I began working at Meshify. Like many React developers, we kept our heads in the sand, weary of the work and the risks that accompany learning a new feature of the codebase. It is important to note that this is a completely fine thing to do; it is absolutely okay to take your time with hooks and by no means should one try to push code to production that they don’t understand.

My coworkers and I were lucky, because our early-stage app didn’t have any users yet, we had a significant amount of time to learn hooks with minimal risk involved. During this time we made a litany of beginner’s mistakes that had to be rectified. Furthermore, we invested heavily in learning tools that would help us stabilize and investigate the work that we created. A result of this process was the uncovering of three major problems that had to be overcome.

The first issue that became apparent was testing. Previously, we had been using Enzyme as a testing framework for React. Currently, Enzyme works when testing components using hooks. But, 4 months ago, it was completely unable to adequately test components with hooks. Fortuitously, this absence of functionality led us to React Testing Library.

The official @testing-library/react mascot
The official @testing-library/react mascot

React Testing Library follows a “behavior driven testing” approach. This approach lets us test the actual render target — the markup we end up with in the DOM. With Enzyme, we were testing actual implementation details, which ended up being a frequent source of distraction. Changes in component implementation details don’t necessarily imply that there will be a regression in the actual behavior we want, but will almost certainly guarantee a regression in our Enzyme tests.

The next serious concern that cropped up was verifying app behavior after the adoption of hooks. This was certainly a problem before hooks ever entered the fray. By adopting a tool that nobody on the team was familiar with, when something went wrong, it could often be blamed. To avoid these issues, we had to become familiar with debugging our tests and profiling our rendered views inside our app.

/* Put this code in <repo_directory_path>/.vscode/launch.json */{
"version": "0.2.0",
"configurations": [
{
"name": "Debug CRA Tests",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts",
"args": ["test", "--no-cache", "${relativeFile}"],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}

Debugging tests can be a very useful sanity check. By using this launch.json file in your project using Create-React-App, you can run through each render of a component in your React tests.

The profiler tutorial:

The React Profiler is a handy addition to any React developer’s ensemble of tools. The flame graph, direct inspection of component props, and component render timing helps verify that components are reacting to props and state in the fashion that we expect.

Around this time we also began to realize how important it is to understand the fundamentals of how hooks work. We started reading more blog posts for guidance and education, the blogs written by Kent C. Dodds and Dan Abramov were foundational to how we understood the implications of our work and opportunities for refactoring along the way. With these tools and learning resources we were able to gain understanding around the behavior of components, using hooks.

Unfortunately, as humans, we tend to make mistakes, even when using available resources. The third real problem was bugs we created for ourselves due to a lack of fundamental understanding. By upgrading our package version of `react-scripts` we were able to get access to the `rules-of-hooks` eslint rules. By having these rules evaluate our useEffect, useCallback, and useMemo dependency arrays, we can programmatically validate that any variable being referenced inside one of these hooks, that can change between renders, is listed as a dependency. Any change in dependency will cause the hook to be invalidated and rerun. This linter can expose and can become a source of problems. Often making changes from linter rules necessitates a thorough understanding of what variables a hook body is relying on, which is invariably a good thing.

In addition to the insight provided by dependency arrays, this linter exposed us to a very alarming fact that caused us plenty of refactoring pain. Our app is mostly composed of Formik forms, and those forms rely on child callback functions.

An example child callback function
An example child callback function

We had absolutely no idea that useEffects inside a child callback function would run on each render. Eventually, we found that in most cases these useEffects worked best when they were as close as possible to the component relying on the result of that useEffect. For example, a form field might rely on a change in selection for an adjacent field in the same form. By creating a component that wraps the field and reacts to the change in the adjacent component, the benefit is two-fold. The linter is satisfied because the useEffect is technically no longer called in a child callback function, and the logic for the reaction is directly adjacent to where the reaction is being rendered. Often, one might find that his makes work easier to comprehend and more reusable.

Similarly, we had no idea that subscription cleanup was a requirement for hooks that use promises. In-flight promises, animation frames, event listeners, and other global entities must be cleaned up if they are used in a hook. These constraints of hooks felt like the most unavoidable errors. We had no indication that cleanup tasks needed to be done until the errors showed up in our code.

Subscription Cleanup Example
Subscription Cleanup Example

These constant refactoring efforts with hooks were certainly arduous. A lot of this work felt avoidable; these pain points didn’t seem to be well documented at the time we were learning hooks. This is no indictment on anyone, but simply my way of expressing that if you have ever felt frustrated learning hooks you are not alone.

After going through the work of learning how hooks work, the benefits of adoption have been tremendous. Attached are a set of examples that are roughly based on some of the real-world uses we’ve had for hooks. Hooks Examples

The core reducer of our useTableState hook
The core reducer of our useTableState hook

As Dan Abramov explains in his blog post A Complete Guide to useEffect, hooks let functions participate in the data flow for the first time. All of our tables use the same useTableState hook to manage
“loading | error | loaded” states upon fetching new data. The benefits from using this one hook have been multi-faceted. By defining a useCallback function that is invalidated on changes to dependencies, we can use the same hook for tables that depend on pagination options, those that depend on a currently logged in user that is subject to change, some that depend on both possibilities, or neither. In each case, we can use the same hook that accepts a useCallback function that is memoized in whatever way we determine. This hook is flexible and reusable across the entirety of our app!

These benefits are too hard to ignore, and ultimately we have been delighted by the transition to using hooks. The effort has been non-trivial and incremental, we still have yet to move all our work over to using functional components and hooks. This lack of complete overhaul is a feature, not a bug! Incremental adoption has made the entire process feasible. Ultimately, we are glad that we didn’t functionalize everything at once before realizing the entire scope of what we were doing. We encourage everyone to take the time to learn hooks, the future of React is bright and exciting to us at Meshify.

John Detlefs is a Frontend Developer at Meshify.

Meshify is an Austin, TX-based IoT hardware & software company for the insurance industry providing innovative IoT solutions with a focus on simple installation, actionable insights, and cost-effectiveness.

--

--

Meshify
Meshify

We are an IoT hardware & software company providing innovative IoT solutions with a focus on simple installation, actionable insights, and cost-effectiveness.