State
Pre-existing inputs
These are 3 kinds of "inputs" that you can read while rendering. You should always treat these inputs as read-only.
Props
The object passed to the function component, included children. Props are like arguments you pass to a function. They let a parent component pass data to a child component and customize its appearance. For example, a Form
can pass a color
prop to a Button
.
State
State is like a component’s memory. It lets a component keep track of some information and change it in response to interactions. For example, a Button
might keep track of isHovered
state.
Context
Context lets the parent component make some information available to any component in the tree below it—no matter how deep—without passing it explicitly through props, thus avoid prop drilling.
It’s designed for data that changes over time, like the current user’s theme, or the preferred language.
The biggest weakness of the Context API is that a component cannot selectively subscribe to parts of a context value, so all components reading that context will re-render when the value is updated.
Prop drilling can make your app hard to maintain. If you want to add or remove a prop to/from a component, you often have to make the same change in all of the components above it in the tree.
- Full Example
- Example: I18nContext.tsx
- I18n Usage: SomeComponent.tsx
// Make sure the file extension is `.tsx`
import { createContext, useContext, useState, useMemo, ReactNode } from 'react'
import Layout from '../components/layout'
export type Currency = 'USD' | 'VND'
export type CartContextProps =
| {
currency: Currency
setCurrency: (currency: Currency) => void
}
| undefined
// Init context with no arg (default to `undefined`) or `null` is a must to check if `useCart` is used within a `CartProvider` (see `useCart` below)
const CartContext = createContext<CartContextProps>()
// B1: Create Provider
export const CartProvider = ({ children }: { children: ReactNode }) => {
// To update context, you need to combine it with state
const [currency, setCurrency] = useState<Currency>('USD')
// When `CartProvider` re-render, components consuming the Context also re-render
// Memoizing the value passed to `Provider` will reduce unnecessary re-renders if CartProvider is NOT a top-level component...
// Being at the top-level means it has no parent components that can trigger re-rendering to it...
// It only gets re-rendered when `setCurrency` is called from a `child` in the Context, so `useMemo` do nothing here
const memoizedValue = useMemo(() => ({ currency, setCurrency }), [currency])
return (
<CartContext.Provider value={memoizedValue}>
{children}
</CartContext.Provider>
)
}
export const useCart = () => {
const ctx = useContext(CartContext)
if (!ctx) throw new Error('useCart must be used within a CartProvider')
return ctx
}
function MyApp({ Component, pageProps }) {
return (
// B2: Wrap Provider cho ~ part cần
<CartProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</CartProvider>
)
}
function SomeChildComponent() {
// B3: Use Context
const { currency, setCurrency } = useCart()
...
}
import {
createContext,
useContext,
useState,
useCallback,
useMemo,
type ReactNode,
} from 'react'
const translations = {
en: {
'Welcome to React': 'Welcome to React',
},
vi: {
'Welcome to React': 'Chào mừng đến với React',
},
}
export type Language = 'vi' | 'en'
export type I18nContextProps =
| {
language: Language
setLanguage: (language: Language) => void
t: (key: string) => string
}
| undefined
export const I18nContext = createContext<I18nContextProps>(undefined)
export const I18nProvider = ({ children }: { children: ReactNode }) => {
const [language, setLanguage] = useState<Language>('vi')
const t = useCallback(
(key: string) => translations[language][key] || key,
[language],
)
const memoizedValue = useMemo(
() => ({
language,
setLanguage,
t,
}),
[language, setLanguage, t],
)
return (
<I18nContext.Provider value={memoizedValue}>
{children}
</I18nContext.Provider>
)
}
export const useTranslation = () => {
const context = useContext(I18nContext)
if (!context) {
throw new Error('useTranslation must be used within a I18nProvider')
}
return context
}
The API is the same as react-i18next
const { t } = useTranslation()
return <h1>{t('Welcome to React')}</h1>
Read more: Kết hợp giữa
useReducer
vàContext
để manage wide sate
Context alternative: Reduce prop drilling using Composition
- Before
- After
export default function App({ currentUser }) {
return <Dashboard user={currentUser} />
}
function Dashboard({ user }) {
return (
<main>
<h2>The Dashboard</h2>
<DashboardContent user={user} />
</main>
)
}
function DashboardContent({ user }) {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage user={user} />
</div>
)
}
function WelcomeMessage({ user }) {
return <p>Welcome {user.name}</p>
}
// Dashboard and DashboardContent are no longer passed irrelevant data
export default function App({ currentUser }) {
return (
<Dashboard>
<DashboardContent>
<WelcomeMessage user={currentUser} />
</DashboardContent>
</Dashboard>
)
}
function Dashboard({ children }) {
return (
<main>
<h2>The Dashboard</h2>
{children}
</main>
)
}
function DashboardContent({ children }) {
return (
<div>
<h3>Dashboard Content</h3>
{children}
</div>
)
}
function WelcomeMessage({ user }) {
return <p>Welcome {user.name}</p>
}
This usually only works in simple app because it can make higher-level components more complicated and forces the lower-level components to be more flexible than you may want. For larger and more complex app, Context is a better option.
Local mutation
Pure functions don’t mutate variables outside of the function’s scope or objects that were created before the call — that makes them impure!
// This will not re-render as expected because the reference of `todos` hasn’t changed
const [todos, setTodos] = useState(someTodosArray)
const handleClick = () => {
todos[3].completed = true
setTodos(todos)
}
However, it’s completely fine to change variables and objects that you’ve just created while rendering. In this example, you create an []
array, assign it to a cups
variable, and then push
a dozen cups into it:
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>
}
export default function TeaGathering() {
let cups = []
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />)
}
return cups
}
If the cups
variable or the []
array were created outside the TeaGathering
function, this would be a huge problem! You would be changing a preexisting object by pushing items into that array.
However, it’s fine because you’ve created them during the same render (render nghĩa là call Component function - ở đây là call TeaGathering
, với mỗi lần call thì cups
dc tạo lại trong lần call đấy). No code outside of TeaGathering
will ever know that this happened. This is called “local mutation” — it’s like your component’s little secret.
Object & Array inputs
You should treat Pre-existing Variable as immutable!
Instead of mutating an object/array, create a new version of it, and trigger a re-render by setting state to it.
Object
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
},
})
person.artwork.city = 'New Delhi' // ❌: Don't mutate state
// ✅: Create a new object and set the state to it
// If you want to update a nested property, you’ll have to use it more than once (bc `...` spread is "shallow" - it only copies things one level deep)
const nextArtwork = { ...person.artwork, city: 'New Delhi' }
const nextPerson = { ...person, artwork: nextArtwork }
setPerson(nextPerson)
Array
Avoid (mutates the array) | Prefer (returns a new array) | |
---|---|---|
Adding | push , unshift | concat , [...arr1, ...arr2] (spread syntax) |
Removing | splice , pop , shift | filter , slice |
Replacing | splice , arr[i] = ... assignment | map |
Sorting | sort , reverse | Copy the array by spread syntax first: cons nextList = [...list] -> nextList.reverse() |
Example: Inserting at any position to an array
const [artists, setArtists] = useState([])
function handleClick() {
const insertAt = 1 // Could be any index
const nextArtists = [
// Items before the insertion point:
...artists.slice(0, insertAt),
// New item:
{ id: nextId++, name: name },
// Items after the insertion point:
...artists.slice(insertAt),
]
setArtists(nextArtists)
}
Managing State
Preserving state
Same component (<section>
, <p>
)+ Same position (1st
, 2nd
element of the parent) → Preserves state. It means the structure of your tree needs to “match up” from one render to another.
// These 2 share the `count` state in the above image
isPlayerA ? <Counter person="Taylor" /> : <Counter person="Sarah" />
Resetting state at the same position
Option 1: Resetting state with a key
// These 2 won't share the `count` state!
{
isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)
}
Option 2: Rendering a component in different positions
// These 2 won't share the `count` state!
{
isPlayerA && <Counter person="Taylor" />
}
{
!isPlayerA && <Counter person="Sarah" />
}