Lesson 05 / 20
Touch and Interaction
A touch changes state, state changes a prop, and the prop leaves a node updated. The lesson measures that the pressed state updates only the button and that its siblings stay kept.
Published Updated
In this lesson
- Orders the chain from touch to tree as event, state, prop, and node
- Measures that the pressed state prop leaves only the button updated
- Verifies by counting that the button's siblings and the container stay kept
- Explains that the touch itself does not appear in the tree and that only its result does
Contents
Until now we put two trees side by side ourselves: the previous one and the next one. On a real screen, what produces the next tree is an event, most often a touch. The finger meets the screen, the framework passes the event to the handler of the relevant node, the handler changes the state, and the screen is described again from the new state. At the end of this chain the rule asks the same question again: which node is kept and which is updated?
The snippet below shows a button that responds to touch. The snippet is not measured.
// Platform code: Action
function Action({ label, onPress }) {
const [pressed, setPressed] = useState(false);
return (
<Pressable
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
onPress={onPress}
>
<Text>{pressed ? "Pressed" : label}</Text>
</Pressable>
);
}
Pressable is a container that responds to touch. onPressIn runs when the finger lands, onPressOut when it lifts, and onPress when a press is completed. The handlers change a state value, and this value determines the content of the node. A touch is not a node in the tree; only the trace it leaves is visible in the tree.
From Event to Node
The chain has four links: event, state, prop, node. The event is outside the tree. The state is kept outside the tree and causes the screen to be described again. The prop is the value the description gives to a node. The node is what the rule assigns a fate to. The model has no place for the event; there are only two consecutive descriptions. This has a benefit: however the event arrived, the fate is read from the difference between the two descriptions.
// 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 button tree from the pressed state. The screen consists of a title, a button, and a note line. The button carries the pressed prop; this prop is the face of the pressed state in the tree. The content of the title and the note is not affected by the event.
// button.mjs — produces a screen tree from the pressed state import { el } from "./fate.mjs"; export function renderButton(state) { return el("Screen", {}, [ el("Text", { value: "Order" }), el("Button", { label: state.label, pressed: state.pressed }), el("Text", { value: state.note }), ]); }
When the Finger Lifts
When the finger lifts, onPressOut runs and pressed becomes false again. The tree returns to its first form.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderButton } from "./button.mjs"; const pressedTree = renderButton({ label: "Confirm", pressed: true, note: "Two items" }); const releasedTree = renderButton({ label: "Confirm", pressed: false, note: "Two items" }); const ops = reconcile(pressedTree, releasedTree); printOps(ops.filter((op) => op.fate !== "kept")); countFates(ops);
updated: /Screen/Button kept: 3 updated: 1 mounted: 0 unmounted: 0 commands: 1
Pressing and releasing are symmetric: each is a single updated node and a single command. In this model a whole touch is two commands, and both go to the same node. The button was never unmounted and mounted; the same native view was updated twice.
The Changed Node, Not the Touched Node
A common misconception is to assume that the touched node will come out updated. The finger touched the button, so the button changed. But the rule looks not at the touch but at the difference between descriptions. Touching a button may change a text somewhere else on the screen, and the button itself may change none of its props. We measure it: the touch updates only the note line, and the button stays the same.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderButton } from "./button.mjs"; const before = renderButton({ label: "Confirm", pressed: false, note: "Two items" }); const after = renderButton({ label: "Confirm", pressed: false, note: "Three items" }); const ops = reconcile(before, after); printOps(ops); countFates(ops);
kept: /Screen kept: /Screen/Text kept: /Screen/Button updated: /Screen/Text kept: 3 updated: 1 mounted: 0 unmounted: 0 commands: 1
The button was the target of the touch and came out kept; the text node that carries the note came out updated. The touch target, that is, the area the finger touches, is not a prop of the tree; only the props of nodes stand in the tree. There is no link between the target and the fate; the link in between is built by the state the handler changes. For this reason, before predicting the number of commands, the question “which state changed” is asked, not “what was touched”.
We read one more snippet line by line. The Action function given at the start holds a pressed value with useState. onPressIn makes the value true and onPressOut makes it false; onPress runs the handler that comes from outside and fires once when the press is completed. The Text node shows a fixed text if pressed and its label if not. The function returns a single tree; the pressed form and the normal form are two separate trees, and the difference is only in the content of that text. The same job can also be done by giving the button a pressed prop; which form is preferred in the tree does not change the rule, it only changes which node the difference appears in.
One Touch, Many States
Now the touch changes not only the pressed state but also the note: when the press is completed, the handler changes the label and writes the updated order count to the note line.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderButton } from "./button.mjs"; const before = renderButton({ label: "Confirm", pressed: true, note: "Two items" }); const after = renderButton({ label: "Confirmed", pressed: false, note: "Three items" }); const ops = reconcile(before, after); printOps(ops); countFates(ops);
kept: /Screen kept: /Screen/Text updated: /Screen/Button updated: /Screen/Text kept: 2 updated: 2 mounted: 0 unmounted: 0 commands: 2
A single event changed three fields: the button’s label, the button’s pressed prop, and the content of the note. Two nodes came out updated in the tree; the commands are two. The button’s two fields were counted as one command; the fate is per node. The title stayed kept again. How many nodes a touch reaches depends on which states the handler changes; the touch itself does not determine a number.
A distinction needs to be made here. What determines the number is not the event but the state difference the event leaves. The same touch can cause zero commands on one screen and five commands on another. The number of commands is not predicted by looking at the touch; the two descriptions have to be compared.
If the Same State Arrives Again
Touch events arrive frequently. While the finger slides slightly on the screen, the framework may pass the same event several times in a row, and the handler rewrites the state each time. Writing the state and changing the state are not the same thing: when the handler makes the pressed value true and the value is already true, no difference arises in the tree. We measure it.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderButton } from "./button.mjs"; const first = renderButton({ label: "Confirm", pressed: true, note: "Two items" }); const again = renderButton({ label: "Confirm", pressed: true, note: "Two items" }); const ops = reconcile(first, again); printOps(ops); countFates(ops);
kept: /Screen kept: /Screen/Text kept: /Screen/Button kept: /Screen/Text kept: 4 updated: 0 mounted: 0 unmounted: 0 commands: 0
All four nodes are kept, and the commands are zero. Writing the same value again produces a new description, but because the description is equal to the previous one, the rule sends no command. This is the counterpart on the touch side of what the first lesson taught, that a re-render and a command are separate things: however often the events arrive, it is the difference that determines the command. Whether a heavy stream of events puts a load on the tree depends not on the number of events but on how different the state left by the events is.
A Press Is Three Events
To understand a press as a whole, the events have to be separated. onPressIn runs when the finger lands, onPressOut when it lifts. If the finger slides outside the target between the two, the press is canceled and onPress never runs. If the finger lifts inside the target, the press counts as completed and onPress runs. So the pressed feedback is always undone, but the action is taken only on a completed press. Two separate things must not be confused: the feedback is a prop change in the tree, while the action is the real work that changes the application’s state.
This distinction is reflected in the result in the tree as well. A canceled press amounts to the pressed form being opened and closed in the tree: two updates, no lasting difference. A completed press, in addition to these two updates, carries the difference that comes with the action changing the state; many of the states we measured above were of this kind. Telling the two presses apart by looking only at the tree is possible if something changed in the end; if nothing changed, both leave the same two commands.
The size of the touch target is also a design matter. A small target makes it hard to hit with a finger, and the user may touch the wrong place. The framework allows defining extra space to enlarge the target; this space enlarges the area the finger can touch without enlarging the look. This value may appear in the tree as a prop, but it was not taken into the model in this lesson. The reason: the rule looks only at whether a prop has changed, and the target size changing on its own is a rarely seen situation. What matters is to remember that nothing not written in the tree receives a fate; enlarging the target is the native side’s job, and it either never enters the tree or enters it as a prop.
The Bounding Measurement
The focus node is /Screen/Button, and the measured moment is the onPressIn moment. The state changes only one field: pressed goes from false to true.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderButton } from "./button.mjs"; const before = renderButton({ label: "Confirm", pressed: false, note: "Two items" }); const after = renderButton({ label: "Confirm", pressed: true, note: "Two items" }); const ops = reconcile(before, after); printOps(ops); countFates(ops);
kept: /Screen kept: /Screen/Text updated: /Screen/Button kept: /Screen/Text kept: 3 updated: 1 mounted: 0 unmounted: 0 commands: 1
Of four nodes, three are kept and one is updated; the commands are one. The touch makes the whole screen be described again starting from the root, but the rule finds a difference only in /Screen/Button. The title, the note, and the container are described again, compared in the tree, and come out kept. This is why describing again is cheap: the expensive thing is the native command, and it goes only where there is a difference.
It is also possible to keep the same feedback only on the native side: the pressed look changes inside the native view itself and never returns to the shared code. In that case nothing changes in the tree and the button stays kept. This is not measured, because there is no difference to measure: the rule sees only what is in the tree. Writing the pressed state into the tree as a prop is a choice, not a necessity; it was written in this lesson so that the trace the touch leaves in the tree could be shown.
CS8. The model carries pressed as an ordinary prop. In the real framework the pressed state is often handled on the native side and changes the look without ever returning to the shared code. In that case no command is visible in the tree either. The model keeps this path closed and brings the pressed state into the tree deliberately; what is measured is what happens if the state enters the tree.
CS9. The model does not run the event’s handler. It gives the state the handler produces directly to the describing function. In the real framework, state updates can be batched and several touches can merge into a single new description; the model counts each state as a separate description.
The model builds this fate’s rule; the model is not the framework itself.
The practical rule that comes out of this lesson’s measurements is short: to see the trace a touch leaves in the tree, one must first ask which state changed, and then which node reads that state. If the answers to the two questions point to the same node, that node is updated. If they point to different nodes, the touched node stays kept. If they point to no node, that is, if the function that produces the tree does not read the changed state, the tree comes out the same and the commands are zero; the counter measurement in the first lesson was exactly this case.
Finally, we note that in all the measurements the touch itself has no fate. A fate belongs only to nodes. As long as a touch does not turn into a trace in the tree, that is, as long as it does not change the value of a prop, it counts as not having happened for the rule.
Summary
- This lesson’s fate: updated. In the bounding measurement, because the pressed state was written to the tree as a prop,
/Screen/Buttoncame out updated and its siblings and the container stayed kept; if the same feedback had been kept only on the native side, the node would have stayed kept. - The chain is event, state, prop, and node; in the tree only the prop and the node are visible.
- When the pressed state is carried as a prop, a touch leaves a single node updated and a single command goes.
- Pressing and releasing are separate updates, one command each; the button is never unmounted and mounted.
- What determines the number of commands is not the touch but the state difference the touch leaves.
Next Step
Until now, two descriptions belonged to the same condition of the same framework. In the last lesson, Platform-Specific Code, we cover the case where the same screen is described differently on two platforms, and we measure what the difference leaves in the tree.
To keep your progress and take notes, Log in
My notes
Log in to take notes.