Skip to main content

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

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

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).
caution

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.

note

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.

note

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

note

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,...
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])

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} />
}
  1. <MyButton ref={buttonRef}> tells React to put the corresponding DOM node into buttonRef.current. However, it’s up to the MyButton component to opt into that. By default, it doesn’t.
  2. The MyButton component is declared using forwardRef. This opts it into receiving the buttonRef.
  3. MyButton itself passes the ref it received to the <button> inside of it.

useRef vs useState vs let

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

Demo CodeSandbox

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

  1. Better project structure by implementing the logic in a Custom Hook, then using them in Presentational Components, similar to Container/ Presentational Pattern.
  2. 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',
})
}