September 24, 2019

Selecting Across Components

Let’s look at a popular implementation of TodoMVC that uses a single, Redux-style global store for state.

A screenshot of a todo list

The global state model for this app looks something vaguely like

Model {
    entries: List<Entry>
    field: String
}
Entry {
    description: String
    completed: Boolean
    id: Int
}

There are many advantages to having your todo list state stored globally with something like Redux or Elm. For instance, we can easily reuse the same data for the actual todo list and the “2 items left” indicator. Useful if these two sections are far apart in our component hierarchy, or if they were expensive to calculate. However, there’s state here that isn’t in this global store. To show you, all I need to do is click in the middle of the header, and drag to somewhere in the footer.

A screenshot of a todo list in the browser, but the page is selected from the header of the todo list to halfway through the footer

We’re clearly looking at a different state – the page has a selection! Yet the global state object remains unchanged. This interface still contains local state, it’s just handled by the browser. This may seem like a one-off interaction, but the browser handles quite a bit of element-local state. Just off the top of my head:

  • text selection of elements, as we’ve seen
  • selection inside text fields
  • element attributes
  • focus
  • a cached render to speed up subsequent draws
  • scroll position and momentum
  • if a cursor has pressed down on a button
  • CSS animation progress

Some of these we can imagine being local React component state (although it would break the nice properties of the global store) but text selection across multiple elements has confounded me. If you put it in a global store, you end up replicating your view structure inside the model. If you put it in a local store — well, a selection across multiple components crosses, by definition, multiple components. So which component locally stores the selection? Remember this has to work universally across all components. How do we make sure the selection state is available when rendering a component, so that we can also render the selection?

This last problem is particularly tricky, since a selection depends on a component’s contents. In order to know these contents, we must render them. So we can’t call render without the selection information, but we need to call render to get the selection information?

I’ve talked to folks who know much more about frontend frameworks than me, and looked at open source repos of text editors in the browser. I’ve seen and heard of two possible solutions:

  1. Don’t select across components. This works for things like Ace, where you’re selecting text in a document with pre-defined structure, and can just store the selection in the document root. However, it means that you cannot select arbitrary elements.
  2. Let the platform handle it. On the web, the web browser handles selections, so we don’t have to worry about this.

The first option works in limited situations, but doesn’t quite solve the problem. The second option works when building on the web or a mobile app, and so many folks will likely dismiss this post as a hypothetical thought exercise. But I wonder — if we were building a platform from scratch, what kind of framework could we use, now that we’ve shown both local component and global state isn’t a great approach to selections?