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
useStateto store local stateuseEffectto 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
propsfrom parent - only one level deep, to avoid intermediate components passing through
propsthat 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
reduxormobx - 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
useStateanduseEffectare 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
createContextanduseContextto 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 toO(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
componentdirective, 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
$digestcycle ensures UI and state are synchronized- grouped components: access group leader's state
{`component("inner", { require: { sharedState: "^^outer" } })`}- also
$scopeinheritance, if not usingcomponentarchitecture - transclusion:
ngTransclude
# Svelte equivalents
- own JS-like language, compiled into JS, no browser runtime
- single component: local state and behaviour
- nested components:
exportvariables as props - distant components: shared in out-of-tree
svelte/store - grouped components:
setContextandgetContext - transclusion:
<slot>