Rethinking React Hooks: A Cautionary Exploration
Written on
The Case Against React Hooks
In the previous installments of this series, I intended to illustrate how bloggers confidently share advice about React hooks, often leading others astray. After struggling with an infinite loop in a React application I inherited, I decided to analyze the work of developers who confidently offered their contracting services. The codebase I encountered was a product of several developers from different contracting firms, alongside an employee who was juggling multiple responsibilities.
While many of these developers might be regarded as "good" React developers based on their shared knowledge, it became evident that there was a lack of cohesive direction in the project. The absence of caution and skepticism toward rapidly changing approaches led to a codebase riddled with remnants of outdated methods.
Instead of removing obsolete code when new strategies were adopted, developers seemed apprehensive about breaking the existing, fragile system. Consequently, the code evolved into a hodgepodge of Context API, Recoil, and Redux, with API calls scattered haphazardly across Redux thunks, RTK queries, and inline implementations.
As I attempted to standardize on RTK Query, chaos ensued. The specific code I was dealing with resided within a Modal, triggered by a context menu on a Grid Row, nested within a DataGrid, all contained in a larger component within a Next.js page layout. This description barely scratches the surface of the complex component tree I was navigating.
Identifying the source of excessive rendering was challenging. Was a Recoil atom being updated unexpectedly? Did someone force a rerender by adding a key somewhere? Or was the problem buried deep within a hook in a nested component? All these possibilities loomed large.
One glaring realization struck me: many developers involved lacked an understanding that every render creates a fresh instance where everything is reconstructed—unless specific measures are taken to avoid this. My observations confirmed that very few components were wrapped in useMemo or useCallback, likely because they had encountered misleading articles suggesting such practices were unnecessary.
While these articles might serve those who already grasp the complexities of React, they inadvertently mislead less experienced developers, resulting in mistakes that require remediation. The critical misunderstanding stems from the fact that using an inline anonymous function in conjunction with useCallback defeats its purpose of maintaining stable function references.
A Closer Look at Inline Components
During my refactoring efforts, I encountered an inline component serving as the row renderer for the DataGrid. My attempts to extract it into a standalone component were thwarted by the challenge of referencing the local state setter, which was confined within the closure of the inline definition. Although React documentation discourages inline component definitions, the library I was using made it exceedingly cumbersome to add handlers without doing so.
As I wrestled with this design flaw, I diligently wrapped components in useCallback and useMemo, only to find the process burdensome. The talking point that unnecessary renders are negligible fell flat in the context of a component tree housing a DataGrid with numerous rows and columns.
It's essential to recognize that excessive renders can lead to more than just performance issues; they can also result in infinite loops and unpredicted component states. The architectural complications arising from the prevalent trend of "pushing state down" only exacerbate these challenges.
The Broader Implications of Stateful Functional Components
Beyond the design flaws of stateful functional components, I frequently encounter additional complications with hooks:
- Lack of Clarity: Unlike traditional methods, where you can name functions and leave notes for future developers, hooks often involve anonymous functions that obscure their purpose.
- Complex Debugging: In enterprise applications, multiple useEffects can lead to a convoluted render/useEffect/render cycle, complicating debugging efforts. This becomes especially problematic when the state changes occur post-render.
- Difficulty in Identifying Fixes: Once an issue is pinpointed, determining the appropriate location for a fix can be challenging due to the widespread use of hooks across components.
- Fragmented Logic: While hooks claim to collocate related logic, they often lead to disjointed systems where data requests occur haphazardly, creating confusion in data management.
- Testability Concerns: The architecture necessitated by hooks often complicates testing, as components become reliant on numerous Providers, leading to fragile tests that are prone to breakage.
Moving Forward: Alternatives and Best Practices
Many have asked for a way to navigate these challenges without reverting entirely to Class components. Here are some general strategies to consider:
- Design Before Coding: Following principles from "Thinking in React," it's crucial to plan data flow and component interactions before diving into code.
- Keep Components Simple: Aim to create components that derive most of their data from props, enhancing flexibility and enabling independent development.
- Avoid Unnecessary Complexity: Wrap components in React.memo and ensure objects/functions passed to child components are wrapped in useCallback/useMemo, even if it feels redundant.
- Establish Team Guidelines: Set clear guidelines regarding the use of useStates and useEffects to maintain consistency and clarity across the codebase.
- Emphasize Clarity Over Style: Prioritize clear, maintainable code over trendy practices that may obscure functionality.
The evolution of React and its hooks presents both challenges and opportunities. With thoughtful design and adherence to best practices, developers can navigate these complexities more effectively.
In this video, explore common mistakes made with React hooks and how to avoid them.
This video discusses the useEffect hook, its potential pitfalls, and best practices for effective usage.