Reflecting on Epic React

8 min readNov 27, 2024

I just wrapped up Kent C. Dodds’ course called Epic React. After a little under two years, I’ve made my way from React fundamentals to React Suspense through a gauntlet of exercises and projects. While some, if not most, of the curriculum was review by the time I started the course back in 2023, each module went in depth enough that I felt I was learning something new at each turn. In an effort to deepen the grooves of my learning, I thought I would revisit each section of the course and share one thing that I learned. Without further ado, here are my main takeaways from each module.

React Fundamentals

This section was a deep look under the hood of React, starting from the basics of building a webpage with Javascript, to building a page with “vanilla” React. It’s been a while since I looked at React without its JSX syntactic sugar, and it was helpful to review the fundamentals.

That this…

<body>
<div id="root"></div>
<script type="module">
const rootElement = document.getElementById('root')
const element = document.createElement('div')
element.textContent = 'Hello World'
element.className = 'container'
rootElement.append(element)
</script>
</body>

…is the same as this…

const rootElement = document.getElementById('root')
const ui = React.createElement('div', {className: 'container', children: 'Hello World'})
const root = ReactDOM.createRoot(rootElement)
root.render(ui)

…was nice to revisit. It was a useful reminder both that React rendering magic is javascript function calls under the hood, and that it’s a pleasure not to have to write all those function calls myself.

React Hooks

This section covered React’s most commonly used hooks. While I’ve used useState hundreds of times throughout my React career, it wasn’t until this module that I learned about lazy state initialization.

If a component contains a state value whose initial state is the result of some expensive computation, that computation can be pass as a callback function

This…

function Checklist() {
// Query localStorage each render to get the value below
const initialList = window.localStorage.getItem('list')
const [list, setList] = React.useState(initialList)
}

…could be refactored and optimized as this…

function Checklist() {
// Define the value below as a function that will only be called once
const initialList = () => window.localStorage.getItem('list')
const [list, setList] = React.useState(initialList)
}

In the above example, localStorage is only queried on initial render but not each subsequent re-render.

This may never come in handy as React is fast enough that users may not notice a difference in most cases. Often these optimizations may just distract from more important work, especially early on in the life of an app. That being said, knowledge is power, and understanding how these things work is the whole reason I took this course in the first place.

Advanced React Hooks

This section was a brain twister. The sections on useReducer, useCallback, and useContext were helpful basic explorations of concepts I had only seen demonstrated in complex situations at scale. Later sections, which covered less often used concepts, were more difficult in that I had trouble imagining a situation in which I would use those APIs. That being said, the concepts in this section have helped me in a number of ways.

For instance, seeing how useReducer lays the foundations for Redux stores, or saga-slice modules which we use at Zencare, has allowed me to understand the lifecycle of our app state and make better use of these state stores in order to write more maintainable code. Looking at basic useContext examples inspired me to think more carefully about state composition, especially as it pertains to the search functionality of our homepage. Lastly the section on useCallback, which I approached with a long stick while the mantra “avoid premature optimizations” echoed quietly in my head, helped me find a middle ground between memoizing nothing (which is often fine as React does a great job and handling these optimizations itself) and memoizing functions where obvious performance benefits could be made.

Advanced React Patterns

This sections implements the concepts mentioned in the above section in more advanced ways. Certain learning pieces that I mentioned in the paragraphs above get more attention in this section. Since I’ve already covered those concepts above, I’ll focus on a concept I learned a name for that sits at the gates to those more advanced implementations, the inversion of control.

Simply put, the inversion of control is when certain aspects of an abstraction are handed over to the end user to determine how said aspects should be implemented, rather than the abstraction author writing a use case for every possible implementation of an API.

Common examples of this are the Array.prototype.filter() and the Array.prototype.map() functions, where end users can pass a callback function to determine the behavior of the array method.

Now while this concept is interesting, it’s not one that I would use on a regular basis. My understanding is that this concept come more in handy when writing reusable functions to be consumed by other developers. While I may occasionally write a function that others at work may come along at use themselves, it seems far more likely that this concept would apply to library functions. That being said, the inversion of control is an example of AHA Programming, an additional new concept to me, and one that I will use on a weekly basis at work. the main takeaway here is that avoiding hasty abstractions can help future proof code. We don’t know what the future of our codebase holds and by writing code in a way that makes future iterations more approachable for any developer who happens to be working on them, we can build better products.

This, like all things in this realm, are a balance. I can think of a few times at work when we’ve abstracted too quickly only to completely throw out a feature completely, and I can think of times when we duplicated components to avoid over abstracting only to have to maintain too nearly identical components in parallel. That’s not to throw out hope. Knowing useful heuristics on both ends of the spectrum of abstractions can help us make informed decisions down the line.

React Performance

Despite the previously mentioned mantra to “avoid premature optimizations”, I will return to this section frequently over the course of my career. Majority of the work I do relates to improving existing features as opposed to developing new ones. Tips like optimizing context and ideas on how to use third-party libraries to achieve performance gains will come in handy as inevitable refactors come about.

What I want to highlight for this section is React performance monitoring, the process of wrapping component trees in React.Profiler, receiving data about said components render cycle, then doing something with that data.

I didn’t know that there were built in tools to monitor performance at the component level with React, and I found this fascinating. I can think of a few components I’ve authored where production render cycle data would be useful to inform changes, and I have plans to start making use of this API in time. I’m sure at large enough companies there are teams dedicated to wiring up these component inspections, writing APIs to receive data from the onRender function, interacting with databases to store said data, then displaying that data on custom dashboards. That bandwidth is hard to come by. While it may be that this type of work never makes it into a sprint, I think it’s valuable to know that these types of tools exist, if for no other reason that to remember to consider performance where possible.

Testing React Apps

I’m ashamed to say, I don’t have a lot of experience writing tests for front end code. I’ve written my fair share of integrations tests for our backend code, but for the front end, I’ve typically relied on QA to make sure that things are running smoothly. This chapter showed me that I could be better.

The main thing I want to highlight learning from this chapter is simply the existence of react-testing-library. A recent realization at work was that we need more regression tests, more ways to ensure that things will continue to work as expected despite changes. We have some front end integration testing to ensure this, but those tests haven’t been cared for in a number of years. What react-testing-library showed me is that rendering components in a test environment, interacting with those components, and ensuring that the applications displays and functions as expected is not a tall task. I’ll touch on another testing concept later in this post, but for now the main takeaway is that react-testing-library is a great tool for easily writing useful and maintainable tests for React components.

React Suspense

Suspense is brand new feature of React that lets users display fallback components while child components of the Suspense boundary are loading information. When this section of the course was first authored, React Suspense was still a work in progress. Since then, the API has been polished and adopted by frameworks and libraries in order to gracefully handle asynchronous operations.

While this chapter covers in depth use cases of suspense, including “render as you fetch”, the Suspense API as a whole is a new concept for me, and thus is the most salient takeaway from my experience with this chapter.

I’m still wrapping my head around the concept, a process I’ll be patient with as Suspense is not present in the versions of React I write on a daily basis, but this article helped me understand how a simple wrapper around a data fetching library can reduce complex and repetitive code and make clear declarative boundaries around components that rely on data in transit. I’m not 100% sold that ‘ol return-a-loading-component-if-a-useState-value-is-loading is cumbersome enough to throw out altogether, but I’m confident that the smart people of React wrote this API for a reason, and that I have some work to do in order to unlearn that pattern.

Build an Epic React App

This chapter covered all of the above and a few more items. One of those items was the concept of the “Testing Trophy”. This concept defined and differentiates static, unit, integration, and end-to-end tests and guides developers as to how much to focus on each type of test. The below graphic depicts the Testing Trophy.

Testing Trophy by Kent C. Dodds (https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications)

Prior to this section I was fuzzy on the differences between integration and end-to-end tests. If you had asked me which type of test I think is most important, I would have said end-to-end. After completing this section I now understand the differences, and would tell you that while I would place the most emphasis on writing integration tests, each type of test is important and has its place. I’ve spent a fair amount of time at work improving our static typing capabilities, in part because I adopted a codebase with fantastic integration tests and wanted to return the favor to the giants on whose shoulder I stand. I’ve got a long term goal to improve out end-to-end testing capabilities as well.

I appreciate the Testing Trophy as it is a helpful visual that helps me remember the differences between and the importance of each type of test.

Conclusion

There was a lot to this course. While I feel taking time to reflect on and celebrate the wins is important, completing this course feels like the beginning of a long journey. It’s one thing to know these concepts, but putting them to use is where the magic happens. I will come back to these modules as I look to apply these concepts at work and in personal projects, writing clean performant tested maintainable React.

A rocket, featuring the React logo, soaring across the sky

--

--

Jake McCambley
Jake McCambley

Written by Jake McCambley

Learning to code by teaching others — Living at the cross section of tech, music, and the outdoors — Currently studying Web Development at Practicum by Yandex.

No responses yet