Skip to content

Instantly share code, notes, and snippets.

@sw-yx sw-yx/newdefaults.md Secret
Last active Mar 30, 2019

Embed
What would you like to do?
reacts new defaults

React's New Defaults

Edit from the future:

Got some feedback from Dan: Concurrent has a specific meaning that he doesnt want any confusion about. "Concurrent" in the context of Concurrent React means we can pause something that's happening and then do something else (e.g. handle a click while stuff is being fetched and rendered). This definition applies/fits for both Time Slicing and React Suspense.

My assertion that "siblings appear together" means "concurrently" was wrong/potentially confusing. I'll post this in the youtube description as well.


Original post:

TL;DR: Concurrent React and React Hooks aren't simply new ways to do old things. They represent a new, stronger opinion of what apps should do by default.

⚠️Disclaimer: This op-ed discusses unstable future React APIs which are entirely opt-in - you don't need to learn or adopt them yet. The React Blog and docs remain the primary sources you should refer to for official info.

React Conf 2018 wasn't your regular web developer conference: The keynotes covering React Hooks, Concurrent Mode, and the DevTools Profiler weren't just talks, they were product launches. And given that they were presenting the work of a 7 person team, for an audience of ~1.25 million React developers growing ~70%/yr, they were very carefully crafted:

To exaggerate, this is how Sophie Alpert's keynote felt:

basically

But this isn't yet another React fanboy post.

So I will ask: what was missing from the presentations?

What people aren't talking about (enough)

There have been positive and negative reviews alike of the new features (although I think the DevTools Profiler is pretty universally praised!) and only you can make up your mind if you like them.

What I felt was missing from the talks and subsequent debates was an explicit discussion of how the default behaviors change from Synchronous React to Concurrent React, or from Class Components to Function Components with Hooks. I'll go through the new default behaviors, and then end with why I think they are important to discuss.

Concurrent by Default: Suspense and Time Slicing

"Concurrent" is a new term that the React team are adopting so it is fine if it doesn't immediately mean anything to you. In my interpretation, it describes the React team's new goal to help you write high performance apps by default. Check out the ReactConf 2018 Keynote on Concurrent Rendering if you haven't already.

While React is not claiming to solve every performance issue, they are now providing native solutions for 2 common problems:

  • Unintentional States
  • Uninterruptible Render-and-Commit

(Note that these terms are not official React terms, they are explanations "in my own words".)

Defaults are just that - defaults. You can, of course, opt out of these defaults today by writing a lot of stateful, imperative, probably buggy code, or importing a library like react-loadable where others have written the code for you.

  1. Unintentional States is the default where every component has no knowledge of the state of its children, and where every component is responsible for its own rendering. This default often results in cascading spinners, though spinners are not the problem.

    // example app in Synchronous React
    
    function App() {
      // ❌will show two 🌀's 
      // ❌even if the data loads almost instantly
      // ❌UI may jump around if 2 loads before 1
      return (
        <>
          <ComponentThatLoadsData id={1} />
          <ComponentThatLoadsData id={2} />
        </>
      );
    }
    
    class ComponentThatLoadsData extends Component {
      state = { data: null };
      componentDidMount() {
        const { id } = this.props;
        fetch(`/api/${id}`).then(res =>
          this.setState({
            data: res.json()
          })
        );
      }
      render() {
        if (!this.state.data) return '🌀'; // loading "spinner"
        return <div>{JSON.stringify(this.state.data)}</div>;
      }
    }

    Ordinarily to fix these issues I would have to "lift state up" - the only way to let App control the loading and display of its child components.

    React Suspense switches this default:

    // same app in Concurrent React
    
    function App() {
      // ✅one 🌀
      // ✅unless data is cached/loads under 1s
      // ✅Only shows when both 1 and 2 have loaded, no jumps
      return (
        <Suspense maxDuration={1000} fallback="🌀">
          <ComponentThatLoadsData id={1} />
          <ComponentThatLoadsData id={2} />
        </Suspense>
      );
    }
    
    const Resource = createResource(id =>
      fetch(`/api/${id}`).then(res => res.json())
    );
    
    // who needs classes 😎
    function ComponentThatLoadsData({ id }) {
      const data = Resource.read(id);
      return <div>{JSON.stringify(data)}</div>;
    }

    My advice for understanding React Suspense: Forget the small stuff, like the "cleanliness" of code or the preference of classes vs functions. Focus on how the defaults change. For the same code structure, the Unintentional State problems simply go away. If I actually did want multiple spinners to show up and data loading to be uncoordinated, I'd have to do extra work. With Intention.

    You can try out a React Suspense app for yourself here on CodeSandBox.

  2. Uninterruptible Render-and-Commit is the default where expensive renders will block the thread until they end and commit to the DOM. This often results in "janky UIs" that can be unresponsive to the user.

// example App in Synchronous React

class App extends PureComponent {
  state = { value: ''};

  handleChange = ({ target: { value } }) => {
    this.setState({ value });
  };

  render() {
    const { value } = this.state;
    const data = doExpensiveCalculation(value); // expensive calculation!!!
    return (
      <div className="container">
        <input value={value} onChange={this.handleChange} />
        <Charts data={data} />
      </div>
    );
  }
}

In this example our render becomes expensive by virtue of having an expensive calculation inside it. When I type my first character in the input, the rerender takes a long time, and thus the thread is blocked for the next character I type and the app feels unresponsive.

To fix this, you could split out the component and do the expensive work asynchronously, adding callbacks or promises. Or, with ConcurrentMode and the upcoming scheduler.scheduleCallback API, you can (in the future!) simply declare a low priority update:

// same App, in ConcurrentMode...

// ...
  handleChange = ({ target: { value } }) => {
    scheduleCallback(() => { // low priority update
      this.setState({ value });
    });
  };

And now I can type to my heart's content while the expensive data calculation proceeds in the same component. This works due to React's multi-pass update queue methodology called Time Slicing. The comments in the source code are a highly recommended (though not required) read for understanding this.

As Andrew Clark explained in his keynote, Concurrent React can partially render a tree without committing the result, breaking the commit-and-render paradigm we used to have. It also does not block the main thread. In fact, that's how it works by default.

But it doesn't stop there - assigning appropriate priorities could be a lot more invisible in the future, for example with a hidden={true} attribute to set an absolute lowest render priority for a component. This allows you to start prerendering and data fetching for the "N+1" page without committing it to the DOM.

You can try comparing Synchronous vs Concurrent mode in a Time Slicing app for yourself here on CodeSandBox.

Ultimately, both solutions are two sides of the same coin. Splitting committing from rendering is central to how both Time Slicing and Suspense work, and this new default is what you get in ConcurrentMode.

Immediate Mode by Default: React Hooks

Hooks are the newest proposal from the React team to solve problems with "wrapper hell", bloated components, and confusing classes. Definitely read the docs and check out the ReactConf 2018 Keynote on React Today and Tomorrow if you haven't already, there is no better place to learn about something so new.

It is hard to summarize such a sweeping proposal, but we will focus on what the new default is in Hooks.

First, some perspective. JSX lets us take imperative DOM APIs like:

var el = document.createElement('div');
el.className = 'marker';
const callback = () => {
  // not 100% accurate but you get the picture
  el.removeEventListener('click', callback) // 👎duplicative
})
el.addEventListener('click', callback)

and express it declaratively:

<div className="marker" onClick={callback} />

But the other, perhaps less familiar perspective to view these APIs with is the game programming concept of retained mode vs immediate mode graphics.

To drastically oversimplify, "Retained mode" is how the DOM works - there is a concept of persistent objects that you modify incrementally. "Immediate mode" is how React wants you to treat your render functions, declaring your JSX as if you were rendering out anew every single time. However, for the rest of your class components, React still expects you to code imperatively in "Retained mode" with state and lifecycle methods.

This "Immediate vs Retained Mode" language doesn't seem much in fashion anymore, but when React was first introduced, core team member Pete Hunt gave an excellent talk on how React's Virtual DOM helps you write like you are in Immediate Mode, while actually interacting with the DOM's Retained Mode API's. Similar views from James Long, Sebastian Markbage, and Andre Staltz from that period support this point of view.

Immediate mode is fundamentally more declarative and therefore easier to reason about, but Retained mode is more performant and is fundamentally how the DOM APIs work. React's reconciler bridges the two modes and allows us to write like we are in Immediate mode for the render function.

With this perspective, we can view React Hooks as doing the same thing, but for the remaining functions of a stateful React Component. Let's adapt an example from the Hooks docs:

// inside a class component
  componentDidMount() {
    document.title = `${this.props.name} mounted`;
  }

  componentWillUnmount() {
    document.title = `${this.props.name} unmounted`;
  }

Here the component modifies the document title with its name on mount and unmount. However a bug appears: If this.props.name changes from foo to bar while the component is still mounted, then the document title will be out of date! (It will still show foo mounted). You'd have to implement componentDidUpdate as well with a carbon copy of your componentDidMount code to get it working like you'd expect.

This is because the default for state and side effects in Class Component API is analogous to "Retained Mode", where you imperatively run code inside generic lifecycle methods React exposes to you. The onus is on you to remember to chain together the right sequence of lifecycle methods to keep your side effects (non-render-based actions like setting document.title) current and up to date.

Compare the same code to hooks in Function Components:

// inside a function component
  useEffect(() => {
    document.title = `${props.name} mounted`;
    return () => document.title = `${props.name} unmounted`;
  })

Because effects are run on each update, you never forget to implement a "componentDidUpdate" equivalent; it's just how it works out of the box! In fact you have to add an empty array to skip effects if you really don't want the effect to rerun on update.

To a smaller extent, you can make this argument for how state works in classes vs functions:

  • In classes, you set this.state inside the constructor and reference it from this (Retained Mode)
  • In functions, you destructure from useState every time (Immediate Mode)

This is a new default in React, analogous to letting you run stateful and side-effect-ful lifecycles in "Immediate Mode" the same way React does for its render function. The better composition, avoidance of wrapper hell, and avoidance of class footguns is a nice outcome of this model (again, my assertion; not theirs).

In fact, the analogy of hooks to JSX is so close that the Rules of Hooks (where order of calling is implicitly important but in practice a nonissue) have a direct analogy to JSX (where order of components is implicitly important but in practice a nonissue).


Conclusion: The Forest for the Trees

I have often suspected there is a Maslow's Hierarchy-esque hierarchy of concerns when it comes to discussing API design, something like:

At the bottom is the easiest bikeshedding fodder (which, by the way, doesn't mean these complaints are invalid, because an ergonomic design for something we interact with frequently is indeed important). They are easily changed and therefore least permanent, and also afford a wide array of valid alternatives.

Whatever is at the top level of the pyramid is certainly up for debate (in fact all of it is - very meta!). Having a "Philosophy" of API design feels a little too heavy handed and grandiose while being underspecified. Having "Motivations" at the top level of the pyramid focuses too much on what the problems are when the goal is really solution design.

I submit that what we really care about that determines an API's long term success or failure is the opinion it embodies.

All good libraries have one: even if they say they are "unopinionated", that really means they are unopinionated about everything else that they don't have an opinion on. An implicit tautology. Opinions are good, especially small opinions executed well.

React's new opinions are that its two new defaults, Concurrent By Default and Immediate Mode by Default, are better ways to write better apps.

When considering if you like the new APIs or not, start there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.