Text-Inputs and Performance in React (and React Native)

Charles Goode
Nerd For Tech
Published in
10 min readMay 20, 2021

Performance is one of the most important factors in UX among functionality, friction, and accessibility. As of writing this, React applications perform interaction-blocking updates to the DOM when React State is updated. This will change soon with the advent of Concurrent Mode, FYI. For now, more state updates means less responsive. For form input elements, state updates are likely to occur more frequently for elements whose input types are more dynamic. On one hand, a radio button is probably only going to be clicked on once or twice, on the other a text based input like the one I’m using to type this article may update hundreds to thousands of times. So what all happens when when a text-input triggers an update?

An example of a text-input potentially wastefully updating another component

Rendering Basics

When a component in React re-renders, it attempts to re-render all of its children and their progeny down to the leaves of the DOM. Alas, there is one retort for components who are being to told re-render themselves, shouldComponentUpdate and its kin. You can implement this directly in a class component or implicitly by extending React.PureComponent or by wrapping a functional component in React.memo (Note: implementing it explicitly for functional components is done by passing a props-are-equal function and passing it as the second argument to React.memo) . When a component determines it doesn’t need to re-render, it will prevent re-renders down the rest of its sub-tree. The exception to all of this being React Context which can trigger re-renders at any node consuming the context when the closest parent context provider changes the value.

Sibling Component Dependence Principle

This means that whenever you press a key in a text-input, everything else in that component could potentially re-render. In some cases, other components within the same component as the text-input may depend on the value of the text-input, such as when you have a search bar and list of search results. That list ought to re-render every time you change the text-input value (actually if the search function or list-rendering is particularly expensive you may consider another option I’ll mention later). This leads us to make the distinction between dependent and independent components with relation to the text-input.

Any component that is independent of the text-input value can use React.memo (and kin) to avoid the being re-rendered needlessly. This is the first and simplest approach and it solves the problem quite nicely. By extracting independent siblings of the text-input into their own components, we are taking advantage of the power components, namely encapsulation and re-usability. This also looks more like the “lifting-state-up” and Higher Order Component best practices of React. This, I believe, is a good framework at its best: performance is motivating use of best practices in component architecture! Let’s see how some other React tricks can allow us to further decrease renders. But first some disclaimers…

Disclaimers

I’m writing these examples in React, but I am primarily a React Native developer and have much less experience with these HTML elements and their fundamentals than I do with the component set in React Native. So I may not be writing <form>s following conventions. Firstly, I believe the lessons learned here apply to render minimization in general (see Dan Abramov’s blog); text-inputs simply are a highly motivating example of these ideas. Secondly, I’m also writing for a React Native audience where there is no form tag and nowhere near the functionality provided by browsers to HTML. The other disclaimer I wanted to make is there are well-accepted packages for writing forms that can keep your hands clean in these situations. Packages such as Formik and React Hook Form probably help you avoid these issues. Additionally if you keep form data in a state management store such as Redux, there are some additional ways to solve this problem which I will include in this article (Hint: remember Context?).

An example of re-rendering a sibling via indirect dependence on the text-input

Indirect Dependence… Not a problem

Continuing on, let us note that there can be an indirect dependence on the text-input value. In the above example, the ExpensiveIndependentComponent does not need the name for rendering purposes (it’s never passed to the component, so clearly the component doesn’t need it); however, it does dependent on a callback that captures name in a closure and thus has a reference that updates at the same time name does. For all intents and purposes, ExpensiveIndependentComponent now depends on name even though it doesn’t need the value for rendering. That’s unpleasant 😑. This example is especially relevant because, most likely, this submit callback will depend on everything in the form, so it will have many dependencies. Now there’s no point to even memo ing the expensive component. What can be done?

There are ways to hold a value which is mutable, preserved across renders, and when mutated, does not trigger a DOM update: Refs (for functional components) and instance variables (for class component). If we shadow some state with a ref (or instance variable), we can then memoize non-rendering callbacks using fewer dependencies. I’ll call this the state-shadowing pattern, and we can even be fancy with a custom hook! Now the useCallback hook can drop the dependency on name(you never need refs in a dependency list because their reference is always the same, but do NOT forget to access .current !); the submit reference is now stable so it does not cause re-renders of the expensive component!

Solving the indirect dependence problem with the state-shadowing pattern and a custom hook.

Shifting the Blame: Imperative Approach.

Does it strike you as unjust that the presence of the text-input forced all of its siblings to memo themselves and isolate? That’s a lot of work put into refactoring, and also memoizing has some overhead in terms of memory and input comparison computations. What if we could prevent the text-input from causing a render at this level of the hierarchy altogether? What if we could force the text-input to solves its own problem instead of figuring how other components can ignore the text-input?

Let’s push the state down. When the state updates are local to just the text-input, only the text-input itself gets rendered which is ideal. However, now how do we access that value when we need to submit? We will have to get imperative and use a ref! In functional component land, we have useImperativeHandle and React.forwardRef() to make this possible. I’ll write a getter for the state.

Pushing the state down requires accessing the value imperatively

It should be noted that this is not considered very React-like. React is known for being extensively declarative as opposed to imperative. But as developers, we should always be aware of our options; that’s just good hacking. Also the React team gave us this tool as an escape hatch, so they are recognizing that there are instances where this is the main viable option. The main criteria for choosing this method is probably how much refactoring is involved.

Does the text-input needs to be a controlled component at all? Most of the times yes, but let’s be explicit. If a component needs to render anytime the value of the text-input changes, then we must be in control of the component. This extends to text-input itself whose value we may want to validate at every step. Otherwise, don’t bother make this fancy wrapper component; just give the <input/> a ref itself and read the value when needed with inputRef.current.value . The element keeps track of its value by default!

There are, however, plenty of times when pushing the state down is a bad approach. When siblings need the value of this component declaratively, this approach just won’t do. You might put all components that need the value declaratively inside of this component, but, due to the compositional nature of JSX, this only works when those components are adjacent to the input component (no components in between in the UI and at similar tree-depth). Well if down doesn’t work let’s try up!

Superpowers: React Context and the like (Redux)

If we are clever enough with React Context and willing to live with some verbosity, we won’t have to refactor all siblings like in the first solution and we can still enjoy declarative dependent siblings all while maintaining our minimal rendering propagation. Let’s see the code!

React Context to the rescue. Pretty Complicated though.

Okay so how does it work? We’re opting here for a controller component wrapping the FormPage with context that provides the name and its setter. Using setName will trigger an update in the controller component which triggers an update to all context consuming components and its direct children. By wrapping its single child in React.memo, the render will stop propagating immediately and only pick back up at context consumers. This is a vital, classic context pattern for React performance. Note that we also lifted up the submit callback and used the state-shadowing pattern to continue to avoid rendering due to indirect dependence. It wasn’t super intuitive, and also took quite a bit of code. Is there a way to make this more reusable?

Making a bunch of context everywhere for everything can be a pain when the project scales. Luckily, some good ol’ redux can help us out though; let’s see what that looks like.

Redux solution (with trivial reducer missing)

Wow, we are back down to just two components, no custom hooks, no refs, and no memo needed! That’s what a good library can accomplish. I’m guessing you accomplish the same feat with modern state management libraries, e.g. Recoil, Jotai, Zustand, etc..

Expensive Dependent Siblings

Say your text-input is a search bar and you display a list of results right under it. Lists are expensive to render because you generally are rendering quite a large number of items at once. Even if your list is virtualized, you are paying overhead for virtualization (especially if you have dynamically sized list items). Also searching itself can be expensive whether you are filtering a list or querying an API. For these reasons, updates can to the text-input can be very costly and seemingly necessary. Well, they are necessary, but are they necessary within the next 100ms? Probably not. Especially not when for all but the final character of the search query, the user doesn’t expect to have their exact result.

If I’m searching up “foo fighters everlong music video” on YouTube, I’m not gonna care about the results that popped up for “foo” or even “foo fighters” cause the search is too ambiguous until the user has made their query complete. Now you may want to search for autocompletion suggestions, but that’s different and cheaper than fetching all of the data associated with those queries (thumbnails, durations, descriptions, views, etc.). So we want to minimize the amount of needless updates (assuming updates correspond to queries). My trick for you is debouncing. Debouncing ultimately delays calling a function until some time has passed. If the same function is called again in the meantime, the timer starts over and the function call that is executed after the timer is over is the second function call. If you call a function a bunch of times nearly sequentially, it will only run the final one. So only when the user pauses typing (or types slowly), the update will take place.

Debouncing expensive updates

I would recommend using debounce from lodash. There’s a number of parameters there to customize the feel of it. You could also consider debouncing your search function instead of the text-input setter which would have the benefit of making the text-input feel more responsive while accomplishing the same minimization of API calls. If the query can change without the results changing due to asynchronicity, then the ResultList becomes independent of the text-input and you can memo it to further reduce renders.

Bonus: with redux-batched-subscribe and debounce you can debounce redux’s notifications to subscribed components which can cut some N-render process down to 1–2 renders when many actions are going through at a time. I would recommend using both trailing and leading calls, though, for proper reactivity.

Conclusion

I don’t like writing conclusions. Anyway, I presented a number of options on different scales of refactoring that allow you to improve the render quantity due to typing in a text-input. In a lot of instances, state management libraries are probably the best option if you already know how to use one and have it set up already in your app. It should also be obvious that text-inputs are just a good example here of any stateful component. Good luck on your refactors!

--

--

Charles Goode
Nerd For Tech

Founding Engineer @ dive.chat. Working in React Native and Expo (managed-workflow)