19

React as a UI Runtime

 5 years ago
source link: https://www.tuicool.com/articles/hit/biaammB
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Most tutorials introduce React as a UI library. This makes sense because React is a UI library. That’s literally what the tagline says!

react.png

I’ve written about the challenges of creatinguser interfaces before. But this post talks about React in a different way — more as a programming runtime .

This post won’t teach you anything about creating user interfaces.But it might help you understand the React programming model in more depth.

Note: If you’re learning React, check out the docs instead.

:warning:

This is a deep dive — THIS IS NOT a beginner-friendly post.In this post, I’m describing most of the React programming model from first principles. I don’t explain how to use it — just how it works.

It’s aimed at experienced programmers and folks working on other UI libraries who asked about some tradeoffs chosen in React. I hope you’ll find it useful!

Many people successfully use React for years without thinking about most of these topics.This is definitely a programmer-centric view of React rather than, say, a designer-centric one . But I don’t think it hurts to have resources for both.

With that disclaimer out of the way, let’s go!

Host Tree

Some programs output numbers. Other programs output poems. Different languages and their runtimes are often optimized for a particular set of use cases, and React is no exception to that.

React programs usually output a tree that may change over time . It might be a DOM tree , an iOS hierarchy , a tree of PDF primitives , or even of JSON objects . However, usually we want to represent some UI with it. We’ll call it a “ host tree” because it is a part of the host environment outisde of React — like DOM or iOS. The host tree usually has its own imperative API. React is a layer on top of it.

So what is React useful for? Very abstractly, it helps you write a program that predictably manipulates a complex host tree in response to external events like interactions, network responses, timers, and so on.

A specialized tool works better than a generic one when it can impose and benefit from particular constraints. React makes a bet on two principles:

  • Stability.The host tree is relatively stable and most updates don’t radically change its overall structure. If an app rearranged all its interactive elements into a completely different combination every second, it would be difficult to use. Where did that button go? Why is my screen dancing?

  • Regularity.The host tree can be broken down into UI patterns that look and behave consistently (such as buttons, lists, avatars) rather than random shapes.

These principles happen to be true for most UIs.However, React is ill-suited when there are no stable “patterns” in the output. For example, React may help you write a Twitter client but won’t be very useful for a 3D pipes screensaver .

Host Instances

The host tree consists of nodes. We’ll call them “host instances”.

In the DOM environment, host instances are regular DOM nodes — like the objects you get when you call document.createElement('div') . On iOS, host instances could be values uniquely identifying a native view from JavaScript.

Host instances have their own properties (e.g. domNode.className or view.tintColor ). They may also contain other host instances as children.

(This has nothing to do with React — I’m describing the host environments.)

There is usually an API to manipulate host instances. For example, the DOM provides APIs like appendChild , removeChild , setAttribute , and so on. In React apps, you usually don’t call these APIs. That’s the job of React.

Renderers

A renderer teaches React to talk to a specific host environment and manage its host instances. React DOM, React Native, and even Ink are React renderers. You can also create your own React renderer .

React renderers can work in one of two modes.

The vast majority of renderers are written to use the “mutating” mode. This mode is how the DOM works: we can create a node, set its properties, and later add or remove children from it. The host instances are completely mutable.

React can also work in a “persistent” mode. This mode is for host environments that don’t provide methods like appendChild() but instead clone the parent tree and always replace the top-level child. Immutability on the host tree level makes multi-threading easier. React Fabric takes advantage of that.

As a React user, you never need to think about these modes. I only want to highlight that React isn’t just an adapter from one mode to another. Its usefulness is orthogonal to the target low-level view API paradigm.

React Elements

In the host environment, a host instance (like a DOM node) is the smallest building block. In React, the smallest building block is a React element .

React element is a plain JavaScript object. It can describe a host instance.

// JSX is a syntax sugar for these objects.
// <button className="blue" />
{
  type: 'button',
  props: { className: 'blue' }
}

A React element is lightweight and has no host instance tied to it. Again, it is merely a description of what you want to see on the screen.

Like host instances, React elements can form a tree:

// JSX is a syntax sugar for these objects.
// <dialog>
//   <button className="blue" />
//   <button className="red" />
// </dialog>
{
  type: 'dialog',
  props: {
    children: [{
      type: 'button',
      props: { className: 'blue' }
    }, {
      type: 'button',
      props: { className: 'red' }
    }]
  }
}

(Note: I omittedsome properties that aren’t important to this explanation.)

However, remember that React elements don’t have their own persistent identity. They’re meant to be re-created and thrown away all the time.

React elements are immutable. For example, you can’t change the children or a property or a React element. If you want to render something different later, you will describe it with a new React element tree created from scratch.

I like to think of React elements as being like frames in a movie. They capture what the UI should look like at a specific point in time. They don’t change.

Entry Point

Each React renderer has an “entry point”. It’s the API that lets us tell React to render a particular React element tree inside a container host instance.

For example, React DOM entry point is ReactDOM.render :

ReactDOM.render(
  // { type: 'button', props: { className: 'blue' } }
  <button className="blue" />,
  document.getElementById('container')
);

When we say ReactDOM.render(reactElement, domContainer) , we mean: “Dear React, make the domContainer host tree match my reactElement .”

React will look at the reactElement.type (in our example, 'button' ) and ask the React DOM renderer to create a host instance for it and set the properties:

// Somewhere in the ReactDOM renderer (simplified)
function createHostInstance(reactElement) {
  let domNode = document.createElement(reactElement.type);  domNode.className = reactElement.props.className;  return domNode;
}

In our example, effectively React will do this:

let domNode = document.createElement('button');domNode.className = 'blue';
domContainer.appendChild(domNode);

If the React element has child elements in reactElement.props.children , React will recursively create host instances for them too on the first render.

Reconciliation

What happens if we call ReactDOM.render() twice with the same container?

ReactDOM.render(
  <button className="blue" />,  document.getElementById('container')
);

// ... later ...

// Should this *replace* the button host instance
// or merely update a property on an existing one?
ReactDOM.render(
  <button className="red" />,  document.getElementById('container')
);

Again, React’s job is to make the host tree match the provided React element tree . The process of figuring out what to do to the host instance tree in response to new information is sometimes called reconciliation .

There are two ways to go about it. A simplified version of React could blow away the existing tree and re-create it from scratch:

let domContainer = document.getElementById('container');
// Clear the tree
domContainer.innerHTML = '';
// Create the new host instance tree
let domNode = document.createElement('button');
domNode.className = 'red';
domContainer.appendChild(domNode);

But in DOM, this is slow and loses important information like focus, selection, scroll state, and so on. Instead, we want React to do something like this:

let domNode = domContainer.firstChild;
// Update existing host instance
domNode.className = 'red';

In other words, React needs to decide when to update an existing host instance to match a new React element, and when to create a new one.

This raises a question of identity . The React element may be different every time, but when does it refer to the same host instance conceptually?

In our example, it’s simple. We used to render a <button> as a first (and only) child, and we want to render a <button> in the same place again. We already have a <button> host instance there so why re-create it? Let’s just reuse it.

This is pretty close to how React thinks about it.

If an element type in the same place in the tree “matches up” between the previous and the next renders, React reuses the existing host instance.

Here is an example with comments showing roughly what React does:

// let domNode = document.createElement('button');
// domNode.className = 'blue';
// domContainer.appendChild(domNode);
ReactDOM.render(
  <button className="blue" />,
  document.getElementById('container')
);

// Can reuse host instance? Yes! (button → button)// domNode.className = 'red';ReactDOM.render(
  <button className="red" />,
  document.getElementById('container')
);

// Can reuse host instance? No! (button → p)// domContainer.removeChild(domNode);
// domNode = document.createElement('p');
// domNode.textContent = 'Hello';
// domContainer.appendChild(domNode);
ReactDOM.render(
  <p>Hello</p>,
  document.getElementById('container')
);

// Can reuse host instance? Yes! (p → p)// domNode.textContent = 'Goodbye';ReactDOM.render(
  <p>Goodbye</p>,
  document.getElementById('container')
);

The same heuristic is used for child trees. For example, when we update a <dialog> with two <button> s inside, React first decides whether to re-use the <dialog> , and then repeats this decision procedure for each child.

Conditions

If React only reuses host instances when the element types “match up” between updates, how can we render conditional content?

Say we want to first show only an input, but later render a message before it:

// First render
ReactDOM.render(
  <dialog>
    <input />
  </dialog>,
  domContainer
);

// Next render
ReactDOM.render(
  <dialog>
    <p>I was just added here!</p>    <input />
  </dialog>,
  domContainer
);

In this example, the <input> host instance would get re-created. React would walk the element tree, comparing it with the previous version:

  • dialog → dialog : Can reuse the host instance? Yes — the type matches.

    • input → p : Can reuse the host instance? No, the type has changed! Need to remove the existing input and create a new p host instance.
    • (nothing) → input : Need to create a new input host instance.

So effectively the update code executed by React would be like:

let oldInputNode = dialogNode.firstChild;dialogNode.removeChild(oldInputNode);
let pNode = document.createElement('p');
pNode.textContent = 'I was just added here!';
dialogNode.appendChild(pNode);

let newInputNode = document.createElement('input');dialogNode.appendChild(newInputNode);

This is not great because conceptually the <input> hasn’t been replaced with <p> — it just moved. We don’t want to lose its selection, focus state, and content due to re-creating the DOM.

While this problem has an easy fix (which we’ll get to in a minute), it doesn’t occur often in the React applications. It’s interesting to see why.

In practice, you would rarely call ReactDOM.render directly. Instead, React apps tend to be broken down into functions like this:

function Form({ showMessage }) {
  let message = null;
  if (showMessage) {
    message = <p>I was just added here!</p>;
  }
  return (
    <dialog>
      {message}
      <input />
    </dialog>
  );
}

This example doesn’t suffer from the problem we just described. It might be easier to see why if we use object notation instead of JSX. Look at the dialog child element tree:

function Form({ showMessage }) {
  let message = null;
  if (showMessage) {
    message = {
      type: 'p',
      props: { children: 'I was just added here!' }
    };
  }
  return {
    type: 'dialog',
    props: {
      children: [        message,        { type: 'input', props: {} }      ]    }
  };
}

Regardless of whether showMessage is true or false , the <input> is the second child and doesn’t change its tree position between renders.

If showMessage changes from false to true , React would walk the element tree, comparing it with the previous version:

  • dialog → dialog : Can reuse the host instance? Yes — the type matches.

    • (null) → p : Need to insert a new p host instance.
    • input → input : Can reuse the host instance? Yes — the type matches.

And the code executed by React would be similar to this:

let inputNode = dialogNode.firstChild;
let pNode = document.createElement('p');
pNode.textContent = 'I was just added here!';
dialogNode.insertBefore(pNode, inputNode);

No input state is lost now.

Lists

Comparing the element type at the same position in the tree is usually enough to decide whether reuse or re-create the corresponding host instance.

But this only works well if children positions are static and don’t re-order. In our example above, even though message could be a “hole”, we still knew that there the input goes after the message, and there are no other children.

With dynamic lists, we can’t be sure the order is ever the same:

function ShoppingList({ list }) {
  return (
    <form>
      {list.map(item => (
        <p>
          You bought {item.name}
          <br />
          Enter how many do you want: <input />
        </p>
      ))}
    </form>
  )
}

If the list of our shopping items is ever re-ordered, React will see that all p and input elements inside have the same type, and won’t know to move them. (From React’s point of view, the items themselves changed, not their order.)

The code executed by React to re-order 10 items would be something like:

for (let i = 0; i < 10; i++) {
  let pNode = formNode.childNodes[i];
  let textNode = pNode.firstChild;
  textNode.textContent = 'You bought ' + items[i].name;
}

So instead of re-ordering them, React would effectively update each of them. This can create performance issues and possible bugs. For example, the content of the first input would stay reflected in first input after the sort — even though conceptually they might refer to different products in your shopping list!

This is why React nags you to specify a special property called key every time you include an array of elements in your output:

function ShoppingList({ list }) {
  return (
    <form>
      {list.map(item => (
        <p key={item.productId}>          You bought {item.name}
          <br />
          Enter how many do you want: <input />
        </p>
      ))}
    </form>
  )
}

A key tells React that it should consider an item to be conceptually the same even if it has different positions inside its parent element between renders.

When React sees <p key="42"> inside a <form> , it will check if the previous render also contained <p key="42"> inside the same <form> . This works even if <form> children changed their order. React will reuse the previous host instance with the same key if it exists, and re-order the siblings accordingly.

Note that the key is only relevant within a particular parent React element, such as a <form> . React won’t try to “match up” elements with the same keys between different parents. (React doesn’t have idiomatic support for moving a host instance between different parents without re-creating it.)

What’s a good value for a key ? An easy way to answer this is to ask: when would you say an item is the “same” even if the order changed? For example, in our shopping list, the product ID uniquely identifies it between siblings.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK