Skip to main content

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.

info

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.

// 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()
...
}

Read more: Kết hợp giữa useReducerContext để manage wide sate

Context alternative: Reduce prop drilling using Composition

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

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)
Addingpush, unshiftconcat, [...arr1, ...arr2] (spread syntax)
Removingsplice, pop, shiftfilter, slice
Replacingsplice, arr[i] = ... assignmentmap
Sortingsort, reverseCopy the array by spread syntax first: cons nextList = [...list] -> nextList.reverse()

Unlike React, Vue is able to detect when a reactive array's mutation methods are called and trigger necessary updates.

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.

Preserving state

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