Skip to main content

React Basic Notes

Props and States

SetState

  • setState synchronous way: when it comes blocking mode (ReactDOM.createBlockingRoot(rootNode).render(<App />)), setState works in synchronous mode: scheduleUpdateOnFiber -> ensureRootIsScheduled -> flushSyncCallbackQueue.
  • setState asynchronous way: at most of the other time, setState works in asynchronous mode, including legacy mode(ReactDOM.render(<App />, rootNode)) and concurrent mode(ReactDOM.createRoot(rootNode).render(<App />)).
  • 在异步模式下, 为了防止子组件在处理事件时多次渲染, 将多个 setState (包括父组件) 移到浏览器事件之后执行 (Batched Updates: 此时 React 内部变量 isBatchingUpdates 变成 true), 可以提升 React 性能. 未来会在更多的可以 Batched Updates 的场景下将 setState 设为异步执行, 所以编写代码时最好将 setState 总是当做异步执行函数.
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0,
}
}

componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 第 1 次 log

this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 第 2 次 log

setTimeout(() => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 第 3 次 log

this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 第 4 次 log
}, 0)
}

render() {
return <div>Example</div>
}
}

// => 0 0 2 3
State Structure Principles

Principles for structuring state:

  • Group related state.
  • Avoid contradictions in state.
  • Avoid duplication in state.
  • Avoid redundant state.
  • Avoid deeply nested state.

componentDidMount

  • Don't setState directly in this method.
  • Can use setInterval/setTimeout/AJAX request/fetch in this method, and call setState as callback inside these functions.
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
error: null,
isLoaded: false,
items: [],
}
}

componentDidMount() {
fetch('https://api.example.com/items')
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items,
})
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error,
})
},
)
}

render() {
const { error, isLoaded, items } = this.state
if (error) {
return (
<div>
Error:
{error.message}
</div>
)
} else if (!isLoaded) {
return <div>Loading...</div>
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name}
{' '}
{item.price}
</li>
))}
</ul>
)
}
}
}

Props Validation

  • React.PropTypes.array/bool/func/number/object/string/symbol/node/element.
  • React.PropTypes.any.isRequired.
  • React.PropTypes.objectOf(React.PropsTypes.number).
  • React.PropTypes.arrayOf(React.PropsTypes.number).
  • React.PropTypes.instanceOf/oneOf/oneOfType(type).

Element and Component

React Element 实际上是纯对象, 可由 React.createElement()/JSX/Element Factory Helper 创建, 并被 React 在必要时渲染成真实的 DOM Nodes.

type ReactInternalType =
| 'react.element'
| 'react.portal'
| 'react.fragment'
| 'react.strict_mode'
| 'react.profiler'
| 'react.provider'
| 'react.context'
| 'react.forward_ref'
| 'react.suspense'
| 'react.suspense_list'
| 'react.memo'
| 'react.lazy'
| 'react.block'
| 'react.server.block'
| 'react.fundamental'
| 'react.scope'
| 'react.opaque.id'
| 'react.debug_trace_mode'
| 'react.offscreen'
| 'react.legacy_hidden'

export interface ReactElement<Props> {
$$typeof: any
key: string | number | null
type:
| string
| ((props: Props) => ReactElement<any>)
| (new (props: Props) => ReactComponent<any>)
| ReactInternalType
props: Props
ref: Ref

// ReactFiber
_owner: any

// __DEV__
_store: { validated: boolean }
_self: React$Element<any>
_shadowChildren: any
_source: Source
}
ReactDOM.render(
{
type: Form,
props: {
isSubmitted: false,
buttonText: 'OK!',
},
},
document.getElementById('root'),
)

// React: You told me this...
const FormElement = {
type: Form,
props: {
isSubmitted: false,
buttonText: 'OK!',
},
}

// React: ...And Form told me this...
const ButtonElement = {
type: Button,
props: {
children: 'OK!',
color: 'blue',
},
}

// React: ...and Button told me this! I guess I'm done.
const HTMLButtonElement = {
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!',
},
},
},
}

JSX

在 JSX 中, 小写标签被认为是 HTML 标签. 但是, 含有 . 的大写和小写标签名却不是.

  • <component />: 转换为 React.createElement('component') (e.g HTML native tag).
  • <obj.component />: 转换为 React.createElement(obj.component).
  • <Component />: 转换为 React.createElement(Component).

JSX Transform

import React from 'react'

function App() {
return React.createElement('h1', null, 'Hello world')
}
// Inserted by a compiler
import { jsx as _jsx } from 'react/jsx-runtime'

function App() {
return _jsx('h1', { children: 'Hello world' })
}

ESLint config for new JSX transform:

{
"rules": {
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off"
}
}

TypeScript config for new JSX transform:

{
"include": ["./src/**/*"],
"compilerOptions": {
"module": "esnext",
"target": "es2015",
"jsx": "react-jsx",
"strict": true
}
}

Functional and Class component

  • 函数型组件没有实例, 类型组件具有实例, 但实例化的工作由 react 自动完成
  • With React Hooks, functional component can get state, lifecycle hooks and performance optimization consistent to class component.

Stateless and Stateful component

React Component definition:

  • React.Component.
  • React.PureComponent.
interface NewLifecycle<P, S, SS> {
getSnapshotBeforeUpdate?: (
prevProps: Readonly<P>,
prevState: Readonly<S>,
) => SS | null

componentDidUpdate?: (
prevProps: Readonly<P>,
prevState: Readonly<S>,
snapshot?: SS,
) => void
}

interface ComponentLifecycle<P, S, SS = any> extends NewLifecycle<P, S, SS> {
componentDidMount?: () => void

shouldComponentUpdate?: (
nextProps: Readonly<P>,
nextState: Readonly<S>,
nextContext: any,
) => boolean

componentWillUnmount?: () => void

componentDidCatch?: (error: Error, errorInfo: ErrorInfo) => void
}

class Component<P = object, S = object, SS = any> extends ComponentLifecycle<
P,
S,
SS
> {
readonly props: Readonly<P> & Readonly<{ children?: ReactNode | undefined }>
state: Readonly<S>

static contextType?: Context<any> | undefined
context: any

constructor(props: Readonly<P> | P)

setState<K extends keyof S>(
state:
| ((prevState: Readonly<S>, props: Readonly<P>) => Pick<S, K> | S | null)
| (Pick<S, K> | S | null),
callback?: () => void,
): void

forceUpdate(callback?: () => void): void

render(): ReactNode
}

class PureComponent<P = object, S = object, SS = any> extends Component<
P,
S,
SS
> {}

Stateless component

采用函数型声明, 不使用 setState(), 一般作为表现型组件.

Stateful component

  • 采用类型声明, 使用 setState(), 一般作为容器型组件(containers)
  • 结合 Redux 中的 connect 方法, 将 store 中的 state 作为此类组件的 props
class Component {
render() {
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment,
}))

return <div>Component</div>
}
}

Component Lifecycle

  • Reconciliation phase:
    • constructor.
    • getDerivedStateFromProps.
    • getDerivedStateFromError.
    • shouldComponentUpdate.
    • ClassComponent render function.
    • setState updater functions.
    • FunctionComponent body function.
    • useState/useReducer/useMemo updater functions.
    • UNSAFE_componentWillMount.
    • UNSAFE_componentWillReceiveProps.
    • UNSAFE_componentWillUpdate.
  • Commit phase:
    • componentDidMount.
    • getSnapshotBeforeUpdate.
    • componentDidUpdate.
    • componentWillUnmount.
    • componentDidCatch.

因为协调阶段可能被中断与恢复, 甚至重做, React 协调阶段的生命周期钩子可能会被调用多次, 协调阶段的生命周期钩子不要包含副作用: e.g fetch promises, async functions. 通过 React.StrictMode 可以自动检测应用中隐藏的问题.

React Component Lifecycle

Creation and Mounting Phase

constructor(props, context) -> static getDerivedStateFromProps() -> render() -> componentDidMount().

Updating Phase

Update for three reasons:

  • Parent/top components (re-)rendering.
  • this.setState() called.
  • this.forceUpdate() called.

static getDerivedStateFromProps() -> shouldComponentUpdate(nextProps, nextState) -> render() -> getSnapshotBeforeUpdate() -> componentDidUpdate(prevProps, prevState).

getSnapshotBeforeUpdate(): 在最新的渲染输出提交给 DOM 前将会立即调用, 这对于从 DOM 捕获信息(比如:滚动位置)很有用.

Unmounting Phase

componentWillUnmount().

Error Handling Phase

static getDerivedStateFromError() -> componentDidCatch().

Render Function

  • Default render behavior (without any memo/useMemo/PureComponent): when a parent component renders, React will recursively render all child components inside of it (because props.children is always a new reference when parent re-rendering).
  • Render logic:
    • Can't mutate existing variables and objects.
    • Can't create random values like Math.random() or Date.now().
    • Can't make network requests.
    • Can't queue state updates.

React Element API

React Clone Element API

Modify children properties:

function CreateTextWithProps({
text,
ASCIIChar,
...props
}: {
text: string
ASCIIChar: string
}) {
return (
<span {...props}>
{text}
{ASCIIChar}
</span>
)
}

function RepeatCharacters({ times, children }) {
return React.cloneElement(children, {
ASCIIChar: children.props.ASCIIChar.repeat(times),
})
}

export default function App() {
return (
<div>
<RepeatCharacters times={3}>
<CreateTextWithProps text="Foo Text" ASCIIChar="." />
</RepeatCharacters>
</div>
)
}
function RadioGroup({
name,
children,
}: {
name: string
children: ReactElement
}) {
const RenderChildren = () =>
React.Children.map(children, (child) => {
return React.cloneElement(child, {
name,
})
})

return (
<div>
<RenderChildren />
</div>
)
}

function RadioButton({
value,
name,
children,
}: {
value: string
name: string
children: ReactElement
}) {
return (
<label>
<input type="radio" value={value} name={name} />
{children}
</label>
)
}

export default function App() {
return (
<RadioGroup name="numbers">
<RadioButton value="first">First</RadioButton>
<RadioButton value="second">Second</RadioButton>
<RadioButton value="third">Third</RadioButton>
</RadioGroup>
)
}

React Children API

  • React.Children.toArray(children).
  • React.Children.forEach(children, fn).
  • React.Children.map(children, fn).
  • React.Children.count(children).
  • React.Children.only(children).
import { Children, cloneElement } from 'react'

function Breadcrumbs({ children }: { children: ReactElement }) {
const arrayChildren = Children.toArray(children)

return (
<ul
style={{
listStyle: 'none',
display: 'flex',
}}
>
{Children.map(arrayChildren, (child, index) => {
const isLast = index === arrayChildren.length - 1

if (!isLast && !child.props.link) {
throw new Error(
`BreadcrumbItem child no. ${index + 1}
should be passed a 'link' prop`,
)
}

return (
<>
{child.props.link
? (
<a
href={child.props.link}
style={{
display: 'inline-block',
textDecoration: 'none',
}}
>
<div style={{ marginRight: '5px' }}>
{cloneElement(child, {
isLast,
})}
</div>
</a>
)
: (
<div style={{ marginRight: '5px' }}>
{cloneElement(child, {
isLast,
})}
</div>
)}
{!isLast && <div style={{ marginRight: '5px' }}></div>}
</>
)
})}
</ul>
)
}

function BreadcrumbItem({
isLast,
children,
}: {
isLast: boolean
children: ReactElement
}) {
return (
<li
style={{
color: isLast ? 'black' : 'blue',
}}
>
{children}
</li>
)
}

export default function App() {
return (
<Breadcrumbs>
<BreadcrumbItem link="https://example.com/">Example</BreadcrumbItem>
<BreadcrumbItem link="https://example.com/hotels/">Hotels</BreadcrumbItem>
<BreadcrumbItem>A Fancy Hotel Name</BreadcrumbItem>
</Breadcrumbs>
)
}

Refs

Refs 用于返回对元素的引用. 但在大多数情况下, 应该避免使用它们. 当需要直接访问 DOM 元素或组件的实例时, 它们可能非常有用:

  • Managing focus, text selection, or media playback.
  • Triggering imperative animations.
  • Integrating with third-party DOM libraries.k

Ref 通过将 Fiber 树中的 instance 赋给 ref.current 实现

function commitAttachRef(finishedWork: Fiber) {
// finishedWork 为含有 Ref effectTag 的 Fiber
const ref = finishedWork.ref

// 含有 ref prop, 这里是作为数据结构
if (ref !== null) {
// 获取 ref 属性对应的 Component 实例
const instance = finishedWork.stateNode
let instanceToUse
switch (finishedWork.tag) {
case HostComponent:
// 对于 HostComponent, 实例为对应 DOM 节点
instanceToUse = getPublicInstance(instance)
break
default:
// 其他类型实例为 fiber.stateNode
instanceToUse = instance
}

// 赋值 ref
if (typeof ref === 'function')
ref(instanceToUse)
else ref.current = instanceToUse
}
}
class CssThemeProvider extends React.PureComponent<Props> {
private rootRef = React.createRef<HTMLDivElement>()

render() {
return <div ref={this.rootRef}>{this.props.children}</div>
}
}

String Refs

不建议使用 String Refs:

  • React 无法获取 this 引用, 需要持续追踪当前render出的组件, 性能变慢.
  • String Refs 不可组合化: if library puts ref on passed child, user can't put another ref on it. Callback Refs are perfectly composable.
  • String Refs don't work with static analysis: Flow can't guess the magic that framework does to make string ref appear on this.refs, as well as its type (which could be different). Callback Refs are friendly to static analysis.
class Foo extends Component {
render() {
return <input onClick={() => this.action()} ref="input" />
}

action() {
console.log(this.refs.input.value)
}
}
class App extends React.Component<{ data: object }> {
renderRow = (index) => {
// ref 会绑定到 DataTable 组件实例, 而不是 App 组件实例上
return <input ref={`input-${index}`} />

// 如果使用 function 类型 ref, 则不会有这个问题
// return <input ref={input => this['input-' + index] = input} />;
}

render() {
return <DataTable data={this.props.data} renderRow={this.renderRow} />
}
}

Forward Refs

不能在函数式组件上使用ref属性, 因为它们没有实例, 但可以在函数式组件内部使用ref. Ref forwarding 是一个特性, 它允许一些组件获取接收到 ref 对象并将它进一步传递给子组件.

// functional component
function Button({ children }: { children: ReactElement }, ref) {
return (
<button type="button" ref={ref} className="CustomButton">
{children}
</button>
)
}

const ButtonElement = React.forwardRef(Button)

// Create ref to the DOM button:
// get ref to `<button>`
const ref = React.createRef()
export default function App() {
return <ButtonElement ref={ref}>Forward Ref</ButtonElement>
}
type Ref = HTMLButtonElement
interface Props {
children: React.ReactNode
}

function Button({ children }: Props, ref: Ref) {
return (
<button type="button" ref={ref} className="MyClassName">
{children}
</button>
)
}

const FancyButton = React.forwardRef<Ref, Props>(Button)

export default FancyButton

Callback Refs

class UserInput extends Component {
setSearchInput = (input) => {
this.input = input
}

render() {
return (
<>
<input type="text" ref={this.setSearchInput} />
<button type="submit">Submit</button>
</>
)
}
}

Compound Components

Compound components example:

import * as React from 'react'

interface Props {
onStateChange?: (e: string) => void
defaultValue?: string
}

interface State {
currentValue: string
defaultValue?: string
}

interface RadioInputProps {
label: string
value: string
name: string
imgSrc: string
key: string | number
currentValue?: string
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
}

function RadioImageForm({
children,
onStateChange,
defaultValue,
}: React.PropsWithChildren<Props>): React.ReactElement {
const [state, setState] = React.useState<State>({
currentValue: '',
defaultValue,
})

// Memoized so that providerState isn't recreated on each render
const providerState = React.useMemo(
() => ({
onChange: (event: React.ChangeEvent<HTMLInputElement>): void => {
const value = event.target.value
setState({
currentValue: value,
})
onStateChange?.(value)
},
...state,
}),
[state, onStateChange],
)

return (
<div>
<form>
{React.Children.map(children, (child: React.ReactElement) =>
React.cloneElement(child, {
...providerState,
}),)}
</form>
</div>
)
}

function RadioInput({
currentValue,
onChange,
label,
value,
name,
imgSrc,
key,
}: RadioInputProps): React.ReactElement {
return (
<label className="radio-button-group" key={key}>
<input
type="radio"
name={name}
value={value}
aria-label={label}
onChange={onChange}
checked={currentValue === value}
aria-checked={currentValue === value}
/>
<img alt="" src={imgSrc} />
</label>
)
}

RadioImageForm.RadioInput = RadioInput

export default RadioImageForm
  • Compound components manage their own internal state, which they share among several child components.
  • When importing a compound component, automatically import child components available on compound component.
import type { CSSProperties, ReactNode } from 'react'
import React from 'react'

interface Props {
children: ReactNode
style?: CSSProperties
rest?: any
}

function Header({ children, style, ...rest }: Props): JSX.Element {
return (
<div style={{ ...style }} {...rest}>
{children}
</div>
)
}

function Body({ children, style, ...rest }: Props): JSX.Element {
return (
<div style={{ ...style }} {...rest}>
{children}
</div>
)
}

function Footer({ children, style, ...rest }: Props): JSX.Element {
return (
<div style={{ ...style }} {...rest}>
{children}
</div>
)
}

function getChildrenOnDisplayName(children: ReactNode[], displayName: string) {
return React.Children.map(children, child =>
child.displayName === displayName ? child : null,)
}

function Card({ children }: { children: ReactNode[] }): JSX.Element {
const header = getChildrenOnDisplayName(children, 'Header')
const body = getChildrenOnDisplayName(children, 'Body')
const footer = getChildrenOnDisplayName(children, 'Footer')

return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{body}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
)
}

Header.displayName = 'Header'
Body.displayName = 'Body'
Footer.displayName = 'Footer'
Card.Header = Header
Card.Body = Body
Card.Footer = Footer

function App() {
return (
<div>
<Card>
<Card.Header>Header</Card.Header>
<Card.Body>Body</Card.Body>
<Card.Footer>Footer</Card.Footer>
</Card>
</div>
)
}

export default App

React Synthetic Events

  • Events delegation:
    • React 16: delegate events handlers on document DOM node.
    • React 17: delegate events handlers on app root DOM node.
    • 先处理原生事件, 后处理 React 事件.
  • Events dispatching: dispatch native events to React.onXXX handlers by SyntheticEvent.
    • 收集监听器: const listeners = accumulateSinglePhaseListeners(targetFiber, eventName).
    • 派发合成事件: dispatchQueue.push({ new SyntheticEvent(eventName), listeners }).
    • 执行派发: processDispatchQueue(dispatchQueue, eventSystemFlags) -> executeDispatch(event, listener, currentTarget).
    • Capture event: 从上至下调用 Fiber 树中绑定的回调函数.
    • Bubble event: 从下至上调用 Fiber 树中绑定的回调函数.

React Synthetic Events

react-dom/src/events/DOMPluginEventSystem:

function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (enableEagerRootListeners) {
// 1. 节流优化, 保证全局注册只被调用一次.
if (rootContainerElement[listeningMarker])
return

rootContainerElement[listeningMarker] = true

// 2. 遍历 allNativeEvents 监听冒泡和捕获阶段的事件.
allNativeEvents.forEach((domEventName) => {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(
domEventName,
false, // 冒泡阶段监听.
rootContainerElement,
null,
)
}

listenToNativeEvent(
domEventName,
true, // 捕获阶段监听.
rootContainerElement,
null,
)
})
}
}

function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null,
eventSystemFlags?: EventSystemFlags = 0,
): void {
const target = rootContainerElement
const listenerSet = getEventListenerSet(target)
const listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener)

// 利用 Set 数据结构, 保证相同的事件类型只会被注册一次.
if (!listenerSet.has(listenerSetKey)) {
if (isCapturePhaseListener)
eventSystemFlags |= IS_CAPTURE_PHASE

// 注册事件监听.
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
)
listenerSet.add(listenerSetKey)
}
}

function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 1. 构造 listener.
const listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
)

// 2. 注册事件监听.
let unsubscribeListener

if (isCapturePhaseListener) {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
)
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
)
}
}

// 注册原生冒泡事件.
function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false)
return listener
}

// 注册原生捕获事件.
function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true)
return listener
}

react-dom/src/events/ReactDOMEventListener:

// 派发原生事件至 React.onXXX.
function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 1. 根据优先级设置 listenerWrapper.
const eventPriority = getEventPriorityForPluginSystem(domEventName)
let listenerWrapper

switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent
break
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate
break
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent
break
}

// 2. 返回 listenerWrapper.
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
)
}

function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
const previousPriority = getCurrentUpdatePriority()
const prevTransition = ReactCurrentBatchConfig.transition
ReactCurrentBatchConfig.transition = null

try {
setCurrentUpdatePriority(DiscreteEventPriority)
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
} finally {
setCurrentUpdatePriority(previousPriority)
ReactCurrentBatchConfig.transition = prevTransition
}
}

function dispatchContinuousEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
const previousPriority = getCurrentUpdatePriority()
const prevTransition = ReactCurrentBatchConfig.transition
ReactCurrentBatchConfig.transition = null

try {
setCurrentUpdatePriority(ContinuousEventPriority)
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
} finally {
setCurrentUpdatePriority(previousPriority)
ReactCurrentBatchConfig.transition = prevTransition
}
}

function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
let blockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)

if (blockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
)
clearIfContinuousEvent(domEventName, nativeEvent)
return
}

if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
nativeEvent.stopPropagation()
return
}

// We need to clear only if we didn't queue because queueing is accumulative.
clearIfContinuousEvent(domEventName, nativeEvent)

if (
eventSystemFlags & IS_CAPTURE_PHASE
&& isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn)

if (fiber !== null)
attemptSynchronousHydration(fiber)

const nextBlockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)

if (nextBlockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
)
}

if (nextBlockedOn === blockedOn)
break

blockedOn = nextBlockedOn
}

if (blockedOn !== null)
nativeEvent.stopPropagation()

return
}

dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
)
}

React Reusability Patterns

HOC

Higher Order Components.

Solve:

  • Reuse code with using ES6 classes.
  • Compose multiple HOCs.

Upside:

  • Reusable (abstract same logic).
  • HOC is flexible with input data (pass input data as parameters or derive it from props).

Downside:

  • Wrapper hell: withA(withB(withC(withD(Comp)))).
  • Implicit dependencies: which HOC providing a certain prop.
  • Name collision/overlap props: overwrite the same name prop silently.
  • HOC is not flexible with output data (to WrappedComponent).
// ToggleableMenu.jsx
function withToggleable(Clickable) {
return class Toggleable extends React.Component<{ children: ReactElement }> {
constructor() {
super()
this.toggle = this.toggle.bind(this)
this.state = { show: false }
}

toggle() {
this.setState(prevState => ({ show: !prevState.show }))
}

render() {
return (
<div>
<Clickable {...this.props} onClick={this.toggle} />
{this.state.show && this.props.children}
</div>
)
}
}
}

class NormalMenu extends React.Component<{ onClick: Function, title: string }> {
render() {
return (
<div onClick={this.props.onClick}>
<h1>{this.props.title}</h1>
</div>
)
}
}

const ToggleableMenu = withToggleable(NormalMenu)
export default ToggleableMenu
class Menu extends React.Component {
render() {
return (
<div>
<ToggleableMenu title="First Menu">
<p>Some content</p>
</ToggleableMenu>
<ToggleableMenu title="Second Menu">
<p>Another content</p>
</ToggleableMenu>
<ToggleableMenu title="Third Menu">
<p>More content</p>
</ToggleableMenu>
</div>
)
}
}

Render Props

Children/Props as render function:

Solve:

  • Reuse code with using ES6 classes.
  • Lowest level of indirection.
  • No naming collision.

e.g Context or ThemesProvider is designed base on Render Props.

Upside:

  • Separate presentation from logic.
  • Extendable.
  • Reusable (abstract same logic).
  • Render Props is flexible with output data (children parameters definition free).

Downside:

  • Wrapper hell (when many cross-cutting concerns are applied to a component).
  • Minor memory issues when defining a closure for every render.
  • Unable to optimize code with React.memo/React.PureComponent due to render() function always changes.
  • Render Props is not flexible with input data (restricts children components from using the data at outside field).
interface Props {
title: string
children: ReactElement
}

class Toggleable extends React.Component<Props> {
constructor() {
super()
this.toggle = this.toggle.bind(this)
this.state = { show: false }
}

toggle() {
this.setState(prevState => ({ show: !prevState.show }))
}

render() {
return this.props.children(this.state.show, this.toggle)
}
}

export default function ToggleableMenu({ title, children }: Props) {
return (
<Toggleable>
{(show, onClick) => (
<div>
<div onClick={onClick}>
<h1>{title}</h1>
</div>
{show && children}
</div>
)}
</Toggleable>
)
}
class Menu extends React.Component {
render() {
return (
<div>
<ToggleableMenu title="First Menu">
<p>Some content</p>
</ToggleableMenu>
<ToggleableMenu title="Second Menu">
<p>Another content</p>
</ToggleableMenu>
<ToggleableMenu title="Third Menu">
<p>More content</p>
</ToggleableMenu>
</div>
)
}
}

React Hooks

  • No wrapper hell: every hook is just one line of code.
  • No implicit dependencies: explicit one certain call for one certain hook.
  • No name collision and overlap props due to flexible data usage.
  • No need for JSX.
  • Flexible data usage.
  • Flexible optimization methods:
    • Avoid re-render with hook deps list.
    • useMemo hook for memorized values.
    • useCallback hook for memorized functions.
    • useRef hook for lifecycle persistent values.
  • Recap related-logic into separate well-structured hooks.
  • Reuse same stateful logic with custom hooks.

React TypeScript

Props Types

export declare interface AppProps {
children: React.ReactNode // best
style?: React.CSSProperties // for style
onChange?: (e: React.FormEvent<HTMLInputElement>) => void // form events!
props: Props & React.HTMLProps<HTMLButtonElement>
}

React Refs Types

class CssThemeProvider extends React.PureComponent<Props> {
private rootRef: React.RefObject<HTMLDivElement> = React.createRef()

render() {
return <div ref={this.rootRef}>{this.props.children}</div>
}
}

Function Component Types

Don't use React.FC/React.FunctionComponent:

  • React 17: Unnecessary addition of children (hide some run-time error).
  • React 18: @types/react v18 remove implicit children in React.FunctionComponent.
  • React.FC doesn't support generic components.
  • Barrier for <Comp> with <Comp.Sub> types (component as namespace pattern).
  • React.FC doesn't work correctly with defaultProps.
// Declaring type of props
interface Props {
message: string
}

// Inferred return type
const Message = ({ message }: Props) => <div>{message}</div>

// Explicit return type annotation
const Message = ({ message }: Props): JSX.Element => <div>{message}</div>

// Inline types annotation
const Message = ({ message }: { message: string }) => <div>{message}</div>

export default function App() {
return <Message message="message" />
}

Class Component Types

  • React.Component<P, S>
  • readonly state: State
  • static defaultProps
  • static getDerivedStateFromProps
class MyComponent extends React.Component<{
message?: string
}> {
render() {
const { message = 'default' } = this.props
return <div>{message}</div>
}
}
import React from 'react'
import Button from './Button'

type Props = typeof ButtonCounter.defaultProps & {
name: string
}

const initialState = { clicksCount: 0 }
type State = Readonly<typeof initialState>

class ButtonCounter extends React.Component<Props, State> {
readonly state: State = initialState

static defaultProps = {
name: 'count',
}

static getDerivedStateFromProps(
props: Props,
state: State,
): Partial<State> | null {
// ...
}

render() {
return <span>{this.props.foo}</span>
}
}

Generic Component Types

// 一个泛型组件
interface SelectProps<T> {
items: T[]
}

class Select<T> extends React.Component<SelectProps<T>, any> {}

// 使用
const Form = () => <Select<string> items={['a', 'b']} />

export default function App() {
return <Form />
}

In .tsx file, <T> maybe considered JSX.Element, use extends {} to avoid it:

const foo = <T extends object>(arg: T) => arg

Component Props Type

  • React.ComponentProps
  • React.ComponentPropsWithRef
  • React.ComponentPropsWithoutRef
import { Button } from 'library'

type ButtonProps = React.ComponentProps<typeof Button>
type AlertButtonProps = Omit<ButtonProps, 'onClick'>

const AlertButton: React.FC<AlertButtonProps> = props => (
<Button onClick={() => alert('hello')} {...props} />
)

export default function App() {
return <AlertButton />
}

Typing existing untyped React components:

declare module 'react-router-dom' {
import * as React from 'react'

interface NavigateProps<T> {
to: string | number
replace?: boolean
state?: T
}

export class Navigate<T = any> extends React.Component<NavigateProps<T>> {}
}

Component Return Type

  • JSX.Element: return value of React.createElement.
  • React.ReactNode: return value of a component.
function foo(bar: string) {
return { baz: 1 }
}

type FooReturn = ReturnType<typeof foo> // { baz: number }

React Event Types

  • React.SyntheticEvent.
  • React.AnimationEvent: CSS animations.
  • React.ChangeEvent: <input>/<select>/<textarea> change events.
  • React.ClipboardEvent: copy/paste/cut events.
  • React.CompositionEvent: user indirectly entering text events.
  • React.DragEvent: drag/drop interaction events.
  • React.FocusEvent: elements gets/loses focus events.
  • React.FormEvent<HTMLElement>: form focus/change/submit events.
  • React.InvalidEvent: validity restrictions of inputs fails.
  • React.KeyboardEvent: keyboard interaction events.
  • React.MouseEvent: pointing device interaction events (e.g mouse).
  • React.TouchEvent: touch device interaction events. Extends UIEvent.
  • React.PointerEvent: advanced pointing device interaction events (includes mouse, pen/stylus, touchscreen), recommended for modern browser. Extends UIEvent.
  • React.TransitionEvent: CSS transition. Extends UIEvent.
  • React.UIEvent: base event for Mouse/Touch/Pointer events.
  • React.WheelEvent: mouse wheel scrolling events.
  • Missing InputEvent (extends UIEvent): InputEvent is still an experimental interface and not fully supported by all browsers. Use SyntheticEvent instead.

React Event Handler Types

  • React.ChangeEventHandler<HTMLElement>.

React Form Event Types

interface State {
text: string
}

class App extends React.Component<Props, State> {
state = {
text: '',
}

// typing on RIGHT hand side of =
onChangeEvent = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ text: e.currentTarget.value })
}

// typing on LEFT hand side of =
onChangeHandler: React.ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({ text: e.currentTarget.value })
}

render() {
return (
<div>
<input type="text" value={this.state.text} onChange={this.onChange} />
</div>
)
}
}
export default function Form() {
return (
<form
ref={formRef}
onSubmit={(e: React.SyntheticEvent) => {
e.preventDefault()

const target = e.target as typeof e.target & {
email: { value: string }
password: { value: string }
}

const email = target.email.value // Type Checks
const password = target.password.value // Type Checks
}}
>
<div>
<label>
Email:
<input type="email" name="email" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Log in" />
</div>
</form>
)
}

React HTML and CSS Types

  • React.DOMAttributes<HTMLElement>
  • React.AriaAttributes<HTMLElement>
  • React.SVGAttributes<HTMLElement>
  • React.HTMLAttributes<HTMLElement>
  • React.ButtonHTMLAttributes<HTMLButtonElement>
  • React.HTMLProps<HTMLElement>
  • React.CSSProperties

React Input Types

type StringChangeHandler = (newValue: string) => void
type NumberChangeHandler = (newValue: number) => void
type BooleanChangeHandler = (newValue: boolean) => void

interface BaseInputDefinition {
id: string
label: string
}

interface TextInputDefinition extends BaseInputDefinition {
type: 'text'
value: string
onChange: StringChangeHandler
}

interface NumberInputDefinition extends BaseInputDefinition {
type: 'number'
value: number
onChange: NumberChangeHandler
}

interface CheckboxInputDefinition extends BaseInputDefinition {
type: 'checkbox'
value: boolean
onChange: BooleanChangeHandler
}

type Input =
| TextInputDefinition
| NumberInputDefinition
| CheckboxInputDefinition

React Portal Types

const modalRoot = document.getElementById('modal-root') as HTMLElement

export class Modal extends React.Component<{ children: ReactElement }> {
el: HTMLElement = document.createElement('div')

componentDidMount() {
modalRoot.appendChild(this.el)
}

componentWillUnmount() {
modalRoot.removeChild(this.el)
}

render() {
return ReactDOM.createPortal(this.props.children, this.el)
}
}
import type React from 'react'
import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'

const modalRoot = document.querySelector('#modal-root') as HTMLElement

const Modal: React.FC<object> = ({ children }) => {
const el = useRef(document.createElement('div'))

useEffect(() => {
const current = el.current
modalRoot!.appendChild(current)
return () => modalRoot!.removeChild(current)
}, [])

return createPortal(children, el.current)
}

export default Modal
import { Modal } from '@components'

export default function App() {
const [showModal, setShowModal] = React.useState(false)

return (
<div>
<div id="modal-root"></div>
{showModal && (
<Modal>
<div>
I&apos;m a modal!
{' '}
<button type="button" onClick={() => setShowModal(false)}>
close
</button>
</div>
</Modal>
)}
<button type="button" onClick={() => setShowModal(true)}>
show Modal
</button>
</div>
)
}

React Redux Types

const initialState = {
name: '',
points: 0,
likesGames: true,
}

type State = typeof initialState
export function updateName(name: string) {
return {
type: 'UPDATE_NAME',
name,
} as const
}

export function addPoints(points: number) {
return {
type: 'ADD_POINTS',
points,
} as const
}

export function setLikesGames(value: boolean) {
return {
type: 'SET_LIKES_GAMES',
value,
} as const
}

type Action = ReturnType<
typeof updateName | typeof addPoints | typeof setLikesGames
>

// =>
// type Action = {
// readonly type: 'UPDATE_NAME';
// readonly name: string;
// } | {
// readonly type: 'ADD_POINTS';
// readonly points: number;
// } | {
// readonly type: 'SET_LIKES_GAMES';
// readonly value: boolean;
// }
import type { Reducer } from 'redux'

function reducer(state: State, action: Action): Reducer<State, Action> {
switch (action.type) {
case 'UPDATE_NAME':
return { ...state, name: action.name }
case 'ADD_POINTS':
return { ...state, points: action.points }
case 'SET_LIKES_GAMES':
return { ...state, likesGames: action.value }
default:
return state
}
}

React Hook Types

  • useState<T>
  • Dispatch<T>
  • SetStateAction<T>
  • RefObject<T>
  • MutableRefObject<T>
  • More TypeScript Hooks.

UseState Hook Type

export default function App() {
const [user, setUser] = React.useState<IUser>({} as IUser)
const handleClick = () => setUser(newUser)

return <div>App</div>
}

UseReducer Hook Type

const initialState = { count: 0 }
type State = typeof initialState

type Action =
| { type: 'increment', payload: number }
| { type: 'decrement', payload: string }

function reducer(state: State, action: Action) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload }
case 'decrement':
return { count: state.count - Number(action.payload) }
default:
throw new Error('Error')
}
}

export default function Counter() {
const [state, dispatch] = React.useReducer(reducer, initialState)

return (
<>
Count:
{' '}
{state.count}
<button
type="button"
onClick={() => dispatch({ type: 'decrement', payload: '5' })}
>
-
</button>
<button
type="button"
onClick={() => dispatch({ type: 'increment', payload: 5 })}
>
+
</button>
</>
)
}

UseRef Hook Type

DOM Element Ref Type
  • If possible, prefer as specific as possible.
  • Return type is RefObject<T>.
export default function Foo() {
const divRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (!divRef.current)
throw new Error('divRef is not assigned')

doSomethingWith(divRef.current)
})

return <div ref={divRef}>etc</div>
}
Mutable Value Ref
  • Return type is MutableRefObject<T>.
export default function Foo() {
const intervalRef = useRef<number | null>(null)

// You manage the ref yourself (that's why it's called MutableRefObject!)
useEffect(() => {
intervalRef.current = setInterval()
return () => clearInterval(intervalRef.current)
}, [])

// The ref is not passed to any element's "ref" prop
return (
<button type="button" onClick={() => clearInterval(intervalRef.current)}>
Cancel timer
</button>
)
}

Custom Hooks Types

Use as const type assertion to avoid type inference (especially for [first, second] type).

export function useLoading() {
const [isLoading, setState] = React.useState(false)
const load = () => {
setState(true)
}

// return `[boolean, () => void]` as want
// instead of `(boolean | () => void)[]`
return [isLoading, load] as const
}
import type { Dispatch, SetStateAction } from 'react'
import { useState } from 'react'

interface ReturnType {
value: boolean
setValue: Dispatch<SetStateAction<boolean>>
setTrue: () => void
setFalse: () => void
toggle: () => void
}

function useBoolean(defaultValue?: boolean): ReturnType {
const [value, setValue] = useState(!!defaultValue)

const setTrue = () => setValue(true)
const setFalse = () => setValue(false)
const toggle = () => setValue(x => !x)

return { value, setValue, setTrue, setFalse, toggle }
}

export default useBoolean
import type { RefObject } from 'react'
import { useEffect, useRef } from 'react'

function useEventListener<T extends HTMLElement = HTMLDivElement>(
eventName: keyof WindowEventMap,
handler: (event: Event) => void,
element?: RefObject<T>,
) {
// Create a ref that stores handler
const savedHandler = useRef<(event: Event) => void>()

useEffect(() => {
// Define the listening target
const targetElement: T | Window = element?.current || window
if (!(targetElement && targetElement.addEventListener))
return

// Update saved handler if necessary
if (savedHandler.current !== handler)
savedHandler.current = handler

// Create event listener that calls handler function stored in ref
const eventListener = (event: Event) => {
savedHandler?.current(event)
}

targetElement.addEventListener(eventName, eventListener)

// Remove event listener on cleanup
return () => {
targetElement.removeEventListener(eventName, eventListener)
}
}, [eventName, element, handler])
}

export default useEventListener
import { useEffect, useReducer, useRef } from 'react'

import type { AxiosRequestConfig } from 'axios'
import axios from 'axios'

// State & hook output
interface State<T> {
status: 'init' | 'fetching' | 'error' | 'fetched'
data?: T
error?: string
}

type Cache<T> = Record<string, T>

// discriminated union type
type Action<T> =
| { type: 'request' }
| { type: 'success', payload: T }
| { type: 'failure', payload: string }

function useFetch<T = unknown>(
url?: string,
options?: AxiosRequestConfig,
): State<T> {
const cache = useRef<Cache<T>>({})
const cancelRequest = useRef<boolean>(false)

const initialState: State<T> = {
status: 'init',
error: undefined,
data: undefined,
}

// Keep state logic separated
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
switch (action.type) {
case 'request':
return { ...initialState, status: 'fetching' }
case 'success':
return { ...initialState, status: 'fetched', data: action.payload }
case 'failure':
return { ...initialState, status: 'error', error: action.payload }
default:
return state
}
}

const [state, dispatch] = useReducer(fetchReducer, initialState)

useEffect(() => {
if (!url)
return

const fetchData = async () => {
dispatch({ type: 'request' })

if (cache.current[url]) {
dispatch({ type: 'success', payload: cache.current[url] })
} else {
try {
const response = await axios(url, options)
cache.current[url] = response.data

if (cancelRequest.current)
return

dispatch({ type: 'success', payload: response.data })
} catch (error) {
if (cancelRequest.current)
return

dispatch({ type: 'failure', payload: error.message })
}
}
}

fetchData()

return () => {
cancelRequest.current = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url])

return state
}

export default useFetch

React Internationalization

  • XLIFF: XML Localization Interchange File Format.
  • ICU: International Components for Unicode.
  • BCP 47: IETF BCP 47 language tag.

Simple i18n Implementation

// locale/zh-CN.js

export default {
hello: '你好,{name}',
}
// locale/en-GB.js

export default {
hello: 'Hello,{name}',
}
import IntlMessageFormat from 'intl-messageformat'
import zh from '../locale/zh'
import en from '../locale/en'
const MESSAGES = { en, zh }
const LOCALE = 'en' // 这里写上决定语言的方法,例如可以从 cookie 判断语言

class Intl {
get(key, defaultMessage, options) {
let msg = MESSAGES[LOCALE][key]

if (msg == null) {
if (defaultMessage != null)
return defaultMessage

return key
}

if (options) {
msg = new IntlMessageFormat(msg, LOCALE)
return msg.format(options)
}

return msg
}
}

export default Intl

React i18n Library

i18n Solution

Modern React

ES6 Binding for This

class Component extends React.Component {
state = {}
handleES6 = (event) => {}

constructor(props) {
super(props)
this.handleLegacy = this.handleLegacy.bind(this)
}

handleLegacy(event) {
this.setState(prev => ({ ...prev }))
}

render() {
return <div>{this.state.foo}</div>
}
}

Context API

Context API provide a Dependency Injection style method, to provide values to children components.

Context 中只定义被大多数组件所共用的属性 (avoid Prop Drilling):

  • Global state.
  • UI Theme.
  • Preferred locale language.
  • Application configuration.
  • User setting.
  • Authenticated user.
  • Service collection.

频繁的 Context value 更改会导致依赖 value 的组件 穿透 shouldComponentUpdate/React.memo 进行 forceUpdate, 增加 render 次数, 从而导致性能问题.

import React, { createContext, useContext, useMemo, useState } from 'react'
import { fakeAuth } from './app/services/auth'

const authContext = createContext()

function useAuth() {
return useContext(authContext)
}

export default function AuthProvider({ children }: { children: ReactElement }) {
const [user, setUser] = useState(null)

const signIn = useCallback((cb) => {
return fakeAuth.signIn(() => {
setUser('user')
cb()
})
}, [])

const signOut = useCallback((cb) => {
return fakeAuth.signOut(() => {
setUser(null)
cb()
})
}, [])

const auth = useMemo(() => {
return {
user,
signIn,
signOut,
}
}, [user, signIn, signOut])

return <authContext.Provider value={auth}>{children}</authContext.Provider>
}

Context Refs

// Context.js
import React, { Component, createContext } from 'react'

// React team — thanks for Context API 👍
const context = createContext()
const { Provider: ContextProvider, Consumer } = context

class Provider extends Component<{ children: ReactElement }> {
// refs
// usage: this.textareaRef.current
textareaRef = React.createRef()

// input handler
onInput = (e) => {
const { name, value } = e.target

this.setState({
[name]: value,
})
}

render() {
return (
<ContextProvider
value={{
textareaRef: this.textareaRef,
onInput: this.onInput,
}}
>
{this.props.children}
</ContextProvider>
)
}
}
// TextArea.jsx
import React from 'react'
import { Consumer } from './Context'

export default function TextArea() {
return (
<Consumer>
{context => (
<textarea
ref={context.textareaRef}
className="app__textarea"
name="snippet"
placeholder="Your snippet…"
onChange={context.onInput}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
wrap="off"
/>
)}
</Consumer>
)
}

Context Internals

createContext 创建了一个 { _currentValue, Provider, Consumer } 对象:

  • _currentValue 保存值.
  • Provider 为一种 JSX 类型, 会转为对应的 fiber 类型, 负责修改 _currentValue.
  • ConsumeruseContext 负责读取 _currentValue.
  • Provider 处理每个节点之前会入栈当前 Context, 处理完会出栈, 保证 Context 只影响子组件, 实现嵌套 Context.

Error Boundary

以下是错误边界不起作用的情况:

  • 事件处理器内代码.
  • setTimeoutrequestAnimationFrame 回调中的异步代码.
  • 服务端渲染代码.
  • 错误边界代码本身.

React Error Boundary library:

class ErrorBoundary extends React.Component<{ children: ReactElement }> {
state = {
hasError: false,
error: null,
info: null,
}

// key point
componentDidCatch(error, info) {
this.setState({
hasError: true,
error,
info,
})
}

render() {
if (this.state.hasError) {
return (
<div>
<h1>Oops, something went wrong :(</h1>
<p>
The error:
{this.state.error.toString()}
</p>
<p>
Where it occurred:
{this.state.info.componentStack}
</p>
</div>
)
}

return this.props.children
}
}

React Fragment

  • Less node, less memory, faster performance.
  • Avoid extra parent-child relationship for CSS flex and grid layout.
  • DOM debug inspector is less cluttered.
class Items extends React.Component {
render() {
return (
<>
<Fruit />
<Beverages />
<Drinks />
</>
)
}
}

class Fruit extends React.Component {
render() {
return (
<>
<li>Apple</li>
<li>Orange</li>
<li>Blueberry</li>
<li>Cherry</li>
</>
)
}
}

class Frameworks extends React.Component {
render() {
return (
<>
<p>JavaScript:</p>
<li>React</li>
,
<li>Vuejs</li>
,
<li>Angular</li>
</>
)
}
}

React Portal

Portal provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component ReactDOM.createPortal(child, container).

<div id="root"></div>
<div id="portal"></div>
const portalRoot = document.getElementById('portal')

class Portal extends React.Component<{ children: ReactElement }> {
constructor() {
super()
this.el = document.createElement('div')
}

componentDidMount = () => {
portalRoot.appendChild(this.el)
}

componentWillUnmount = () => {
portalRoot.removeChild(this.el)
}

render() {
const { children } = this.props
return ReactDOM.createPortal(children, this.el)
}
}

interface Props {
on: boolean
toggle: Function
children: ReactElement
}

class Modal extends React.Component<Props> {
render() {
const { children, toggle, on } = this.props

return (
<Portal>
{on
? (
<div className="modal is-active">
<div className="modal-background" />
<div className="modal-content">
<div className="box">
<h2 className="subtitle">{children}</h2>
<button
type="button"
onClick={toggle}
className="closeButton button is-info"
>
Close
</button>
</div>
</div>
</div>
)
: null}
</Portal>
)
}
}

class App extends React.Component {
state = {
showModal: false,
}

toggleModal = () => {
this.setState({
showModal: !this.state.showModal,
})
}

render() {
const { showModal } = this.state
return (
<div className="box">
<h1 className="subtitle">Hello, I am the parent!</h1>
<button
type="button"
onClick={this.toggleModal}
className="button is-black"
>
Toggle Modal
</button>
<Modal on={showModal} toggle={this.toggleModal}>
{showModal ? <h1>Hello, I am the portal!</h1> : null}
</Modal>
</div>
)
}
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

Concurrent Features

import * as ReactDOM from 'react-dom'
import App from 'App'

// Create a root by using ReactDOM.createRoot():
const root = ReactDOM.createRoot(document.getElementById('app'))

// Render the main <App/> element to the root:
root.render(<App />)

Batching Updates

  • All updates will be automatically batched, including updates inside of promises, async code and native event handlers.
  • ReactDOM.flushSync can opt-out of automatic batching.
function handleClick() {
// React 17: Re-rendering happens after both of the states are updated.
// This is called batching.
// This is also the default behavior of React 18.
setIsBirthday(b => !b)
setAge(a => a + 1)
}

// For the following code blocks,
// React 18 does automatic batching, but React 17 doesn't.
// 1. Promises:
function handleClick() {
fetchSomething().then(() => {
setIsBirthday(b => !b)
setAge(a => a + 1)
})
}

// 2. Async code:
setInterval(() => {
setIsBirthday(b => !b)
setAge(a => a + 1)
}, 5000)

// 3. Native event handlers:
element.addEventListener('click', () => {
setIsBirthday(b => !b)
setAge(a => a + 1)
})

Reconciler 注册调度任务时, 会通过节流与防抖提升调度性能:

  • 在 Task 注册完成后, 会设置 FiberRoot 的属性, 代表现在已经处于调度进行中.
  • 再次进入 ensureRootIsScheduled 时 (比如连续 2 次 setState, 第二次 setState 同样会触发 Reconciler 与 Scheduler 执行), 如果发现处于调度中, 则会通过节流与防抖, 保证调度性能.
  • 节流: existingCallbackPriority === newCallbackPriority, 新旧更新的优先级相同, 则无需注册新 Task, 继续沿用上一个优先级相同的 Task, 直接退出调用.
  • 防抖: existingCallbackPriority !== newCallbackPriority, 新旧更新的优先级不同, 则取消旧 Task, 重新注册新 Task.

EnsureRootIsScheduled:

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
)

if (nextLanes === NoLanes) {
if (existingCallbackNode !== null)
cancelCallback(existingCallbackNode)

root.callbackNode = null
root.callbackPriority = NoLane
return
}

const newCallbackPriority = getHighestPriorityLane(nextLanes)
const existingCallbackPriority = root.callbackPriority

// Debounce.
if (existingCallbackPriority === newCallbackPriority) {
// The priority hasn't changed. We can reuse the existing task. Exit.
return
}

// Throttle.
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode)
}

// Schedule a new callback.
let newCallbackNode

if (newCallbackPriority === SyncLane) {
if (root.tag === LegacyRoot)
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root))
else scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))

if (supportsMicroTasks) {
scheduleMicroTask(() => {
if (executionContext === NoContext)
flushSyncCallbacks()
})
} else {
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks)
}

newCallbackNode = null
} else {
const eventPriority = lanesToEventPriority(nextLanes)
const schedulerPriorityLevel
= eventPriorityToSchedulePriority(eventPriority)
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
)
}

root.callbackPriority = newCallbackPriority
root.callbackNode = newCallbackNode
}

Suspense

Extract loading/skeleton/placeholder components into single place:

export default function App() {
return (
<Suspense fallback={<Skeleton />}>
<Header />
<Suspense fallback={<ListPlaceholder />}>
<ListLayout>
<List pageId={pageId} />
</ListLayout>
</Suspense>
</Suspense>
)
}
React Bottlenecks
  1. CPU bottleneck: Concurrency Feature (Priority Interrupt Mechanism).
  2. I/O bottleneck: Suspense.

Error Boundary Suspense

function ErrorFallback() {
return (
<div
className="text-red-500 w-screen h-screen flex flex-col justify-center items-center"
role="alert"
>
<h2 className="text-lg font-bold">Oops, something went wrong :( </h2>
<Button
className="mt-4"
onClick={() => window.location.assign(window.location.origin)}
>
Refresh
</Button>
</div>
)
}

interface AppProviderProps {
children: React.ReactNode
}

export function AppProvider({ children }: AppProviderProps) {
return (
<React.Suspense
fallback={(
<div className="h-screen w-screen flex items-center justify-center">
<Spinner size="xl" />
</div>
)}
>
<ErrorBoundary FallbackComponent={ErrorFallback}>
{children}
</ErrorBoundary>
</React.Suspense>
)
}

Lazy Suspense

Lazy loading and code splitting:

import React, { Suspense, lazy } from 'react'

const Product = lazy(() => import('./ProductHandler'))

export default function App() {
return (
<div className="product-list">
<h1>My Awesome Product</h1>
<Suspense fallback={<h2>Product list is loading...</h2>}>
<p>Take a look at my product:</p>
<section>
<Product id="PDT-49-232" />
<Product id="PDT-50-233" />
<Product id="PDT-51-234" />
</section>
</Suspense>
</div>
)
}
const { lazy, Suspense } = React

const Lazy = lazy(
() =>
new Promise((resolve) => {
setTimeout(() => {
resolve({ default: () => <Resource /> })
}, 4000)
}),
)

function Resource() {
return (
<div className="box">
<h1>React Lazy</h1>
<p>
This component loaded after 4 seconds, using React Lazy and Suspense
</p>
</div>
)
}

export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Lazy />
</Suspense>
)
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

SSR Suspense

React v18+: enable Suspense on the server:

  • Selective Hydration: one slow part doesn't slow down whole page.
  • Streaming HTML: show initial HTML early and stream the rest HTML.
  • Enable code splitting for SSR.
export default function LandingPage() {
return (
<div>
<FastComponent />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</div>
)
}

React Server Components

Give React a chunk of code that it runs exclusively on the server, to do the database query to eliminate redundant network requests, polish FCP and LCP, and improve SEO:

React Server Components

Each meta-framework came up with its own approach to achieve such target. Next.js has one approach, Gatsby has another, Remix has yet another. It hasn't been standardized.

// pages/index.js
import ParentComponent from '../components/parent-component'
import type { PageProps } from '@/types'

export default function Page({ data }: PageProps) {
return <ParentComponent data={data} />
}

export async function getServerSideProps() {
const response = await fetch('https://api.github.com/repos/vercel/next.js')
const data = await response.json()

return { props: { data } }
}

For years, the React team has been quietly tinkering on this problem, trying to come up with an official way to solve this problem. Their solution is called React Server Components.

Server Components never re-render. They run once on the server to generate the UI. The rendered value is sent to the client and locked in place. As far as React is concerned, this output is immutable, and will never change:

import db from 'imaginary-db'

export default async function Homepage() {
const link = db.connect('localhost', 'root', 'pass0w0rd')
const data = await db.query(link, 'SELECT * FROM products')

return (
<div>
<h1>Trending Products</h1>
{data.map(item => (
<article key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
</article>
))}
</div>
)
}

Code for Server Components isn't included in the JS bundle. We send the virtual representation (along the rendered value) that was generated by the server. When React loads on the client, it re-uses that description instead of re-generating it: