1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// This JSX syntax: return <MyComponent a={42} b="testing">Text here</MyComponent> // is converted to this call: return React.createElement(MyComponent, {a: 42, b: "testing"}, "Text Here") // and that becomes this element object: {type: MyComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]} // And internally, React calls the actual function to render it: let elements = MyComponent({...props, children}) // For "host components" like HTML: return <button onClick={() => {}}>Click Me</button> // becomes React.createElement("button", {onClick}, "Click Me") // and finally: {type: "button", props: {onClick}, children: ["Click me"]}
React.createElement()
calls (or the newer equivalent) at compile time{type, props, children}
useEffect
hooks run on a short delay after the commit phase is done, to let the browser have a chance to paint in between.setState()
from useState
, dispatch()
from useReducer
this.setState()
, this.forceUpdate()
render(<App />, rootNode)
useSyncExternalStore
hookuseSyncExternalStore
, libraries like React-Redux still called setState()
just to force a re-render.1
const [, forceRender] = useReducer(c => c + 1, 0)
useReducer
as a replacement for forceUpdate
if neededA > B > C > D
:setState()
in B
, queues a renderA
is not marked as needing an update, and moves past itB
is marked as needing an update, and renders it. B
returns <C />
as it did last time.C
was not originally marked as needing an update. However, because its parent B
rendered, React moves down and renders C
as well. C returns <D />
again.D
was also not marked for rendering, but since its parent C
rendered, React moves downward and renders D
too.console.log()
won't hurt.Math.random()
or Date.now()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
export type Fiber = { // Tag identifying the type of fiber. tag: WorkTag, // Unique identifier of this child. key: null | string, // The resolved function/class/ associated with this fiber. type: any, // Singly Linked List Tree Structure. child: Fiber | null, sibling: Fiber | null, index: number, // Input is the data coming into this fiber (arguments/props) pendingProps: any, memoizedProps: any, // The props used to create the output. // A queue of state updates and callbacks. updateQueue: Array<State | StateUpdaters>, // The state used to create the output memoizedState: any, // Dependencies (contexts, events) for this fiber, if any dependencies: Dependencies | null, };
prevElement.type !== currElement.type
, it assumes the entire subtree will be different1 2 3 4 5 6 7 8 9
// ❌ BAD! // This creates a new `ChildComponent` reference every time! function ParentComponent() { function ChildComponent() { return <div>Hi</div> } return <ChildComponent /> }
1 2 3 4 5 6 7 8 9
// ✅ GOOD // This only creates one component type reference function ChildComponent() { return <div>Hi</div> } function ParentComponent() { return <ChildComponent /> }
<SomeComponent key="someValue">
:key
out, so you can never have props.key
todos.map(todo => <TodoListItem key={todo.id} todo={todo} />)
setState()
queues a render pass. However, React "batches" multiple queued updates in the same event loop tick into one combined render pass, executed at the end of the tick in a "microtask". onClick
, etc)1 2 3 4 5 6 7 8 9 10 11
const [counter, setCounter] = useState(0); const onClick = async () => { setCounter(0); setCounter(1); const data = await fetchSomeData(); setCounter(2); setCounter(3); }
setCounter(0/1)
batched togetherasync
causes new event loop ticksetCounter(2)
and setCounter(3)
each render sync, separatelysetCounter(0/1)
batched togetherasync
causes new event loop ticksetCounter(2/3)
batched in this tick1 2 3 4 5 6 7
const [counter, setCounter] = useState(0); const handleClick = () => { setCounter(counter + 1); console.log(counter); // Why is this not updated yet?????? }
counter
value at the time the component last rendered!useLayoutEffect/cDM/cDU
run sync:unstable_batchedUpdates()
to batch them together outside of event handlersflushSync()
to force immediate renders and opt out of automatic batching<StrictMode>
in dev, to catch bugsconsole.log()
in render body to count renders! Log in a useEffect
, or use the React DevTools ProfilersetState()
while rendering, conditionally!UI = f(props + state)
, so if props and state haven't changed, the UI output should be the same.React.memo()
higher-order componentshouldComponentUpdate
or React.PureComponent
useMemo
to save an element reference, or use props.children
:1 2 3 4 5 6 7 8
function ComponentA({data}) { // Consistent `ChildComponent` element reference const memoizedChild = useMemo(() => { return <ChildComponent data={data} />; }, [data]) return <div>{memoizedChild}</div> }
1 2 3 4 5 6 7 8 9 10 11 12 13 14
function Parent({children}) { const [counter, setCounter] = useState(0) // Consistent `props.children` element reference as state changes return ( <div> <button onClick={increment}>Increment</button> {children} </div> ) } // later return <Parent><SomeChild /></Parent>
React.memo()
: controlled by the child componentReact.memo()
). However, note that passing elements as children easily breaks memoization, since new element references are created:<MemoizedChild><OtherComponent /></MemoizedChild>
useCallback
, other data in useMemo
.<button>
in React.memo()
! It won't have any benefit.React.memo()
?"Optimizing withmemo
is only valuable when your component re-renders often with the same exact props, and its re-rendering logic is expensive. If there is no perceptible lag when your component re-renders,memo
is unnecessary. Keep in mind thatmemo
is completely useless if the props passed to your component are always different, such as if you pass an object or a plain function defined during rendering. This is why you will often needuseMemo
anduseCallback
together withmemo
.
React.memo()
would be a net win, but very dependent on app structure and update patterns. React.memo()
does "shallow equality" checks like props.someValue !== prevProps.someValue
, assuming immutable updates will change references. Hooks with dependencies (useMemo/useCallback/useEffect
) work similarly. useState
and useReducer
expect new state references in order to re-render. If you pass in the same state reference, React will bail out of the update!MyContext.Provider>
can read the value from that context instance, without having to explicitly pass that value as a prop through every intervening component.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
function GrandchildComponent() { const value = useContext(MyContext); return <div>{value.a}</div> } function ChildComponent() { return <GrandchildComponent /> } function ParentComponent() { const [a, setA] = useState(0); const [b, setB] = useState("text"); const contextValue = {a, b}; return ( <MyContext.Provider value={contextValue}> <ChildComponent /> </MyContext.Provider> ) }
<MyContext.Provider>
value
will cause all consuming components to re-rendervalue.a
, but since it gets the entire value, it also renders if value.b
is updated because value
itself is a new reference.setState()
queues a render of that componentParent/Grandchild
example: the Grandchild
will re-render by default, but not from the context update! It's just because React renders recursively.React.memo()
<MyContext.Provider>{props.children}</MyContext.Provider>
useSelector
hook and connect
wrapper component is a separate Redux store subscriber callbackcurrentValue !== prevValue
to see if the component should re-renderconnect
and useSelector
Differencesconnect
:mapState+mapDispatch+wrapperProps
to create childProps
React.memo()
with more checks (only re-render if any field in combined childProps
has changed, not just wrapperProps
)mapState
, so subscribes to nearest connected ancestoruseSelector
:useSelector
, it's likely more components will render than with connect
. Use React.memo()
yourself as needed to optimize rendering.useContext
accept a selector function like value => value.a
, and only re-render component if that value changed