Hooks
useEffect
A component must be pure
The component's rendering code is where you take the props and state, transform them, and return the JSX you want to see on the screen. It must be pure, which means:
- It should not change any variables that existed before rendering.
- It should always return the same result when being given the same inputs, including props, state and context.
Side effects
Anything that shouldn't happen during rendering, which means it can modify its own appearance or pre-existing variables is called Side effect. It can do more things rather than just calculate the JSX, e.g. (for example) it can modify a state value other than the initial value, toggle dark mode on the website, query more products from the server,...
In React, side effects usually belong inside event handlers. They don’t need to be pure because even though they are defined inside your component, they don’t run in the rendering phase.
Effects
Effects refers to the React-specific definition, i.e. side effects caused by rendering itself, rather than side effects caused by a particular event, and therefore can't be handled by event handlers. For example, sending a message in the chat is an Event because it is directly caused by the user clicking a button. However, setting up a server connection is an Effect because it needs to happen regardless what makes the component to appear.
In React, effects are handled by useEffect
hook. It let you run some code after rendering to synchronize React components with external systems outside of React. For example, you might want to set up a server connection, send an analytics log when a component appears on the screen, or control an external element based on a React state.
Snippets
- Fetch & Use external value
- Handle setTimeout in Effect firing twice in dev
- Handle fetchs in Effect firing twice in dev
function App({ id }) {
const [user, setUser] = useState()
useEffect(() => {
// Declare the function in Effect so it won't be called on every render
const fetchUser = async () => {
const result = await fetch(`https://users.com/[${id}]`)
setUser(result)
}
fetchUser()
}, [id])
useEffect(() => {
window.someAnalytics.push(user)
}, [user])
return <div>{user}</div>
}
function useInterval() {
// Set initial value of `time` state here will make the component impure...
const [time, setTime] = useState(0)
useEffect(() => {
// ...so we need to set initial value of `time` state in useEffect instead
setTime(new Date().getTime())
let timeoutId
timeoutId = setTimeout(() => {
setTime((prev) => prev + 1000)
}, 1000)
// If we don't clean up the timeout, there will be 2 timeouts because Effects run twice in dev
return () => clearTimeout(timeoutId)
}, [])
return time
}
useEffect(() => {
let shouldIgnore = false
async function getData() {
const data = await fetchTodos(userId)
// 1st Run.3: Here `ignore` is already set to `true` from the cleanup function, so `setTodos` won't be called,...
// 1st Run.3b: which is good as the `setTodos` will run on the 2nd time Effect run in dev
// 2nd Run.2: But this time because the cleanup function is not called, here `ignore` is still `false`, so `setTodos` will be called
if (!shouldIgnore) setTodos(data)
}
// 1st Run.1: `getData` is called
// 2nd Run.1: On the 2nd time Effect run in dev, `getData` is called again...
getData()
// 1st Run.2: Cleanup function doesn't wait for async fn (getData) to finish...
return () => (shouldIgnore = true)
}, [userId])
(Recommended) Thinking from the Effect’s perspective
The Effect start connecting
(until it disconnect
when the id
change), and then connects
to another id
...
Finally when the App
unmount, the Effect disconnect
.
Thinking from the component’s perspective
- Mounting: Render → Commit →
useEffectCode
- Updating: Render → Commit →
useEffectCode
→ Rerender when deps change →cleanUp
with old values →useEffectCode
with new values... - Unmounting:
cleanUp
(AFTER the component is removed from the DOM).
Anything you use in your effect callback that has a stable identity aren't necessary to be included the dependency array, e.g. setState
, refs from useRef
because you’ll always get the same object for those on every render.
Mount/ Unmount means Adding/ Removing nodes to the DOM.
- Ko cần cleanup (Chạy rồi thì k cần quan tâm nữa): Gọi API, Tương tác DOM.
- Cần cleanup:
setTimeout
/setInterval
, connection to a server/database
Optimize
useCallback
Memoize function: When the component re-render, the reference to the function in useCallback
will be retained → Components that take the function as props
won't re-render pointlessly.
If you’re writing a custom Hook, it’s recommended to wrap any functions that it returns into useCallback
if you plan to pass them as Component prop
or useEffect
dependency.
useMemo
Memoize return value of a function: When the component re-render, the reference to the return value in useMemo
callback will be retained → Components that take the return value as props
won't re-render pointlessly. Or when the function that creates the value is too complex (ex: Sort, fetch,...).
It's also generally a good idea to memoize values of a Context because there might be dozens of pure components that consume this context. Without useMemo
, all of these components would be forced to re-render if the Context Provider
happens to re-render.
Read more:
useMemo
benchmark result
useCallback(someFunction, deps)
are the same with useMemo(() => someFunction, deps)
useMemo
can also be used to memoized a component:
export const Page = ({ countries }: { countries: Country[] }) => {
const [selectedCountry, setSelectedCountry] = useState<Country>(countries[0])
const [savedCountry, setSavedCountry] = useState<Country>(countries[0])
const list = useMemo(() => {
return (
<CountriesList
countries={countries}
onCountryChanged={(c) => setSelectedCountry(c)}
// This helps CountriesList to NOT rerender when `selectedCountry` changes, because it's not used in CountriesList
savedCountry={savedCountry}
/>
)
}, [savedCountry, countries])
const selected = useMemo(() => {
return (
<SelectedCountry
// This helps SelectedCountry to NOT rerender when `savedCountry` changes, because it's not used in SelectedCountry
country={selectedCountry}
onCountrySaved={() => setSavedCountry(selectedCountry)}
/>
)
}, [selectedCountry])
return (
<>
<h1>Country settings</h1>
<div css={contentCss}>
{list}
{selected}
</div>
</>
)
}
React.memo
Chỉ re-render component dc wrap bởi React.memo
khi props
của component thay đổi (using useCallback
and useMemo
in the parent component for values which are used as props
can reduce their number of changes).
Wrap React.memo
correctly at high level components → các component ở dưới cũng sẽ ko bị re-render.
Example for all of 3
const Parent = () => {
const someFunction = () => 'Some value'
//1. Parent re-render, cachedFn do có useCallback ko bị create lại.
const cachedFn = useCallback(someFunction, [])
//2. prop của Children là cachedFn ko bị create lại...
return <Children expensiveFn={cachedFn} />
}
export default Parent
const Children = ({ todos, text }) => {
const options = { searchParam: text } // Create lại mỗi lần Children re-render
const uncachedTodos = filterTodos(todos, options) // Create lại mỗi lần Children re-render
const cachedTodos = useMemo(() => {
// Declare `options` in `useMemo` won't let it be re-created as `cachedTodos` ko bị create lại
const options = { searchParam: text }
return filterTodos(todos, options)
}, [todos, text])
return (
<>
{/* `uncachedTodos` là ref value -> `value` prop của Kid1 thay đổi mỗi khi Children re-render */}
<Kid1 value={uncachedTodos} />
{/* Chỉ khi `cachedTodos` thay đổi (khi `todos` và `text` thay đổi) thì `value` prop của Kid2 mới đổi */}
<Kid2 value={cachedTodos} />
</>
)
}
//3. Children sẽ KO re-render khi Parent re-render do prop (ở đây là cachedFn) ko đổi
export default React.memo(Children)
useRef
Usage
Lets you reference a value that’s NOT needed for rendering. You should only use them when you have to “step outside React"
- Communicate with external APIs - often to store DOM elements and control them with browser APIs that doesn't impact the appearance of the component (
focus
,scrollIntoView
) - Storing data that isn't used when calculating the JSX (changing them doesn’t require a re-render), e.g.
timeoutId
, user input,shouldThrottleRef
,...
- Infinite Loading
- Animation
useEffect(() => {
const observer = new IntersectionObserver(
async (entries) => {
// `entries` is an array of observed DOM nodes, in this case we only track one element (the last job card)
// "intersecting" means the element is PARTIALLY visible in the viewport, which can be configured by `threshold` option
entries[0].isIntersecting && loadMore()
},
{
threshold: 0.2,
},
)
// Assume that every element have a class of `job-card`
const lastElement = document.querySelector('.job-card:last-of-type')
if (lastElement) observer.observe(lastElement)
return () => {
if (lastElement) observer.unobserve(lastElement)
}
}, [isLoading, loadMore])
import { useState, useEffect, useRef } from 'react'
export const useViewport = () => {
const [isVisible, setIsVisible] = useState(false)
const ref = useRef()
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsVisible(entry.isIntersecting)
observer.unobserve(entry.target) // If animation is one-time
}
})
const observer2 = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add(
'animate-in',
'slide-in-from-bottom',
'opacity-100',
)
entry.target.classList.remove('opacity-0')
}
})
})
const current = ref.current
if (current) {
observer.observe(current)
}
return () => {
if (current) {
observer.unobserve(current)
}
}
}, [])
return [ref, isVisible]
}
Do not write or read ref.current
during rendering
function MyComponent() {
// ...
// ❌ Don't write a ref during rendering
myRef.current = 123
// ...
// ❌ Don't read a ref during rendering
return <h1>{myOtherRef.current}</h1>
}
You can read or write refs from event handlers or effects instead.
function MyComponent() {
// ...
useEffect(() => {
// ✅ You can read or write refs in effects
myRef.current = 123
})
// ...
function handleClick() {
// ✅ You can read or write refs in event handlers
doSomething(myOtherRef.current)
}
// ...
}
forwardRef
const MyButton = React.forwardRef(
<HTMLButtonElement, ButtonProps>({ className, ...props }, ref) => {
return <button className={className} ref={ref} />
},
)
export default function App() {
return <MyButton ref={buttonRef} />
}
<MyButton ref={buttonRef}>
tells React to put the corresponding DOM node intobuttonRef.current
. However, it’s up to theMyButton
component to opt into that. By default, it doesn’t.- The
MyButton
component is declared usingforwardRef
. This opts it into receiving thebuttonRef
. MyButton
itself passes theref
it received to the<button>
inside of it.
useRef
vs useState
vs let
useRef | useState | let | |
---|---|---|---|
Value is preserved khi Component re-render | ✅ | ✅ | ❌ |
Updating value requires Component re-render | ❌ | ✅ | ❌ |
Value is unique for every instance of the Component | ✅ | ✅ | ❌ |
Mutable | ✅ | ❌ | ✅ |
useState
: Values used for rendering, có thể thay đổi và ~ thay đổi đó require a re-render.useRef
: Values NOT used for rendering, có thể thay đổi và ~ thay đổi đó do NOT require a re-render e.g. user's search box input,timeoutId
.JS variable
: Values NOT used for rendering, constant, can be shared among mutliple instances of the component e.g.env
values.
Snippet: One ref
for a list of elements
const [index, setIndex] = useState(0)
const selectedRef = useRef(null)
{
cats.map((cat, i) => (
<Cat
key={cat.id}
cat={cat}
onClick={() => {
setIndex(i)
}}
ref={index === i ? selectedRef : null}
/>
))
}
useState
& useReducer
Usage & Comparison
Usage: Both are used to manage Narrow State. useReducer
dùng cho ~ Narrow State mà có nhiều case.
Use useContext
or Redux for Wide State.
Read more: So sánh
useState
&useReducer
.
useState
A state variable’s value never changes within a render, even if a event handler affecting it is asynchronous
function Counter() {
// `compute` only run once when initializing the state
const [count, setCount] = useState(() => compute())
// `compute` will run every time Counter re-render, but its return value is ignored because the state has already been initialized.
const [count2, setCount2] = useState(compute())
const handleDelete = (id) => {
// Updater function: Update dựa theo prev state, convention là letter đầu tiên (e.g. count -> c)
// Useful in cases where the new state depends on the old state
setListItems((prev) => prev.filter((item) => item.id !== id))
}
const handleClick = () => {
setCount((c) => c + 5)
// Alert 0 instead of 5 even though `count` changed to 5
// Because `handleClick` function is a closure - it can only see the values of variables as they existed when the function was defined
// In other words, these state variables were a snapshot at the time the button was clicked
setTimeout(() => {
alert(count)
}, 1000)
}
return <button onClick={handleClick}>+5</button>
}
useReducer
import { useReducer } from 'react'
const infoReducer = (state, action) => {
switch (action.type) {
case 'loading':
return { loading: true }
break
case 'success':
return { ...action, loading: false }
break
default:
throw new Error('Unknown action: ' + action.type)
}
}
function ReducerExample() {
const [info, dispatch] = useReducer(infoReducer, {
loading: false,
data: '',
})
const getInfo = () => {
dispatch({ type: 'loading' }) // info = `state` line 3 = { loading: true, data: "", type: "loading" }
setTimeout(() => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((json) => dispatch({ type: 'success', data: json })) // info = { loading: false, data: {...}, type: "success" }
}, 1000)
}
return (
<>
{info.loading ? <h5>Loading...</h5> : <h5>{info.data.title}</h5>}
<button onClick={getInfo}>Get data</button>
</>
)
}
export default ReducerExample
Custom Hook
- Better project structure by implementing the logic in a Custom Hook, then using them in Presentational Components, similar to Container/ Presentational Pattern.
- Let you share stateful logic to React components but NOT the state itself (Each component that uses the same Custom Hook will get its own isolated instance of state).
// Custom hook MUST start with "use"
function useCounterUseCase({ required = 'default', ...rest }) {
const [value, setValue] = useState(0)
return { value, setValue }
}
import useCounterUseCase from './useCounterUseCase.jsx'
function Container() {
const { value, setValue } = useCounterUseCase({
required: 'meo',
optional1: '1',
optional2: '2',
})
}