Appearance
State with useState (and when useReducer shows up)
In Vue you poke a
ref. In React you call a setter and let the next render pick it up.
useState - the daily driver
tsx
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<button type="button" onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
)
}Comments for the code above:
useState(0)returns[value, setter]- likeref+ writing to.value, but split- Calling
setCountschedules a re-render - you don't mutatecount - Initial state argument runs on first mount (pass a function if it's expensive:
useState(() => bigCompute()))
Don't mutate - replace
Vue reactive objects forgive a lot of in-place mutation. React state wants a new value:
tsx
const [todos, setTodos] = useState<{ id: number; text: string }[]>([])
// add
setTodos([...todos, { id: Date.now(), text: 'Ship it' }])
// update one
setTodos(todos.map((t) => (t.id === id ? { ...t, text: 'Done' } : t)))
// remove
setTodos(todos.filter((t) => t.id !== id))As you can see - spread / map / filter. Same immutability story as Redux-ish Vue code, just everywhere.
Functional updates
If the next value depends on the previous, prefer the callback form - especially in effects or rapid clicks:
tsx
setCount((c) => c + 1)Why: React may batch updates. Reading count from the closure can be stale. The callback gets the latest queued value.
Multiple pieces of state
tsx
const [name, setName] = useState('')
const [age, setAge] = useState(0)Or one object (closer to reactive):
tsx
const [form, setForm] = useState({ name: '', age: 0 })
setForm({ ...form, name: 'Ada' })I've found: separate useState calls for unrelated things, one object when fields travel together (forms).
Derived state - just compute it
Vue computed:
ts
const fullName = computed(() => `${first.value} ${last.value}`)React - during render:
tsx
const fullName = `${first} ${last}`Don't put that in useEffect + useState. That's a classic "I fought the framework" move. If it's pure derivation, compute it.
useReducer - when updates get spicy
If you have many related transitions (like a mini state machine), useReducer reads nicer:
tsx
type State = { count: number }
type Action = { type: 'inc' } | { type: 'dec' } | { type: 'set'; value: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'inc':
return { count: state.count + 1 }
case 'dec':
return { count: state.count - 1 }
case 'set':
return { count: action.value }
default:
return state
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<>
<p>{state.count}</p>
<button type="button" onClick={() => dispatch({ type: 'inc' })}>+</button>
</>
)
}Comments for the code above:
- Same idea as a tiny Vuex/Pinia slice without the global store
- I reach for it when
setStatecallbacks get nested and sad
Next: effects - the chapter where Vue watch instincts both help and betray you.
Enjoy!