(Lazy) Components Maps

Published Last updated

Conditionally rendering elements is fundamental to every React app, it's the driving force behind interactivity. React doesn't have its own syntax for conditionals like some other frameworks, but relies on just JavaScript conditionals! There are multiple ways to do conditionals in JS, so there are multiple ways to do it in React. The new React docs provides a good overview of approaches in Conditional Rendering.

If/elses, early returns, switch statements, logical and ternary operators, they all work! In the end it mostly comes down to personal preference.

But when you're matching a value against a known set of values — which is very common in conditional rendering — if/else and ternaries get a little verbose. The condition is repeated for each possible value.

For example, say you're conditionally rendering a time picker component based on a period of time:

if (type === "day") {
  return <DayPicker />;
} else if (type === "week") {
  return <WeekPicker />;
} else if (type === "month") {
  return <MonthPicker />;
} else if (type === "year") {
  return <YearPicker />;
} else {
  return "Unknown type!";
}
Pattern Matching Proposal

There's a TC39 "Pattern Matching" proposal to add proper pattern matching to JS to address the shortcomings of the switch statement 3. It will probably take a while to land (if ever). In the meantime, you can 'abuse' a switch statement for pseudo-pattern matching. Set the switch expression to true and make each case an expression.

switch (true) {
  case (energy > 100): {
    return <HighEnergy />;
  }
  case (energy < 100): {
    return <LowEnergy />;
  }
}

See this post Pattern Matching in JavaScript for more on this.

In this case, a switch statement provides slightly cleaner syntax, it matches an expression's value, the type, against a set of values — here day, week, month, and year. If type matches any of these, the corresponding picker component is rendered.

switch (type) {
  case "day": {
    return <DayPicker />;
  }
  case "week": {
    return <WeekPicker />;
  }
  case "month": {
    return <MonthPicker />;
  }
  case "year": {
    return <YearPicker />;
  }
  default: {
    throw new Error("Unknown type!");
  }
}

Not bad but I think there's a better way.

This switch statement is really quite similar to a key/value map. It maps keys (strings) to values (React elements/components). I think maps are an often overlooked tool for conditionals in general.

Every plain ol' JavaScript object is a simple key/value map.

// A component map!
const components = {
  day: DayPicker,
  week: WeekPicker,
  month: MonthPicker,
  year: YearPicker,
};

You can use a Map object too, it can also have non-string keys.

const components = new Map([
  ["day", DayPicker],
  ["week", WeekPicker],
  ["month", MonthPicker],
  ["year", YearPicker],
]);

Component maps have all the values and components in one place, concise and comprehensible! That makes them more legible than if/else or ternaries in my opinion.

Combine this with the fact that JSX can be ’dynamic’, the component type can be determined at runtime.

const SelectedPicker = components[type];

// in component return
return (
  <SelectedPicker 
    selectedPeriod={selectedPeriod}
    setSelectedPeriod={setSelectedPeriod}
  />);

Note that the variable name must be capitalized in order to be used as JSX. It's how React can tell it's a component instead of a 4.

Component maps are also limited in some ways, they can't be used for conditional rendering everytime.

If you can't be sure the key is actually in the map, you want to prefer a switch statement (or any other conditional). A default case or else block can handle missing cases explicitly, component maps can't do that.

If the value is not in the component map, the component type will be undefined and you'll get this error message when you try to render it:

Element type is invalid: expected a string (for built-in components) or a
class/function (for composite components) but got: undefined.
TypeScript Tip

With TypeScript you'll be warned when you try a key that's not in the map. Use both the keyof and typeof operators on the component map to get the string literal union of its keys.

It's possible to make it 'fail' silently by assigning a default HTML tag string using the nullish coalescing operator.

const SelectedPicker = components[type] ?? "div";

But I don't think this is a good solution. It won't display any warnings, it'll just add a useless empty HTML element to the page. You could make the fallback an obscure HTML tag and add an error message to it using CSS selectors and pseudo-elements, but all of this is terribly hacky. Moreover the props are passed to the HTML tag as is, that's not good either.

The components in the example above conveniently take no props! This is not very realistic. In a real app, date picker components would receive props, like a state value and a state setter.

With conditionals you can pass different props to the different component in each case.

switch (type) {
  case "day": {
    return (
      <DayPicker currentDay={today} />
    );
  }
  case "week": {
    return (
      <WeekPicker currentWeek={week} />
    );
  }

Whereas component maps quietly assume that the component takes the same props. In this example, it wouldn't be unreasonable for each picker to require the same props.

Some of these components might never be rendered at all, yet we still load the code for them! Hardly efficient, this is where code splitting comes in.

To code split the components, make each component a module: move each of them to a separate file and export default each. Import them with a dynamic import() using React.lazy().

const DayPicker = React.lazy(() => import("./DayPicker"));

The code for this component will be loaded automatically the first time it is first rendered 5.

Lazy-loading requires us to do two things in the React component tree. First, the lazy-loaded component has to be rendered inside a <Suspense> boundary 7. It will display a while it's loading. It can technically be added anywhere above. It doesn't need to wrapped directly around it, though for showing a fallback component it's better to keep it close 8.

The best practice is to place <Suspense> where you want to see a loading indicator, but to use lazy() wherever you want to do code splitting.

React Docs: Codesplitting - React.lazy

Second, make sure an <ErrorBoundary/> is placed anywhere above to handle when the module fails to load (and to attempt a retry). Like the Suspense boundary, it's better to keep it close.

Here's how lazy-loading can be combined with a component map.

const components = {
  day: lazy(() => import("./day-picker")),
  week: lazy(() => import("./week-picker")),
  month: lazy(() => import("./month-picker")),
  year: lazy(() => import("./year-picker")),
};

// in component
const SelectedPicker = components[type];

return (
  <ErrorBoundary FallbackComponent={ErrorFallback} onError={handleError}>
    <Suspense fallback={<Spinner />}>
      <SelectedPicker />
    </Suspense>
  <ErrorBoundary>
)

Lazy-loading is (probably) only worth it when there are a lot of components in the map, and/or if the component are heavy, meaning they render a large subtree or make use of heavy dependencies. It's not necessary to make every component map lazy.

Also note that lazy-loading is not unique to component maps, any conditionally rendered component can be code split with React.lazy.

Avoid a 'fallback' flash in React 18

If you are on React 18, first off, credits to you for keeping up. Second, you can avoid a when switching to a lazy-loaded component for the first time by wrapping the setter in startTransition 9. That way React will keep the old UI around until the new UI is ready 10.

onChange={(e) => startTransition(() => setType(e.target.value))}

I saw this tweet by Cory House that demonstrates a great use case of a lazy component map, an SVG icon component.

Situation: You have a big list of icons or images. You want to dynamically display a few without importing them all.
Solution: Create a lazy loading wrapper.
Steps:

  1. Lazy load all the options
  2. Specify the component to render via a prop I call this a "Lazy loaded, strongly typed, component set wrapper. I generate this component via a Node script that looks like this. [Click the link to see the image of the code!]
    This way, the component is updated anytime we add new icons. 😎
    Sure beats having to manually maintain an import containing a hundred icons!
Cory House

Agree, nice one Cory! Although I like my name for it slightly better. Here, using an object to select the component is much more compact than a switch statement would be. Another thing that makes this a great fit for component maps is that these components all take the same props, namely SVG element props.

As usual, I've made a CodeSandbox to go along with the post. There are a few things to play around with so be sure to check it out.

  1. Ternary operators become hard to read after the first level. Even if they seem to save space at the time, it’s better to be explicit and obvious in your intentions. Tao of React: Avoid Nested Ternaries - Software Design, Architecture & Best Practices | Alex Kondov

  2. A ternary is a fine way to switch between two pieces of JSX. Once you go beyond 2 items, the lack of an else if () turns your logic into a bloody mess real quick. Any extra conditions inside a ternary branch, be it a nested ternary or a simple &&, are a red flag. Good advice on JSX conditionals - Don’t get stuck in ternaries

  3. There are many ways to match values in the language, but there are no ways to match patterns beyond regular expressions for strings. switch is severely limited: it may not appear in expression position; an explicit break is required in each case to avoid accidental fallthrough; scoping is ambiguous (block-scoped variables inside one case are available in the scope of the others, unless curly braces are used); the only comparison it can do is ===; etc. TC 39 Pattern Matching Proposal

  4. 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

  5. The React.lazy function lets you render a dynamic import as a regular component. React.lazy takes a function that must call a dynamic import(). This will automatically load the bundle containing [that component] when [it's] first rendered. This must return a Promise which resolves to a module with a default export containing a React component. The lazy component should then be rendered inside a Suspense component, which allows us to show some fallback content (such as a loading indicator) while we’re waiting for the lazy component to load. Note that lazy components can be deep inside the Suspense tree — it doesn’t have to wrap every one of them. The best practice is to place <Suspense> where you want to see a loading indicator, but to use lazy() wherever you want to do code splitting. React Docs: Codesplitting - React.lazy

  6. React.lazy currently only supports default exports. If the module you want to import uses named exports, you can create an intermediate module that reexports it as the default. This ensures that tree shaking keeps working and that you don’t pull in unused components. React docs Code Splitting: Named Exports

  7. The lazy API is powered by Suspense. When you render a component imported with lazy, it will suspend if it hasn’t loaded yet. This allows you to display a loading indicator while your component’s code is loading. React Beta Docs: Suspense — Lazy-loading components with Suspense

  8. When a component suspends, it activates the fallback of only the nearest parent Suspense boundary. This means you can nest multiple Suspense boundaries to create a loading sequence. Each Suspense boundary’s fallback will be filled in as the next level of content becomes available. React Beta Docs: Suspense — Revealing nested content as it loads

  9. Any component may suspend as a result of rendering, even components that were already shown to the user. In order for screen content to always be consistent, if an already shown component suspends, React has to hide its tree up to the closest <Suspense> boundary. However, from the user’s perspective, this can be disorienting. [...] However, sometimes this user experience is not desirable. In particular, it is sometimes better to show the “old” UI while the new UI is being prepared. You can use the new startTransition API to make React do this. React docs Code Splitting: React.lazy - Avoiding fallbacks

  10. React.startTransition lets you mark updates inside the provided callback as transitions. This method is designed to be used when React.useTransition is not available. Updates in a transition yield to more urgent updates such as clicks. Updates in a transition will not show a fallback for re-suspended content, allowing the user to continue interacting while rendering the update. React API: useTransition