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 = {}, S = {}, 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 = {}, S = {}, 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:

const CreateTextWithProps = ({ text, ASCIIChar, ...props }) => {
return (
<span {...props}>
{text}
{ASCIIChar}
</span>
);
};

const RepeatCharacters = ({ times, children }) => {
return React.cloneElement(children, {
ASCIIChar: children.props.ASCIIChar.repeat(times),
});
};

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

return <div>{<RenderChildren />}</div>;
};

const RadioButton = props => {
return (
<label>
<input type="radio" value={props.value} name={props.name} />
{props.children}
</label>
);
};

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 }) {
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 }) {
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 {
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
const ButtonElement = React.forwardRef((props, ref) => (
<button ref={ref} className="CustomButton">
{props.children}
</button>
));

// Create ref to the DOM button:
// get ref to `<button>`
const ref = React.createRef();
<ButtonElement ref={ref}>{'Forward Ref'}</ButtonElement>;
type Ref = HTMLButtonElement;
interface Props {
children: React.ReactNode;
type: 'submit' | 'button';
}

const FancyButton = React.forwardRef<Ref, Props>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type}>
{props.children}
</button>
));

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;
}

const 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>
);
};

const RadioInput = ({
currentValue,
onChange,
label,
value,
name,
imgSrc,
key,
}: RadioInputProps): React.ReactElement => (
<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;
}

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

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

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

const getChildrenOnDisplayName = (children: ReactNode[], displayName: string) =>
React.Children.map(children, child =>
child.displayName === displayName ? child : null
);

const 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;

const App = () => (
<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 extends React.Component {
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 {
render() {
return (
<div onClick={this.props.onClick}>
<h1>{this.props.title}</h1>
</div>
);
}
}

export default withToggleable(NormalMenu);
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).
class Toggleable extends React.Component {
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);
}
}

const ToggleableMenu = props => (
<Toggleable>
{(show, onClick) => (
<div>
<div onClick={onClick}>
<h1>{props.title}</h1>
</div>
{show && props.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 AppProps {
message: string;
}

// Inferred return type
const App = ({ message }: AppProps) => <div>{message}</div>;

// Explicit return type annotation
const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>;

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

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']} />;

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

const foo = <T extends {}>(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} />
);

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>
);
}
}
const Form = () => (
<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 {
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<{}> = ({ 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';

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

return (
<div>
<div id="modal-root"></div>
{showModal && (
<Modal>
<div>
I'm a modal!{' '}
<button onClick={() => setShowModal(false)}>close</button>
</div>
</Modal>
)}
<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';

const 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

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');
}
}

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

return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>
-
</button>
<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>.
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>.
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 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;
}

More hooks

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
// eslint-disable-next-line import/no-anonymous-default-export
export default {
hello: '你好,{name}',
};
// locale/en-GB.js
// eslint-disable-next-line import/no-anonymous-default-export
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>Component</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 AuthProvider({ children }) {
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>;
}

function useAuth() {
return useContext(authContext);
}

export { AuthProvider, useAuth };

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 {
// 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';

const TextArea = () => (
<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 {
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 (
<React.Fragment>
<Fruit />
<Beverages />
<Drinks />
</React.Fragment>
);
}
}

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 {
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);
}
}

class Modal extends React.Component {
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 class="subtitle">{children}</h2>
<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 class="subtitle">Hello, I am the parent!</h1>
<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.render(<App />, document.getElementById('root'));

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:

const App = () => (
<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

const 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 const 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'));

const App = () => (
<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);
})
);

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

const App = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<Lazy />
</Suspense>
);
};

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

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.
const LandingPage = () => (
<div>
<FastComponent />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</div>
);

React Performance

React Performance Mental Model

3L - Less render times, less render calculations, less render nodes:

  • 数据: 利用缓存 (复用数据与 VNode), 减少 re-render 次数.
  • 计算: 精确判断更新时机和范围, 减少计算量, 优化 render 过程.
  • 渲染: 精细粒度, 降低组件复杂度, 减少 DOM 数量.

React Performance Best Practice

  • Use key correctly.
  • React.useMemo and React.useCallback (no anonymous functions).
  • shouldComponentUpdate/React.memo/React.PureComponent: shallow compare on components to prevent unnecessary re-rendering caused by parent components.
  • Lazy loading components (React.lazy and React.Suspense).
  • Virtualized Lists.
  • Stateless component: less props, less state, less nest (HOC or render props).
  • Immutable.js.
  • Isomorphic rendering.
  • Webpack bundle analyzer.
  • Progressive React.
  • useDeferredValue/useTransition hook for debounce concurrent features.

Re-rendering Problem

React will recursively render all child components inside of it (because props.children is always a new reference when parent re-rendering).

The major difference is that React.Component doesn’t implement shouldComponentUpdate() lifecycle method while React.PureComponent implements it.

If component render() function renders the same result given the same props and state, use React.PureComponent/React.memo for a performance boost in some cases.

import React, { PureComponent } from 'react';

const Unstable = props => {
console.log(' Rendered Unstable component ');

return (
<div>
<p> {props.value}</p>
</div>
);
};

class App extends PureComponent {
state = {
value: 1,
};

componentDidMount() {
setInterval(() => {
this.setState(() => {
return { value: 1 };
});
}, 2000);
}

render() {
return (
<div>
<Unstable value={this.state.value} />
</div>
);
}
}

export default App;
import React, { Component } from 'react';

const Unstable = React.memo(props => {
console.log(' Rendered this component ');

return (
<div>
<p> {props.value}</p>
</div>
);
});

class App extends Component {
state = {
value: 1,
};

componentDidMount() {
setInterval(() => {
this.setState(() => {
return { value: 1 };
});
}, 2000);
}

render() {
return (
<div>
<Unstable value={this.state.value} />
</div>
);
}
}

export default App;

Prevent useless re-rendering:

  • shouldComponentUpdate.
  • React.PureComponent: shallow compare diff.
  • React.memo: shallow compare diff, to memorize stateless components that props not changed often, export default React.memo(MyComponent, areEqual).
  • Memorized values.
  • Memorized event handlers.
  • 在用 memo 或者 useMemo 做优化前 (Before You Memo), 可以从不变的部分里分割出变化的部分. 通过将变化部分的 state 向下移动从而抽象出变化的子组件, 或者将变化内容提升 (Lift Up) 到父组件从而将不变部分独立出来:
    • Composition pattern, composite immutable/expensive component:
      • Sibling component.
      • Props component: props.children/props.renderProps.
    • Make reference values become immutable:
      • Styles (object).
      • Event callbacks (function).
      • Babel plugin to hoist reference values.
// BAD
// When <App> re-rendering, <ExpensiveTree> will re-rendering:
// <ExpensiveTree /> is actually <ExpensiveTree props={}>.
// Every time <App> re-rendering will pass a new `{}` reference to <ExpensiveTree>.
import { useState } from 'react';

export default function App() {
const [color, setColor] = useState('red');

return (
<div>
<input value={color} onChange={e => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
<ExpensiveTree />
</div>
);
}

function ExpensiveTree() {
const now = performance.now();

while (performance.now() - now < 100) {
// Artificial delay -- do nothing for 100ms
}

return <p>I am a very slow component tree.</p>;
}

Composite sibling component:

// GOOD
// <ExpensiveTree> will not re-rendering.