Neat Little React Tricks

Published

Some React tricks that are too small to warrant a separate blogpost, neat nonetheless.

In React, state should be kept as close to where relevant as possible 1. Placing state unnecessarily high in the component tree can lead to , even more so when that state updates often.

When performance suffers, a component can be decomposed to isolate frequently updating state and by consequence, renders. That can mean moving some oft-changing state and the parts that need it down into a new component, a . It's a way of colocating state and the UI elements that depend on it.

If you can’t find a component where it makes sense to own the state, create a new component solely for holding the state and add it somewhere in the hierarchy above the common parent component.

Thinking in React: Identify where your state should

Let's see what this means in practice. We got a <NavBar/> component with all the usual navbar stuff and an app-wide loading indicator, a spinner that pops up whenever something is being fetched (f.e. using react-query's useIsFetching).

function NavBar({ currentUser, sidebarOpen, toggleSidebar }) {
  const isFetching = useIsFetching();
  const [openMenu, setOpenMenu] = useState(null); 

  return (
    <nav>
      <SidebarButton sidebarOpen={sidebarOpen} onClick={toggleSidebar} />
      
      <Logo />
      
      {Boolean(isFetching) ? <Spinner /> : null}
      
      <NotificationIcon onClick={() => setOpenMenu("notifications")}/>
      
      {openMenu === "notifications" && <Notifications setOpenMenu={setOpenMenu} />}

      <div onClick={() => setMenuOpen("user")}>
        Logged in as {currentUser.name}
        <Avatar image={currentUser.img} />
      </div>

      {openMenu === "user" && <UserMenu setOpenMenu={setOpenMenu} />}
    </nav>
  );
}

Note that the often changing isFetching state is not relevant to the other children of the navbar. It is, however, causing them to rerender everytime it's toggled. Only the <Spinner/> needs to know about isFetching.

To isolate the renders, make a new component and move <Spinner/> and isFetching down into it. Admittedly it's very small, normally it wouldn't be worth making a component for it. Here extracting it to a component is worth it because it holds the isFetching state and therefore frequent rerenders in the <Navbar/> are avoided.

// Component just to hold `isFetching` state, a State Container Component!
function FetchingSpinner() {
  const isFetching = useIsFetching();

  return Boolean(isFetching) ? (
    <Spinner />
  ) : null;
}

Decomposing doesn't exclusively mean moving down to a new, small leaf component like the example here, it comes in many shapes. Keep an eye open for unrelated states in a single component and try to find the fault lines between states and the JSX it ends up in, compose components accordingly.

JSX can ’dynamic’, in the sense that the component type can be determined at runtime, as long the variable name is capitalized 2. It's just how JSX de-sugars.

This can be useful when having to switch between two different components that take the same props, like variants of styled components:

import { LeftSidebar, RightSidebar } from "./sidebars";

function Sidebar({ position, size, children }) {
  const SidebarComponent = position === "left" ? LeftSidebar : RightSidebar;

  return <SidebarComponent size={size}>{children}</SidebarComponent>;
}

Another real world use case is conditionally replacing an old component with a newly refactored one:

// class-based or perhaps just wonky
import { UserPermission as OldUserPermission } from "./old-user-permissions";
// functional and clean
import { UserPermission as NewUserPermission } from "./new-user-permissions";

function UsersManagement({ users, isNewAPI }) {
  // ...

  // capitalized variable name!
  const UserPermissions = isNewAPI ? UserPermissions : NewUserPermissions;

  return (<UserPermissions
    permission={permission}
    users={users}
    currentUser={currentUser}
  />)

Yeah you could conditionally render the element with a as well but this is less work when the component has a lot of props and the props are the same.

Have some state that only ever updates in one particular way? Consider using a single-action useReducer over useState!

Take, for example, a boolean in useState that can only be toggled. Maybe you’re already using a state updater function to avoid reading the state variable from component scope: setIsOpen((prevIsOpen) => !prevIsOpen). Knock it off and put that logic in a reducer instead, then just call the dispatch without any arguments.

const [isOpen, toggleOpen] = useReducer((prevIsOpen) => !prevIsOpen, false);

// Flip the switch
toggleOpen();

A nice benefit is that the is , whereas a callback for setting state is not. If toggleOpen is called in a useEffect, it will have to be wraped in useCallback to make it referentially stable. And that works but it’s quite some boilerplate for something as simple as flipping a switch.

Also, a reducer restricts what can be done to the state 3. Suppose you’re working in a team on a gigantic app and pass a state setter for a toggle down to parts of the app that fall under your teammates responsibilty. They can now set the state to whatever, which is not what a toggle should be able to do! OK, this might be slightly farfetched but you get what I mean, limiting possibilites is good design✨ .

Real simple, barely a trick but here you go. Just like setting default values on props, you can set a default for the special children prop 4. Set it to a sane default string or element and render children where you want.

I often use it on small reusable (styled) UI components, like buttons and links, that contain the same text most of the time, but need to overriden every now and then. I'm talking about text like “Read more…”, or “Open”,

export const DonateLink = ({ children = "Donate" }) => (
  <Link href="/donate" className="styledLink" title="Go to the donation page :)">
    {children}
    <HeartIcon />
  </Link>
);

// In practice:
<DonateLink /> // default text
<DonateLink>Donate now!</DonateLink> // overridden text

Or using the little known defaultProps property on components.

DonateLink.defaultProps = {
  children: 'Donate',
}

I prefer assigning default values directly when destructuring props, that makes it more obvious.

JSX is just another way to React.createElement, and are just plain objects 5. We can declare element variables before the return for reuse.

Here's an example, it's a component I use on this blog. It's a fleshed out <blockquote> as seen on its MDN page. You can pass the component a url props which will both be set as the url attribute on the blockquote element and as the href on the inside the cite element. If no url is given, no anchor element should be made inside the <cite>.

export function Quote({ children, url, source }) {
  
  const citeEl = <cite>{source}</cite>;

  return (
    <figure>
      <blockquote cite={url}>{children}</blockquote>
      <figcaption>
        {url ? <a href={url} rel="noopener">{citeEl}</a> : citeEl}
      </figcaption>
    </figure>
  );
}

Another use case could be to conditionally wrap an element with a tooltip.

function TableCell({ value, isOutdated }) {
  const tableCell = <td>{value}</td>;

  return isOutdated ? (
    <Tooltip text="This value is not up to date">{tableCell}</Tooltip>
  ) : (
    tableCell
  );
}

Take it a step further with React.cloneElement to conditionally add a prop. I think this is only worth if the wrapped element takes a lot of props, so those don't have to be repeated. Repeating the element is fine if it's a DOM node or a component without props.

function TableCell({ value, setCell, isOutdated }) {
  const tableCell = (
    <EditableCell onChange={setCell} long={long} list={list} ofProps={ofProps}>
      {value}
    </EditableCell>
  );

  return isOutdated ? (
    <Tooltip text="The value is not up to date">
      {cloneElement(tableCell, { className: "outdated" })}
    </Tooltip>
  ) : (
    tableCell
  );
}

First off, putting things in state that don't belong there is a bad idea, you'll have a hard time when you do so. Remember that state is reserved for data that changes over time 6.

There's one exception to this rule and it's using useState to initialize a value once, and it sure is a neat trick.

Why would we wan't to initialize a value once? Well, values defined in React components are reevaluated every render, that can be problematic. Sometimes it's important that a value is initialized strictly once, like an external library. Other times, an the object can be 'expensive' to create, for instance a huge array or heavy class. If you need to initialize such a value per component, useState can help.

First, the state value is guaranteed to be stable. On top of that, useState can take an initializer function to ensure the initial state is . The value should never change, so we can just ignore the state setter by not destructuring it.

function Image(props) {
  const [intersectionObserver] = useState(() => new IntersectionObserver());

  return (
    // JSX
  )
}

Voila, a lazily-created, stable value.

“Isn’t this what useMemo is for?“ No, not really. useMemo should be seen as a performance optimization for expensive computations based on , we shouldn't rely on it for one-time initializations 7. Or as the docs put it:

useMemo lets you memoize an expensive calculation if the dependencies are the same. However, it only serves as a hint, and doesn’t guarantee the computation won’t re-run.

React Hooks FAQ - How to create expensive objects lazily?

Write your code so that it still works without useMemo — and then add it to optimize performance.

React Hooks Reference - useMemo

You can achieve the same with useRef, below is the example from How to create expensive objects lazily? section Hooks FAQ.

function Image(props) {
  const ref = useRef(null);

  // ✅ IntersectionObserver is created lazily once
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(props.onIntersect);
    }
    return ref.current;
  }

  // When you need it, call getObserver()
  // ...
}

But I think it's not as nice as useState with an initializer. I'd love for useRef to have an initializer function too but it doesn't.

See this this post post by Vladimir Klepov for more ways to make useRef lazy.

And those were all the tricks for this time!

  1. Place code as close to where it's relevant as possible State Colocation will make your React app faster - Kent C. Dodds

  2. When an element type starts with a lowercase letter, it refers to a built-in component like <div> or <span> and results in a string 'div' or 'span' passed to React.createElement. Types that start with a capital letter like <Foo /> compile to React.createElement(Foo) and correspond to a component defined or imported in your JavaScript file. We recommend naming components with a capital letter. If you do have a component that starts with a lowercase letter, assign it to a capitalized variable before using it in JSX. JSX In Depth - User-Defined Components Must Be Capitalized

  3. Also passing dispatch down is a better idea than passing a setter because it restricts what child can do. Dan Abramov tweet

  4. When you nest content inside a JSX tag, the parent component will receive that content in a prop called children. React Docs: Passing JSX as children

  5. An element is a plain object describing a component instance or DOM node and its desired properties. It contains only information about the component type (for example, a Button), its properties (for example, its color), and any child elements inside it. An element is not an actual instance. Rather, it is a way to tell React what you want to see on the screen. React Docs: React Components, Elements, and Instances - Elements Describe the Tree

  6. State is reserved only for interactivity, that is, data that changes over time. Thinking in React

  7. You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance. React Hooks Reference: useMemo