gecko-dev/devtools/docs/frontend/react.md

5.7 KiB

We use React to write our user interfaces. In here you can find an explanation of why we chose React and a short primer on it. Additionally, we list best practices that all devtools code should adhere to when writing React.

Quick Intro

This is a very quick introduction on how to use React, but does not explain in-depth the concepts behind it. If you want more in-depth articles, I recommend the following links:

React embraces components as a way of thinking about UIs. Components are the center of everything: they are composable like functions, testable like JSON data, and provide lifecycle APIs for more complex scenarios.

A component can represent anything from a single item in a list to a complete virtualized grid that is made up of sub-components. They can be used to abstract out "behaviors" instead of UI elements (think of a Selectable component). React's API makes it easy to break up your UI into whatever abstractions you need.

The core idea of a component is simple: it's something that takes properties and returns a DOM-like structure.

function Item({ name, iconURL }) {
  return div({ className: "item" },
             img({ className: "icon", href: iconURL }),
             name);
}

The div and span functions refer to React.DOM.div and React.DOM.span. React provides constructors for all DOM elements on React.DOM. These conform to the standard API for creating elements: the first argument takes properties, and the rest are children.

You can see component composition kick in when using Item:

const Item = React.createFactory(require('./Item'));

function List({ items }) {
  return div({ className: "list" },
             items.map(item => Item({ name: item.name, icon: item.iconURL)));
}

You can use custom components exactly the same way you use native ones! The only difference is we wrapped it in a factory when importing instead of using the React.DOM functions. Factories are just a way of turning a component into a convenient function. Without factories, you need to do do React.createElement(Item, { ... }), which is exactly the same as Item({ ... }) if using a factory.

Rendering and Updating Components

Now that we have some components, how do we render them? You use React.render for that:

let items = [{ name: "Dubois", iconURL: "dubois.png" },
             { name: "Ivy", iconURL: "ivy.png" }];

React.render(List({ items: items }),
             document.querySelector("#mount"));

This renders a List component, given items, to a DOM node with an id of mount. Typically you have a top-level App component that is the root of everything, and you would render it like so.

What about updating? First, let's talk about data. The above components take data from above and render out DOM structure. If any user events were involved, the components would call callbacks passed as props, so events walk back up the hierarchy. The conceptual model is data goes down, and events come up.

You usually want to change data in response to events, and rerender the UI with the new data. What does that look like? There are two places where React will rerender components:

1. Any additional React.render calls. Once a component is mounted, you can call React.render again to the same place and React will see that it's already mounted and perform an update instead of a full render. For example, this code adds an item in response to an event and updates the UI, and will perform optimal incremental updates:

function addItem(item) {
  render([...items, item]);
}

function render(items) {
  React.render(List({ items: items,
                      onAddItem: addItem }),
               document.querySelector("#mount"));
}

render(items);

2. Changing component local state. This is much more common. React allows components to have local state, and whenever the state is changed with the setState API it will rerender that specific component. If you use component local state, you need to create a component with createClass:

const App = React.createClass({
  getInitialState: function() {
    return { items: [] };
  },

  handleAddItem: function(item) {
    const items = [...this.props.items, item];
    this.setState({ items: items });
  },

  render: function() {
    return List({ items: this.state.items,
                  onAddItem: this.handleAddItem });
  }
});

If you are using something like Redux to manage state this is handled automatically for you with the library you use to bind Redux with React. See more in Redux.

DOM Diffing

What does it mean when React "updates" a component, and how does it know which DOM to change? React achieves this with a technique called DOM diffing. This alleviates the need for the programmer to worry about how updates are actually applied to the DOM, and components can render DOM structure declaratively in response to data. In the above examples, when adding an item, React knows to only add a new DOM node instead of recreating the whole list each time.

DOM diffing is possible because our components return what's called "virtual DOM": a lightweight JSON structure that React can use to diff against previous versions, and generate minimal changes to the real DOM.

This also makes it really east to test components with a real DOM: just make sure the virtual DOM has what it should.

Next

Read the React Guidelines next to learn how to write React code specifically for the devtools.