React Lifecycle
Steps
TLDR with CodeSandbox example
- React triggers a Component rendering i.e. calling your Component function when...
- "Reconciling" (diffing & calculating) the old and new virtual DOM
- "Committing" the changes to the DOM
- (Minor)
useLayoutEffect
to avoid flashes or flickers - Browser repaints the screen
useEffect
to synchronize your React components with external systems
Step 1: Render phase
React triggers a Component rendering when:
- State (including state consumed by custom hook), Props, Context consumed by your Component changes.
- Parent của Component re-render (trừ khi Component dc wrap bởi
React.memo
). If passed asprop
instead of directly passed to Component, React will NOT re-render Component when Parent re-render.
“Rendering” means React calling your Component function. It uses a concept called Virtual DOM to compute the diffs between what's currently on the page vs what should be on the page, then calculate the minimal necessary DOM operations to make the DOM match the latest rendering output, and wait for the next step, the commit phase. The diffing and calculation process is known as reconciliation.
You must not declare nested components in your Component function because they can be redefined when Parent re-render
// ✅ Tách Child ra ngoài
const Child = ({ onClick }) => {
return <button onClick={onClick}>+</button>
}
const Parent = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
const newCount = count + 1
setCount(newCount)
// This will reflect the correct value of the state after being changed instead of being one step behind
console.log(newCount)
}
// ❌ Nested Component
const NestChild = ({ onClick }) => {
return <button onClick={onClick}>+</button>
}
return (
<>
<Child onClick={handleClick} />
<NestChild onClick={handleClick} />
</>
)
}
Step 2: Commit phase
After rendering (calling) your components, React will applies changes the DOM.
- For the initial render, React will use the
appendChild()
DOM API to put all the DOM nodes it has created on screen. - For re-renders, React will apply the minimal necessary DOM operations that have just been calculated at the rendering phase, which means React only changes the DOM if there’s a difference between renders.
After commit phase, it will run your Effects because this is a good time to synchronize your React components with external systems.
If your Effect also immediately updates the state, this restarts the whole process from scratch, and in some case can cause an infinite loop.
Epilogue: Browser paint
After rendering is done and React updated the DOM, the browser will repaint the screen. Although this process is known as “browser rendering”, we’ll refer to it as “painting” to avoid confusion.
Mimic lifecycle methods
componentDidMount
useEffect(() => {
// Do stuff
}, [])
CAUTION: This is different with logics that only run one time when the application starts
let didInit = false
function App() {
useEffect(() => {
if (!didInit) {
didInit = true
// ✅ Only runs once per app load
checkAuthToken()
}
}, [])
}
OR
if (typeof window !== 'undefined') {
// Check if we're running in the browser.
// ✅ Only runs once per app load
checkAuthToken()
}
function App() {
// ...
}
componentDidUpdate
- With useRef
- With useState
const mounted = useRef()
useEffect(() => {
if (!mounted.current) {
mounted.current = true
// Do `componentDidMount` logic
} else {
// Do `componentDidUpdate` logic
}
}, [yourDepedencies])
const [mounted, setMounted] = useState(false)
useEffect(() => {
if (!mounted) {
setMounted(true)
// Do `componentDidMount` logic
} else {
// Do `componentDidUpdate` logic
}
}, [yourDepedencies])
See example in CodeSandbox
componentWillUnmount
useEffect(() => {
return () => {
// Do `componentWillUnmount` logic
}
}, [])