Skip to content
academia.sh

Lesson 04 / 20

The Style System

A style is a prop that determines how a node looks, and it is compared in the tree like any other prop. The lesson measures that a new style object with the same content leaves the node kept, and that a single changed field leaves it updated.

Published Updated

In this lesson

  • Explains that a style is a prop of the node in the tree and that this prop carries the look
  • Measures that a new style object with the same content leaves the node kept
  • Shows that a change in a single style field leaves the node updated
  • Verifies by counting that a style change does not touch sibling nodes or the subtree
Contents

At the end of the previous lesson we said that a node’s props are plain values: a text, an image source. A style is a prop too, but its value is not a plain value, it is an object: it carries fields such as width, color, and margin together. The question is this: how does the comparison rule read an object? Are two separate objects that carry the same fields the same or different? The answer is hidden in the rule’s fourth question, where it looks at the props, and this lesson measures it.

In the framework, styles are usually defined with a helper and given as a prop of the node. The snippet below is not measured; it only shows where a style stands on the screen.

// Platform code: styles
const styles = StyleSheet.create({
  row: { flexDirection: "row", padding: 8 },
  box: { width: 40, height: 40, backgroundColor: "teal" },
});

function Strip() {
  return (
    <View style={styles.row}>
      <View style={styles.box} />
      <Text>Label</Text>
    </View>
  );
}

We read the snippet line by line. StyleSheet.create takes an object with field names and gathers the styles in one place, named; row and box are style names. flexDirection: "row" determines the main axis of the flexbox layout: the children are lined up side by side. Flexbox layout is the layout rule that places a container’s children along an axis; instead of writing each child’s position one by one, we tell the container the direction. padding: 8 leaves space inside the container. The box style gives the box its size and color. Two places inside the function matter: style={styles.row} binds the container’s style, and style={styles.box} binds the box’s style, to the node’s style prop. No style is given to the Text node; the framework’s default applies.

A Style Is a Prop

The consequence of this snippet is important. A style is not a separate language or a separate channel; it is an object among the node’s props. When a style changes, what happens in the tree is no different from another prop changing. If the node’s type is the same and its key is the same, the rule looks at the props; if all of them, the style included, are the same, the node is kept, and if one is different, it is updated. The core is needed first for the measurement.

// fate.mjs — classifies what an update does to each node of the view tree
export const FATES = ["kept", "updated", "mounted", "unmounted"];

export function el(type, props = {}, children = [], key = null) {
  return { type, props, children, key };
}

function segment(node) {
  return node.key === null ? node.type : `${node.type}[${node.key}]`;
}

function identity(node, index) {
  return node.key === null ? `#${index}` : `key:${node.key}`;
}

function sameProps(a, b) {
  const names = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
  return names.every((n) => JSON.stringify(a[n]) === JSON.stringify(b[n]));
}

function whole(node, path, fate, ops) {
  const here = `${path}/${segment(node)}`;
  ops.push({ fate, path: here });
  node.children.forEach((child) => whole(child, here, fate, ops));
}

export function reconcile(prev, next, path = "", ops = []) {
  if (prev === null) {
    whole(next, path, "mounted", ops);
    return ops;
  }
  if (next === null) {
    whole(prev, path, "unmounted", ops);
    return ops;
  }
  if (prev.type !== next.type || prev.key !== next.key) {
    whole(prev, path, "unmounted", ops);
    whole(next, path, "mounted", ops);
    return ops;
  }
  const here = `${path}/${segment(next)}`;
  ops.push({ fate: sameProps(prev.props, next.props) ? "kept" : "updated", path: here });
  const before = new Map(prev.children.map((child, i) => [identity(child, i), child]));
  const matched = new Set();
  next.children.forEach((child, i) => {
    const id = identity(child, i);
    matched.add(id);
    reconcile(before.has(id) ? before.get(id) : null, child, here, ops);
  });
  prev.children.forEach((child, i) => {
    if (!matched.has(identity(child, i))) reconcile(child, null, here, ops);
  });
  return ops;
}

export function printOps(ops) {
  ops.forEach((op) => console.log(`${op.fate}: ${op.path}`));
}

export function countFates(ops) {
  for (const name of FATES) {
    console.log(`${name}: ${ops.filter((op) => op.fate === name).length}`);
  }
  console.log(`commands: ${ops.filter((op) => op.fate !== "kept").length}`);
}

This lesson’s helper file produces a strip tree from direction, width, and color state; the second function gives the same style to two boxes at once. The strip is a Row node; it carries the direction as a prop. Its first child is a Box, the second is the label text. The box’s style is created as a new object on every call, just as it is on every run of a render function.

// strip.mjs — produces a strip tree from direction, width and color
import { el } from "./fate.mjs";

export function renderStrip(state) {
  return el("Row", { direction: state.direction ?? "row" }, [
    el("Box", { style: { width: state.width, color: state.color } }),
    el("Text", { value: state.label }),
  ]);
}

export function renderPair(state) {
  const style = { width: state.width, color: state.color };
  return el("Row", { direction: "row" }, [
    el("Box", { style }),
    el("Box", { style }),
  ]);
}

Several Fields at Once

An update can change several fields of a style object. In the first measurement both the width and the color of the box change; the sibling text and the container stay the same.

// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStrip } from "./strip.mjs";

const before = renderStrip({ width: 40, color: "teal", label: "Label" });
const after = renderStrip({ width: 80, color: "orange", label: "Label" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
kept: /Row
updated: /Row/Box
kept: /Row/Text
kept: 2
updated: 1
mounted: 0
unmounted: 0
commands: 1

Two fields changed but the command is one. The fate is per node: how many fields changed in a node does not increase how many commands go. In a real app the update command carries which fields changed and the native side applies them one by one; that work is not measured in this lesson. What is measured here is the node coming out updated.

The Container’s Style

A common misconception is to think that when a style changes, everything beneath it changes too. When the container’s direction turns from row to column, the children go to other places on the screen; so in the tree too it looks as if all of them have changed. We measure it.

// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStrip } from "./strip.mjs";

const before = renderStrip({ direction: "row", width: 40, color: "teal", label: "Label" });
const after = renderStrip({ direction: "column", width: 40, color: "teal", label: "Label" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
updated: /Row
kept: /Row/Box
kept: /Row/Text
kept: 2
updated: 1
mounted: 0
unmounted: 0
commands: 1

Only /Row came out updated; because the children’s own props did not change, both are kept. Their moving on the screen is not a fate in the tree. Recalculating the layout is the native side’s work and is a consequence of the container being updated. The tree says whose prop changed; it does not say whose position slid on the screen. This is the limit of the style’s measure in this course.

Here a distinction from M31/K01 is needed. There, whether two values were equal was the language’s own question: equality was an operation that asked whether one value was the same as another, and the answer to the question was a result in its own right. Here equality is only the rule’s input. If the rule finds the contents of two style objects equal, it does something with it: it leaves the node kept. What is measured is not the answer to equality but the fate that comes out of it.

A Shared Style, Two Nodes

Defining a style once and using it in many places is the real reason for this work. Instead of writing a margin or a color separately in every node, we use a single style that has a name. In the tree, however, this sharing is not visible: each node carries its own prop. When the style changes, every node that uses that style finds a difference within itself.

// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderPair } from "./strip.mjs";

const before = renderPair({ width: 40, color: "teal" });
const after = renderPair({ width: 40, color: "orange" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
kept: /Row
updated: /Row/Box
updated: /Row/Box
kept: 1
updated: 2
mounted: 0
unmounted: 0
commands: 2

A single style changed, but two nodes came out updated and the commands became two. A style being shared does not share the command: the rule looks at nodes, not at style names. This shows that gathering styles in one place gives ease of maintenance but does not reduce the update cost. If we want to keep two nodes together, we need to bind them to a single container and change the container’s style; in the earlier sections, the direction change had ended with exactly such a single command.

It is useful to think of the two style approaches side by side. In one, we write a shared prop to every node, and when we change it, all of them are updated. In the other, we put the prop on the container and leave the children to the layout rule. The answer to which is right depends on whether the look is truly shared or only coincidentally the same. The tree does not make this distinction; the count only shows the result of the choice. The decision still belongs to the person writing it.

Here is one more misconception: the thought that “I never changed the style, so the node is never updated”. Even if the node’s style stays the same, if its text, its image source, or its pressed state changes, the node comes out updated. The rule looks at all the props together; a style is only one of these props. The reverse is also true: a node whose only change is its style, with its content staying the same, is also updated.

A style that comes from state is bound by the same rule. When we want a button to look a different color when it is selected, we select the style with a condition: if the state is selected we give one style, otherwise the other. The tree again sees only the result. Which condition the selection came from, where the condition is written, or how many lines it takes does not concern the rule; what concerns it is whether the style object the node produced differs in content from the previous object. If the condition selects the same style every time, the node stays kept; if it selects a different style, it comes out updated. Defining a style and selecting a style are separate jobs, and the tree carries only the result of the selection.

The Bounding Measurement

The focus node is /Row/Box, and the measurement has two steps. The first produces the tree a second time with nothing changed. Each call creates a new style object for the box: two separate objects in memory, but with exactly the same fields.

// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStrip } from "./strip.mjs";

const state = { width: 40, color: "teal", label: "Label" };
const ops = reconcile(renderStrip(state), renderStrip(state));
printOps(ops);
countFates(ops);
kept: /Row
kept: /Row/Box
kept: /Row/Text
kept: 3
updated: 0
mounted: 0
unmounted: 0
commands: 0

All three nodes are kept, and the commands are zero. The model compares props field by field, looking at their contents; two objects being separate is not counted as a difference. Even though /Row/Box received a new style object, no command went to the native side. In the second step only the box’s color changes; the width, the sibling text, and the root’s state stay the same.

// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStrip } from "./strip.mjs";

const before = renderStrip({ width: 40, color: "teal", label: "Label" });
const after = renderStrip({ width: 40, color: "orange", label: "Label" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
kept: /Row
updated: /Row/Box
kept: /Row/Text
kept: 2
updated: 1
mounted: 0
unmounted: 0
commands: 1

A single field of the style object changed and /Row/Box came out updated; this is the lesson’s first real prop change. The two steps are read together: with the same content, zero commands; with one field different, one command. What makes the difference is the content, not the object being recreated. That is why this lesson’s fate is kept: a new object having arrived changes nothing; updated comes out only when a field is truly different.

CS6. The model compares style objects by content. A real framework may compare props by object identity; in that case, a style object recreated on every run could be counted as a difference every time. In this model there is no place that compares by object identity, and this lesson does not measure that behavior. What is measured is clear: what the rule does while the content is the same. One reason why defining a style once and reusing it is recommended in a real framework is this; but the number for that reasoning is not given here.

CS7. Real styles can also be given as an array, and the framework merges them into a single object. The model counts a style as a single plain object. The merging of arrays and the dropping of invalid fields are not visible in the tree.

A style change did not unmount and mount any node. As long as the type and key stay the same, however much the look changes, a node comes out at most updated. The way to unmount is to change the type or the key; the type change was measured in the previous lesson. The model builds this fate’s rule; the model is not the framework itself.

In short, the style system is not a mechanism outside the tree. Styles may stand in a separate file in terms of where they are defined, in a named table; when they enter the tree, they become only a prop value. The comparison does not look at where the style is defined, it looks at the value the node carries. For this reason every question about style comes down to the same three questions: is the type the same, is the key the same, is the content of the props the same? If the answer to all three is yes, the node stays kept, and where and how the style is written does not change the result.

One last note on style: changing a single field of a style and replacing the whole style object with another object can give the same result in the tree. The number of changed fields does not concern the rule; what concerns it is whether the object is equal in content to the previous object.

Summary

  • This lesson’s fate: kept. In the bounding measurement, a new style object with the same content left /Row/Box kept and the commands were zero; then, when a single field changed, the same node came out updated, with the lesson’s first real prop change.
  • A style is an object that stands among the node’s props; it is compared in the tree like any other prop.
  • The model compares objects by content: if the content is the same, a new object is not counted as a difference. Comparison by object identity is not in this model and is not measured.
  • When one or several fields change, the node comes out updated and a single command goes; siblings and children stay kept.
  • When the container’s direction changes, only the container is updated; the children sliding on the screen is not a fate in the tree.

Next Step

Until now we changed the tree ourselves: we gave the state by hand with two separate values. The next lesson, Touch and Interaction, covers the event that changes the tree: a touch changes a prop, and the lesson measures which nodes this change reaches.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close