Frontend trees with React

# Document tree

  • frontend GUIs are inherently trees
    • can contain nested and peer components
  • let's consider the operations that are possible on tree
    • and how React supports those operations

# Single component

  • a component is a small sub-tree of DOM elements
  • has encapsulated state and behaviour
    • useState to store local state
    • useEffect to configure behaviour
  • every instance has its own state and behaviour
  • can have independent instances in parallel

# Example: single component

import { useEffect, useState } from "react"

function MyComponent () {
const [value, setValue] = useState(0)
const increment = () => { setValue(value + 1) }
useEffect(() => { console.log("mounted") }, [])
return (
<div>
<button onClick={increment}>+</button>
<p>{value}</p>
</div>
)
}

# Nested components

  • every component with exception of root component, has a parent
  • state and behaviour supplied as props from parent
  • only one level deep, to avoid intermediate components passing through props that it itself doesn't use
  • analogous to function calls

# Example: child component

import { useState } from "react"

function MyComponent ({ initialValue, onCountDown }) {
const [value, setValue] = useState(initialValue)
const count = () => {
const newValue = value - 1
setValue(newValue)
if (newValue === 0) {
onCountDown()
}
}
return <button onClick={count}>{value}</button>
}

# Distant components

  • to share common state and behaviour, distant components (like distant relatives) need to use an out-of-tree store
    • commonly redux or mobx
  • store publish/subscribe mechanism ensures that relevant components are re-rendered when state changes
  • if not careful, can have poor performance; any change in store affects all components
  • also called dependency injection; each component pulls in its own dependencies (cf. supplied via props)
  • analogous to global variables

# Example: out-of-tree store

// using hooks
function DisplayComponent () {
const { value } = useShared()
return <span>{value}</span>
}


function ActionComponent () {
const { action } = useShared()
return <button onClick={action}>Do</button>
}
// using higher-order-components
const DisplayComponent = withShared(
function ({ value }) {
return <span>{value}</span>
}
)

const ActionComponent = withShared(
function ({ action }) {
return <button onClick={action}>Do</button>
}
)

# Example: React state hooks

  • React hooks useState and useEffect are actually examples of out-of-tree stores
  • that's why for the same component, hooks need to be consistently called from one render to the next
  • cleverly disguised using order of invocation to self-register and deregister the appropriate state and behaviour
  • except here used without sharing with other components

# Grouped components

  • some components are always paired/grouped in a sub-tree
    • createContext and useContext to share state and behaviour within group, owned by group leader
  • can have independent instances of the group in parallel
  • good performance: only sub-tree is affected on change
  • analogous to closures 

# Example: grouped context

function MyComponent () {
return (
<>
{/* all members are red, regardless of nesting */}
<Group color="red">
<Member />
<Subgroup>
<Member />
</SubGroup>
</Group>

{/* all members are green */}
<Group color="green">
<Member />
<Member />
</Group>
</>
)
}

# Transclusion

  • React components is just javascript
    • share components as props
    • along with "regular" state and behaviour
  • component tree is dynamic at runtime 

# Example: render props

import { useState } from "react"

function Layout ({ DynamicComponent, state, behaviour }) {
const [internal, setInternal] = useState()
return (
<StaticComponent onClick={behaviour}>
{state}
<DynamicComponent value={internal} />
</StaticComponent>
)
}

# Portals

  • usually want rendered output "here", but sometimes want it elsewhere 
    • eg., modals need a background, and is sensitive to its location in the DOM tree
  • DOM tree can be different to React tree

# Example: specific DOM element

// render feature based on React tree
function MyComponent ({ operation }) {
return (
<Feature>
You're about to do
{operation}.

{/* but render modal based on DOM tree, via portal */}
<Modal>Are you sure about {operation}?</Modal>
</Feature>
)
}

# Event capture & bubbling

  • especially with render props and portals, DOM tree can be different to React tree
  • synthetic events  capture & bubble along React tree

# Performance

  • in dynamic application, React tree changes in response to user action and data
  • full tree-diffing algorithm is O(n^3), use React keys  to reduce to O(n)
  • eg: remove element B from a list of A, B, & C
    • naive algorithm will keep A, update B to become C, then remove old C
    • better to simply remove B
  • implicitly keyed by components, or explicitly keyed by external keys to enforce render stability
  • can also be used to destroy components
    • deliberately change key to avoid state pollution from one render to the next
  • changes are batched in Virtual DOM, then flushed to DOM periodically for performance

# Summary

  • building frontend applications == building trees
    • single component: local state and behaviour
    • nested components: parent/child relationship using props
    • distant components: shared state and behaviour with out-of-tree store
      • use hooks or higher order components
      • pub/sub ensures components are updated
    • grouped components: shared state and behaviour within sub-tree using contexts from group leader
    • transclusion: dynamic sub-trees at runtime using render props
    • portals: DOM tree !== React tree
    • events: capture & bubble through React tree
    • performance: what to keep/discard as tree changes using keys, virtual DOM
  • Preact  does not support Portals, so DOM tree == Preact tree

# AngularJS equivalents

  • template driven: adds JS-to-HTML (cf. React adds HTML-to-JS)
    • single component: special component directive, instance variables in $ctrl
    • nested components: bi-directional bindings between controllers
    • distant components: providers and dependency injection
      • shared state in out-of-tree providers, injected into components
      • $digest cycle ensures UI and state are synchronized
    • grouped components: access group leader's state
      • {`component("inner", { require: { sharedState: "^^outer" } })`}
      • also $scope inheritance, if not using component architecture
    • transclusion: ngTransclude

# Svelte equivalents

  • own JS-like language, compiled into JS, no browser runtime
    • single component: local state and behaviour
    • nested components: export variables as props
    • distant components: shared in out-of-tree svelte/store
    • grouped components: setContext and getContext
    • transclusion: <slot>