React

Image of Author
September 18, 2023 (last updated September 19, 2024)

namespaced components

This is a great way to organize components that are intended to be composed together.

function Root() {
  return <p>Root</p>
}

function One() {
  return <p>One</p>
}

export const Component = Object.assign(Root, { One, ... })
import { Component } from './Component.jsx'

function Page() {
  return (
    <div>
      <Component />
      <Component.One />
    </div>
  )
}

You can also achieve a similar outcome to the above without a Root by using objects. This is nice in situations where you might not want, e.g., a <Button /> but you do want <Button.Primary />, <Button.Secondary />, etc.

// ...

export const MetaComponent = {
  Root,
  One
}

The probable uselessness of useCallback, useMemo, and memo

As a brief primer: memo memoizes a React component, useMemo caches the result of calling a function, and useCallback caches the function itself. The relationship between useMemo and useCallback is essentially trivial and in the useMemo React docs they write,

The only benefit to useCallback is that it lets you avoid writing an extra nested function inside. It doesn’t do anything else.

This means that the "problem" with using them is the same, and the problem is that you are undermining code readability for no upside.

The "deep dive" sections in the React docs pages are the most helpful breakdowns of this issue. The following quote is from the useCallback doc's deep dive section "should you add useCallback everywhere?".

Caching a function with useCallback is only valuable in a few cases:

  • You pass it as a prop to a component wrapped in memo. You want to skip re-rendering if the value hasn’t changed. Memoization lets your component re-render only if dependencies changed.
  • The function you’re passing is later used as a dependency of some Hook. For example, another function wrapped in useCallback depends on it, or you depend on this function from useEffect.

There is no benefit to wrapping a function in useCallback in other cases. There is no significant harm to doing that either, so some teams choose to not think about individual cases, and memoize as much as possible. The downside is that code becomes less readable. Also, not all memoization is effective: a single value that’s “always new” is enough to break memoization for an entire component.

Note that useCallback does not prevent creating the function. You’re always creating a function (and that’s fine!), but React ignores it and gives you back a cached function if nothing changed.

There is a nearly identical useMemo doc's deep dive section "should I add useMemo everywhere" making similar points.

For the first case, you must wrap your components in memo to take advantage of useCallback. In the second case it is likely that you have an upstream problem like using useEffect when you shouldn't be (see #Try to avoid using useEffect).

As an interesting closing note, the React dev team is trying to remove memo

React testing with the act function

https://github.com/reactwg/react-18/discussions/102 https://legacy.reactjs.org/docs/test-utils.html#act https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning https://callstack.github.io/react-native-testing-library/docs/understanding-act https://legacy.reactjs.org/docs/testing-recipes.html#act

act() can be an emotionally charged topic because most React devs learn to fear it. Testing libraries will sometimes handle it for you, or will handle it for you some of the time. What it does can seem simple, but also thinking you solved it for it to creep in at some later point is very frustrating.

One way it crops up is that you haven't waited for something to finish in a test. If it seems mysterious why some useEffect calls trigger the act warning and other useEffect calls do not, check whether or not asynchronous activity is occurring within the call. In situations like these, with async internal useEffect calls, for example, useEffect(func-containing-async-code, []), we will not be waiting for the async activity, even though it will trigger in most testing libraries (like testing-library, react-native-testing-library, etc). So, the async action completes after the test does, and React warns us that something is amiss. For this problem, the workaround, for jest-based libraries, in the least, is timers.

test('', () => {
  jest.useFakeTimers()
  render(<Component />)
  act(() => {
    jest.runAllTimers()
  })
})

Try to avoid using useEffect

See React useEffect

Managing State

The React docs have amazing content on Managing State.

Don't mirror state

function ({ someColor }) {
  const [color, setColor] = useState(someColor)
}

If someColor changes, color will not. color was merely initialized by someColor.

useContext

In the great article Passing Data Deeply with Context from the React Beta docs, they recommend passing props deeply before reaching for contexts. I agree, though I hesitate to agree with the claim that,

it’s not unusual to pass a dozen props down through a dozen components

I would say that is unusual. That said, I strongly agree with the idea of refactoring your component hierarchy before you reach for contexts. You likely can leverage props.children to create the hierarchy you desire without having the props "leave" the parent component.

function Parent(props) {
  return (
    <A>
      <B>
        <C>{props.datum}</C>
      </B>
    </A>
  );
}

React Fiber

https://github.com/acdlite/react-fiber-architecture

React Fiber is React's redesigned "core algorithm". It emulates stack frames (as in stack frames in "the stack" in computer architecture). It does this in order to optimize when certain types of work should be done. For example, work done on a part of the page not in view should not be prioritized, even if it is "next in line". Animation frames need to almost always render (when in view) to not cause a sense of lagging in the UI.