Goodbye, useState()
useState()
useState()
useState()
useState()
useState()
useState()
useEffect()
useDefect()
useFoot(() => { // 🦶🔫 setGun(true); });


We need to talk about
useState



Closures, batching, weird stuff
useState
const [count, setCount] = useState(0);
return (
<button onClick={() => {
setCount(count + 60);
setCount(count + 8);
setCount(count + 1);
}}>
Count: {count}
</button>
);
1
const [count, setCount] = useState(0);
return (
<button onClick={() => {
setCount(c => c + 1);
setCount(c => c * 2);
setCount(c => c + 40);
}}>
Count: {count}
</button>
);
42
const [set, setSet] = useState(new Set());
return (
<button onClick={() => {
setSet(set.add('set'));
}}>
Set set
</button>
);
const [[set], setSet] = useState([new Set()]);
return (
<button onClick={() => {
setSet([set.add('set')]);
}}>
Set set
</button>
);
🤷
Closures, batching, weird stuff
useState
- Use state setters
- Beware of closures
- Keep object identity in mind
Controlling rerenders
useState
function Component() {
const [value, setValue] = useState('bonjour');
return (
<Foo>
<h1>{value}</h1>
<Bar />
<Baz />
<Quo>
<Inner someProp={value} onChange={setValue} />
</Quo>
</Foo>
);
}
Entire component tree rerenders
function CounterWithUseState() {
const [clickCount, setClickCount] = useState(0);
const handleClick = () => {
// ⚠️ Rerender!
setClickCount(clickCount + 1);
analytics
.trackEvent('button_clicked', { count: clickCount + 1 });
};
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
function CounterWithUseRef() {
const clickCount = useRef(0);
const handleClick = () => {
// This updates the ref without causing a re-render
clickCount.current += 1;
analytics
.trackEvent('button_clicked', { count: clickCount.current });
};
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
Controlling rerenders
useState
- Keep useState as local as possible
- useRef for internal state that shouldn't rerender
URL as state
useState
import { useState } from 'react';
function SortComponent() {
const [sort, setSort] = useState<string | null>(null);
return (
<>
<p>Current sort: {sort}</p>
<button onClick={() => setSort('asc')}>ASC</button>
<button onClick={() => setSort('desc')}>DESC</button>
</>
);
}
import { usePathname, useSearchParams } from 'next/navigation';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
function ExampleClientComponent() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const sort = searchParams.get('sort');
const createQueryString = useCallback(
(name: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set(name, value);
return params.toString();
},
[searchParams]
);
return (
<>
<p>Current sort: {sort}</p>
<button
onClick={() => {
router.push(pathname + '?' + createQueryString('sort', 'asc'));
}}
>
ASC
</button>
<button
onClick={() => {
router.push(pathname + '?' + createQueryString('sort', 'desc'));
}}
>
DESC
</button>
</>
);
}
import { useSearchParams } from '@remix-run/react';
function SortComponent() {
const [searchParams, setSearchParams] = useSearchParams();
const sort = searchParams.get('sort');
return (
<>
<p>Current sort: {sort}</p>
<button onClick={() => setSearchParams({ sort: 'asc' })}>ASC</button>
<button onClick={() => setSearchParams({ sort: 'desc' })}>DESC</button>
</>
);
}
"use client";
import { parseAsInteger, useQueryState } from "nuqs";
export function Demo() {
const [hello, setHello] = useQueryState("hello", { defaultValue: "" });
const [count, setCount] = useQueryState(
"count",
parseAsInteger.withDefault(0),
);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
<input
value={hello}
placeholder="Enter your name"
onChange={(e) => setHello(e.target.value || null)}
/>
<p>Hello, {hello || "anonymous visitor"}!</p>
</>
);
}

URL as state
useState
Use search params for URL-persisted state
Watch the next talk on nuqs
Fetching data
useState
'use client';
function ProductDetailsClient({ productId }) {
const [product, setProduct] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState('description');
useEffect(() => {
setIsLoading(true);
fetchProduct(productId)
.then(data => {
setProduct(data);
setIsLoading(false);
})
.catch(err => {
setError(err.message);
setIsLoading(false);
});
}, [productId]);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
if (!product) return null;
return (
<div>
<h1>{product.name}</h1>
<div className="tabs">
<button
className={activeTab === 'description' ? 'active' : ''}
onClick={() => setActiveTab('description')}
>
Description
</button>
<button
className={activeTab === 'specs' ? 'active' : ''}
onClick={() => setActiveTab('specs')}
>
Specifications
</button>
<button
className={activeTab === 'reviews' ? 'active' : ''}
onClick={() => setActiveTab('reviews')}
>
Reviews
</button>
</div>
{activeTab === 'description' && <div>{product.description}</div>}
{activeTab === 'specs' && <SpecsTable specs={product.specifications} />}
{activeTab === 'reviews' && <ReviewsList reviews={product.reviews} />}
<AddToCartForm product={product} />
</div>
);
}

async function ProductDetailsServer({ productId, searchParams }) {
const product = await fetchProduct(productId);
const activeTab = searchParams.tab ?? 'description';
return (
<div>
<h1>{product.name}</h1>
{/* Client component just for the tab switching UI */}
<ClientTabs activeTab={activeTab} />
{/* Server rendered content based on active tab */}
{activeTab === 'description' && <div>{product.description}</div>}
{activeTab === 'specs' && <SpecsTable specs={product.specifications} />}
{activeTab === 'reviews' && <ReviewsList reviews={product.reviews} />}
{/* Client component for the interactive cart functionality */}
<ClientAddToCartForm productId={product.id} price={product.price} />
</div>
);
}
<Suspense>
I'll keep you in
Just read the docs:
react.dev/reference/react/Suspense
'use client';
import { useTransition } from 'react';
import { makeCafeAllonge } from '../actions';
export function AddFlowButton() {
const [isPending, startTransition] = useTransition();
return (
<button
onClick={() => {
startTransition(async () => {
await makeCafeAllonge();
});
}}
disabled={isPending}
>
{isPending ? 'Making...' : 'Make a cafe allongé'}
</button>
);
}


It handles fetching data better than you
Fetching data
useState
- Use server components when applicable
- Use Suspense for loading states
- useTransition to handle local pending states*
- Eliminate boilerplate; use query libraries
Forms
useState
import { z } from 'zod';
import { useState } from 'react';
const User = z.object({
firstName: z.string(),
variable: z.string(),
bio: z.string(),
});
function App() {
const [firstName, setFirstName] = useState('');
const [variable, setVariable] = useState('');
const [bio, setBio] = useState('');
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = { firstName, variable, bio };
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<label>
<strong>Name</strong>
<input
name="firstName"
placeholder="First name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
</label>
<label>
<strong>Favorite variable</strong>
<select
name="variable"
value={variable}
onChange={(e) => setVariable(e.target.value)}
>
<option value="">Select...</option>
<option value="foo">foo</option>
<option value="bar">bar</option>
<option value="baz">baz</option>
<option value="quo">quo</option>
</select>
</label>
<label>
<strong>Bio</strong>
<textarea
name="bio"
placeholder="About you"
value={bio}
onChange={(e) => setBio(e.target.value)}
/>
</label>
<p>Submit and check console</p>
<input type="submit" />
</form>
);
}
import { z } from 'zod';
const User = z.object({
firstName: z.string(),
variable: z.string(),
bio: z.string(),
});
function App() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.target);
const data = User.parse(Object.fromEntries(formData));
console.log(data);
};
return (
<form
onSubmit={handleSubmit}
>
<label>
<strong>Name</strong>
<input name="firstName" placeholder="First name" />
</label>
<label>
<strong>Favorite variable</strong>
<select name="variable">
<option value="">Select...</option>
<option value="foo">foo</option>
<option value="bar">bar</option>
<option value="baz">baz</option>
<option value="quo">quo</option>
</select>
</label>
<label>
<strong>Bio</strong>
<textarea name="bio" placeholder="About you" />
</label>
<p>Submit and check console</p>
<input type="submit" />
</form>
);
}
import { z } from 'zod';
import { Form } from '@remix-run/react';
const User = z.object({
firstName: z.string(),
variable: z.string(),
bio: z.string(),
});
function App() {
return (
<Form method="post" action="some-action">
<label>
<strong>Name</strong>
<input name="firstName" placeholder="First name" />
</label>
<label>
<strong>Favorite variable</strong>
<select name="variable">
<option value="">Select...</option>
<option value="foo">foo</option>
<option value="bar">bar</option>
<option value="baz">baz</option>
<option value="quo">quo</option>
</select>
</label>
<label>
<strong>Bio</strong>
<textarea name="bio" placeholder="About you" />
</label>
<p>Submit and check console</p>
<input type="submit" />
</Form>
);
}

'use client';
import { useActionState } from 'react';
import { createUser } from '@/app/actions';
const initialState = {
message: '',
};
export function Signup() {
const [state, formAction, pending] =
useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p>{state?.message}</p>
<button disabled={pending}>Sign up</button>
</form>
);
}
nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

'use server';
import { redirect } from 'next/navigation';
export async function createUser(prevState: any, formData: FormData) {
const email = formData.get('email');
const res = await fetch('https://...');
const json = await res.json();
if (!res.ok) {
return { message: 'Please enter a valid email' };
}
redirect('/dashboard');
}
nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations


Forms
useState
- Use native FormData for simple forms
- Use runtime type-checking libraries for parsing & validation
- Use built-in framework features for forms
- Use form libraries for advanced forms
Overusing state
useState
const [firstName, setFirstName] = useState('');
const [firstName, setFirstName] = useState('');
const [age, setAge] = useState(0);
const [address1, setAddress1] = useState('');
const [address2, setAddress2] = useState('');
const [city, setCity] = useState('');
const [state, setState] = useState('');
const [zip, setZip] = useState('');
const [name, setName] = useState('');
function ReactParisConference() {
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [location, setLocation] = useState('');
const [url, setUrl] = useState('');
const [image, setImage] = useState('');
const [price, setPrice] = useState(0);
const [attendees, setAttendees] = useState(0);
const [organizer, setOrganizer] = useState('');
const [countries, setCountries] = useState([]);
const [categories, setCategories] = useState([]);
const [tags, setTags] = useState([]);
const [swag, setSwag] = useState([]);
const [speakers, setSpeakers] = useState([]);
const [sponsors, setSponsors] = useState([]);
const [videos, setVideos] = useState([]);
const [tickets, setTickets] = useState([]);
const [schedule, setSchedule] = useState([]);
const [socials, setSocials] = useState([]);
const [coffee, setCoffee] = useState([]);
const [codeOfConduct, setCodeOfConduct] = useState('');
// ...
}
useState
use eight



VIBE CODING
STATE MANAGEMENT
function UserProfileForm() {
// Personal information
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [birthDate, setBirthDate] = useState('');
// Address information
const [street, setStreet] = useState('');
const [city, setCity] = useState('');
const [state, setState] = useState('');
const [zipCode, setZipCode] = useState('');
const [country, setCountry] = useState('US');
// Preferences
const [theme, setTheme] = useState('light');
const [emailNotifications, setEmailNotifications] = useState(true);
const [smsNotifications, setSmsNotifications] = useState(false);
const [language, setLanguage] = useState('en');
const [currency, setCurrency] = useState('USD');
// This requires individual handler functions for each field
const handleFirstNameChange = (e) => {
setFirstName(e.target.value);
};
const handleLastNameChange = (e) => {
setLastName(e.target.value);
};
const handleEmailChange = (e) => {
setEmail(e.target.value);
};
const handlePhoneChange = (e) => {
setPhone(e.target.value);
};
const handleBirthDateChange = (e) => {
setBirthDate(e.target.value);
};
const handleStreetChange = (e) => {
setStreet(e.target.value);
};
const handleCityChange = (e) => {
setCity(e.target.value);
};
const handleStateChange = (e) => {
setState(e.target.value);
};
const handleZipCodeChange = (e) => {
setZipCode(e.target.value);
};
const handleCountryChange = (e) => {
setCountry(e.target.value);
};
const handleThemeChange = (e) => {
setTheme(e.target.value);
};
const handleEmailNotificationsChange = (e) => {
setEmailNotifications(e.target.checked);
};
const handleSmsNotificationsChange = (e) => {
setSmsNotifications(e.target.checked);
};
const handleLanguageChange = (e) => {
setLanguage(e.target.value);
};
const handleCurrencyChange = (e) => {
setCurrency(e.target.value);
};
// When you need to do something with all the data, you have to gather it manually
const handleSubmit = (e) => {
e.preventDefault();
const userData = {
personal: {
firstName,
lastName,
email,
phone,
birthDate
},
address: {
street,
city,
state,
zipCode,
country
},
preferences: {
theme,
emailNotifications,
smsNotifications,
language,
currency
}
};
saveUserProfile(userData);
};
// Reset all the fields individually
const handleReset = () => {
setFirstName('');
setLastName('');
setEmail('');
setPhone('');
setBirthDate('');
setStreet('');
setCity('');
setState('');
setZipCode('');
setCountry('US');
setTheme('light');
setEmailNotifications(true);
setSmsNotifications(false);
setLanguage('en');
setCurrency('USD');
};
return (
<form onSubmit={handleSubmit}>
<h2>Personal Information</h2>
<div>
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
value={firstName}
onChange={handleFirstNameChange}
/>
</div>
<div>
<label htmlFor="lastName">Last Name</label>
<input
id="lastName"
value={lastName}
onChange={handleLastNameChange}
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={handleEmailChange}
/>
</div>
<div>
<label htmlFor="phone">Phone</label>
<input
id="phone"
value={phone}
onChange={handlePhoneChange}
/>
</div>
<div>
<label htmlFor="birthDate">Birth Date</label>
<input
id="birthDate"
type="date"
value={birthDate}
onChange={handleBirthDateChange}
/>
</div>
<h2>Address</h2>
{/* Address fields with their own handlers */}
<h2>Preferences</h2>
{/* Preference fields with their own handlers */}
<div>
<button type="submit">Save Profile</button>
<button type="button" onClick={handleReset}>Reset</button>
</div>
</form>
);
}
function UserProfileForm() {
const [userProfile, setUserProfile] = useState({
personal: {
firstName: '',
lastName: '',
email: '',
phone: '',
birthDate: ''
},
address: {
street: '',
city: '',
state: '',
zipCode: '',
country: 'US'
},
preferences: {
theme: 'light',
emailNotifications: true,
smsNotifications: false,
language: 'en',
currency: 'USD'
}
});
const handleChange = (section, field, value) => {
setUserProfile(prevProfile => ({
...prevProfile,
[section]: {
...prevProfile[section],
[field]: value
}
}));
};
const handleReset = () => {
setUserProfile({
personal: {
firstName: '',
lastName: '',
email: '',
phone: '',
birthDate: ''
},
address: {
street: '',
city: '',
state: '',
zipCode: '',
country: 'US'
},
preferences: {
theme: 'light',
emailNotifications: true,
smsNotifications: false,
language: 'en',
currency: 'USD'
}
});
};
return (
<form onSubmit={handleSubmit}>
<h2>Personal Information</h2>
<div>
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
value={firstName}
onChange={ev => handleChange('personal', 'firstName', ev.target.value)}
/>
</div>
{/* Other UI components */}
</form>
);
}
import { produce } from 'immer';
// ...
setUserProfile(profile => produce(profile, draft => {
// Not really mutating!
draft.personal.firstName = event.target.value;
}));
function CheckoutWizardBad() {
const [isAddressStep, setIsAddressStep] = useState(true);
const [isPaymentStep, setIsPaymentStep] = useState(false);
const [isConfirmationStep, setIsConfirmationStep] = useState(false);
const [isCompleteStep, setIsCompleteStep] = useState(false);
const goToPayment = () => {
setIsAddressStep(false);
setIsPaymentStep(true);
setIsConfirmationStep(false);
setIsCompleteStep(false);
};
const goToConfirmation = () => {
setIsAddressStep(false);
setIsPaymentStep(false);
setIsConfirmationStep(true);
setIsCompleteStep(false);
};
// More transition functions...
return (
<div>
{isAddressStep && <AddressForm onNext={goToPayment} />}
{isPaymentStep && <PaymentForm onNext={goToConfirmation} />}
{/* Other steps... */}
</div>
);
}
function CheckoutWizardGood() {
const [currentStep, setCurrentStep] = useState('address');
const goToStep = (step) => {
setCurrentStep(step);
};
return (
<div>
{currentStep === 'address' && (
<AddressForm onNext={() => goToStep('payment')} />
)}
{currentStep === 'payment' && (
<PaymentForm onNext={() => goToStep('confirmation')} />
)}
{/* Other steps... */}
</div>
);
}
Overusing state
useState
- Group related state together
- Use Immer for complex state updates
- Use string enums to reduce booleans
- Think before vibe coding
Discriminated unions
useState
import { useEffect, useState } from "react";
function DataFetchExample() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
return (
<div>
<button onClick={fetchData}>Refresh Data</button>
{loading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
{data && <p>Data: {JSON.stringify(data)}</p>}
</div>
);
}

import { useEffect, useState } from "react";
type DataStatus = 'idle' | 'loading' | 'success' | 'error';
function DataFetchExample() {
const [status, setStatus] = useState<DataStatus>('idle');
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const fetchData = async () => {
try {
setStatus('loading');
setError(null);
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const result = await response.json();
setStatus('success');
setData(result);
} catch (err) {
setStatus('error');
setError(err.message);
}
};
useEffect(() => {
fetchData();
}, []);
return (
<div>
<button onClick={fetchData}>Refresh Data</button>
{status === 'loading' && <p>Loading...</p>}
{status === 'error' && <p>Error: {error}</p>}
{status === 'success' && <p>Data: {JSON.stringify(data)}</p>}
</div>
);
}
import { useEffect, useState } from 'react';
type DataState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error'; error: string }
| { status: 'success'; data: any };
function DataFetchExample() {
const [state, setState] = useState<DataState>({ status: 'idle' });
const fetchData = async () => {
setState({ status: 'loading' });
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const result = await response.json();
setState({ status: 'success', data: result });
} catch (err) {
setState({ status: 'error', error: err.message });
}
};
useEffect(() => {
fetchData();
}, []);
return (
<div>
<button onClick={fetchData}>Refresh Data</button>
{state.status === 'loading' && <p>Loading...</p>}
{state.status === 'error' && <p>Error: {state.error}</p>}
{state.status === 'success' && <p>Data: {JSON.stringify(state.data)}</p>}
</div>
);
}
Discriminated
union
Discriminated unions
useState
- Use string enums to reduce booleans
- Use discriminated unions for type safety
- Group related state in discriminated unions
Derived state
useState
function DonutOrder() {
const [selectedDonuts, setSelectedDonuts] = useState([
{ id: 1, name: 'Glazed', price: 1.99, quantity: 2 },
{ id: 2, name: 'Chocolate', price: 2.49, quantity: 1 },
{ id: 3, name: 'Sprinkled', price: 2.29, quantity: 3 }
]);
// Derived state - should be calculated directly!
const [totalItems, setTotalItems] = useState(0);
const [subtotal, setSubtotal] = useState(0);
const [tax, setTax] = useState(0);
const [total, setTotal] = useState(0);
// Recalculate derived values whenever selectedDonuts changes
useEffect(() => {
const itemCount = selectedDonuts.reduce((sum, item) => sum + item.quantity, 0);
const itemSubtotal = selectedDonuts.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const itemTax = itemSubtotal * 0.08;
const itemTotal = itemSubtotal + itemTax;
setTotalItems(itemCount);
setSubtotal(itemSubtotal);
setTax(itemTax);
setTotal(itemTotal);
}, [selectedDonuts]);
const updateQuantity = (id, newQuantity) => {
setSelectedDonuts(
selectedDonuts.map(donut =>
donut.id === id ? { ...donut, quantity: newQuantity } : donut
)
);
// The derived values will be updated by the useEffect
};
return (
<div className="donut-order">
<h2>Donut Order</h2>
{selectedDonuts.map(donut => (
<div key={donut.id} className="donut-item">
<span>{donut.name} (${donut.price.toFixed(2)})</span>
<input
type="number"
min="0"
value={donut.quantity}
onChange={(e) => updateQuantity(donut.id, parseInt(e.target.value))}
/>
</div>
))}
<div className="order-summary">
<p>Items: {totalItems}</p>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<p>Total: ${total.toFixed(2)}</p>
</div>
</div>
);
}
function DonutOrder() {
const [selectedDonuts, setSelectedDonuts] = useState([
{ id: 1, name: 'Glazed', price: 1.99, quantity: 2 },
{ id: 2, name: 'Chocolate', price: 2.49, quantity: 1 },
{ id: 3, name: 'Sprinkled', price: 2.29, quantity: 3 }
]);
// Calculate all derived values directly during render - no useState or useEffect needed
const totalItems = selectedDonuts.reduce((sum, item) => sum + item.quantity, 0);
const subtotal = selectedDonuts.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const tax = subtotal * 0.08; // 8% tax
const total = subtotal + tax;
const updateQuantity = (id, newQuantity) => {
setSelectedDonuts(
selectedDonuts.map(donut =>
donut.id === id ? { ...donut, quantity: newQuantity } : donut
)
);
// All derived values will be recalculated automatically on the next render
};
return (
<div className="donut-order">
<h2>Donut Order</h2>
{selectedDonuts.map(donut => (
<div key={donut.id} className="donut-item">
<span>{donut.name} (${donut.price.toFixed(2)})</span>
<input
type="number"
min="0"
value={donut.quantity}
onChange={(e) => updateQuantity(donut.id, parseInt(e.target.value))}
/>
</div>
))}
<div className="order-summary">
<p>Items: {totalItems}</p>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<p>Total: ${total.toFixed(2)}</p>
</div>
</div>
);
}

Derived state
useState
useState only for primary state
Derive state directly in component
useMemo if you need to
Reducing state logic
useState



useReducer



useState
import { useReducer } from 'react';
function Component({ count }) {
const [isActive, toggle] =
useReducer(a => !a, true);
return <>
<output>{isActive
? 'oui'
: 'non'
}</output>
<button onClick={toggle} />
</>
}
const donutInventory = {
chocolate: {
quantity: 10,
price: 1.5,
},
vanilla: {
quantity: 10,
price: 1.5,
},
strawberry: {
quantity: 10,
price: 1.5,
},
};
type Donut = keyof typeof donutInventory;
import { useState } from 'react';
export function DonutShop() {
const [donuts, setDonuts] = useState<Donut[]>([]);
const addDonut = (donut: Donut) => {
const orderedDonuts = donuts.filter((d) => d === donut).length;
if (donutInventory[donut].quantity > orderedDonuts) {
setDonuts([...donuts, donut]);
}
};
const removeDonut = (donutIndex: number) => {
setDonuts(donuts.filter((_, index) => index !== donutIndex));
};
const total = donuts.reduce(
(acc, donut) => acc + donutInventory[donut].price,
0
);
return (
<div>
<h1>Donut Shop</h1>
<div>
{Object.keys(donutInventory).map((donut) => (
<div key={donut}>
<button onClick={() => addDonut(donut as Donut)}>
Add {donut}
</button>
</div>
))}
</div>
<div>
{donuts.map((donut, index) => (
<div key={donut}>
<p>{donut}</p>
<button onClick={() => removeDonut(index)}>Remove {donut}</button>
</div>
))}
</div>
<div>
<p>Total: {total}</p>
</div>
</div>
);
}
import { useReducer } from 'react';
type Action =
| { type: 'ADD_DONUT'; donut: Donut }
| { type: 'REMOVE_DONUT'; donut: Donut };
function donutReducer(state: Donut[], action: Action): Donut[] {
switch (action.type) {
case 'ADD_DONUT': {
const orderedDonuts = state.filter(d => d === action.donut).length;
if (donutInventory[action.donut].quantity > orderedDonuts) {
return [...state, action.donut];
}
return state;
}
case 'REMOVE_DONUT':
return state.filter((_, index) => index !== action.donutIndex);
default:
return state;
}
}
export function DonutShop() {
const [donuts, dispatch] = useReducer(donutReducer, []);
const addDonut = (donut: Donut) => {
dispatch({ type: 'ADD_DONUT', donut });
};
const removeDonut = (donutIndex: number) => {
dispatch({ type: 'REMOVE_DONUT', donutIndex });
};
const total = donuts.reduce(
(acc, donut) => acc + donutInventory[donut].price,
0
);
return (
<div>
<h1>Donut Shop</h1>
<div>
{Object.keys(donutInventory).map((donut) => (
<div key={donut}>
<button onClick={() => addDonut(donut as Donut)}>
Add {donut}
</button>
</div>
))}
</div>
<div>
{donuts.map((donut, index) => (
<div key={donut}>
<p>{donut}</p>
<button onClick={() => removeDonut(index)}>Remove {donut}</button>
</div>
))}
</div>
<div>
<p>Total: {total}</p>
</div>
</div>
);
}
import { donutReducer } from './donutReducer';
describe('donutReducer', () => {
it('adds a donut if inventory allows', () => {
const newState = donutReducer([], {
type: 'ADD_DONUT',
donut: 'glazed'
});
expect(newState).toEqual(['glazed']);
});
// ...
});
Indirect
send(event)
Event-based
Direct
setState(value)
Value-based
Causality
What caused the change
Context
Parameters related to the change
Timing
When the change happened
Traceability
The ability to log or replay changes
Indirect
send(event)
Event-based
Direct state management is easy.
Indirect state management is simple.
Reducing state logic
useState
useReducer for complex UI logic and
interdependent state updates
Use reducer actions to constrain state updates
useReducer for maximum testability
Use events for better observability
Hydration mismatches
useState
'use client';
import { useState } from 'react';
export default function Today() {
const [date] = useState(new Date());
return <div><p>{date.toISOString()}</p></div>;
}

'use client';
import { useEffect, useState } from 'react';
export default function Today() {
const [date, setDate] = useState(null);
useEffect(() => {
setDate(new Date());
}, []);
return <div>{date ? <p>{date.toISOString()}</p> : null}</div>;
}
'use client';
import { useSyncExternalStore } from 'react';
export default function Today() {
const date = useSyncExternalStore(
() => () => {}, // don't worry about this yet
() => new Date(), // client snapshot
() => null // server snapshot
);
return <div>{date ? <p>{date.toISOString()}</p> : null}</div>;
}



'use client';
import { useSyncExternalStore } from 'react';
export default function Today() {
const dateString = useSyncExternalStore(
() => () => {},
() => new Date().toISOString(),
() => null
);
return <div>{date ? <p>{dateString}</p> : null}</div>;
}

Syncing with external stores
useState

useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot?
)
export default function TodosApp() {
const todos = useSyncExternalStore(
todosStore.subscribe, // subscribe to store
todosStore.getSnapshot, // client snapshot
todosStore.getSnapshot // server snapshot
);
return (
<>
<button onClick={() => todosStore.addTodo()}>
Add todo
</button>
<hr />
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</>
);
}
let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];
export const todosStore = {
addTodo() {
todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
emitChange();
},
subscribe(listener) {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter(l => l !== listener);
};
},
getSnapshot() {
return todos;
}
};
function emitChange() {
for (let listener of listeners) {
listener();
}
}
import { useSyncExternalStore } from 'react';
function getSnapshot() {
return {
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0
};
}
function subscribe(callback) {
window.addEventListener('resize', callback);
return () => {
window.removeEventListener('resize', callback);
};
}
function WindowSizeIndicator() {
const windowSize = useSyncExternalStore(
subscribe,
getSnapshot,
getSnapshot
);
return <h1>Window size: {windowSize.width} x {windowSize.height}</h1>;
}
import { useSyncExternalStore } from 'react';
function useWindowSize() {
const getSnapshot = () => ({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0
});
const subscribe = (callback: () => void) => {
window.addEventListener('resize', callback);
return () => {
window.removeEventListener('resize', callback);
};
};
return useSyncExternalStore(
subscribe,
getSnapshot,
getSnapshot
);
}
function WindowSizeIndicator() {
const windowSize = useWindowSize();
return <h1>Window size: {windowSize.width} x {windowSize.height}</h1>;
}

Syncing with external stores
useState
useSyncExternalStore for preventing hydration mismatches
useSyncExternalStore for subscribing to browser APIs
useSyncExternalStore for external stores that don't already have
React integrations
Third-party libraries
useState
function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<output>{count}</output>
<button onClick={() => setCount(count + 1)}>Add</button>
<button onClick={() => setCount(count - 1)}>Subtract</button>
</section>
);
}
function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<output>{count}</output>
<button
onClick={() => {
if (count < 10) setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
if (count > 0) setCount(count - 1);
}}
>
Subtract
</button>
</section>
);
}
function Counter() {
const [count, setCount] = useState(0);
function increment(count) {
if (count < 10) setCount(count + 1);
}
function decrement(count) {
if (count > 0) setCount(count - 1);
}
return (
<section>
<output>{count}</output>
<input
type="number"
onBlur={(e) => {
setCount(e.target.valueAsNumber);
}}
/>
<button onClick={increment}>Add</button>
<button onClick={decrement}>Subtract</button>
</section>
);
}
function Counter() {
const [count, setCount] = useState(0);
function changeCount(val) {
if (val >= 0 && val <= 10) {
setCount(val);
}
}
return (
<section>
<output>{count}</output>
<input
type="number"
onBlur={(e) => {
changeCount(e.target.valueAsNumber);
}}
/>
<button
onClick={(e) => {
changeCount(count + 1);
}}
>
Add
</button>
<button
onClick={(e) => {
changeCount(count - 1);
}}
>
Subtract
</button>
</section>
);
}
function Counter() {
const [count, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
return (
<section>
<output>{count}</output>
<input
type="number"
onBlur={(e) => {
send({ type: "set", value: e.target.valueAsNumber });
}}
/>
<button
onClick={() => {
send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
const CountContext = createContext();
function CountView() {
const count = useContext(CountContext);
return (
<section>
<strong>Count: {count}</strong>
<button
onClick={() => {
// send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
// send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
function App() {
const [count, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
return (
<CountContext.Provider value={count}>
<CountView />
</CountContext.Provider>
);
}
const CountContext = createContext();
function CountView() {
const [count, send] = useContext(CountContext);
return (
<section>
<strong>Count: {count}</strong>
<button
onClick={() => {
send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
export function App() {
const [count, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
return (
<CountContext.Provider value={[count, send]}>
<CountView />
</CountContext.Provider>
);
}
const CountContext = createContext();
function CountView() {
const countStore = useContext(CountContext);
const [count, setCount] = useState(0);
useEffect(() => {
return countStore.subscribe(setCount);
}, []);
return (
<section>
<strong>Count: {count}</strong>
<button
onClick={() => {
countStore.send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
countStore.send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
function useCount() {
const [state, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
const listeners = useRef(new Set());
useEffect(() => {
listeners.current.forEach((listener) => listener(state));
}, [state]);
return {
send,
subscribe: (listener) => {
listeners.current.add(listener);
return () => {
listeners.current.delete(listener);
};
}
};
}
export function App() {
const countStore = useCount();
return (
<CountContext.Provider value={countStore}>
<CountView />
</CountContext.Provider>
);
}
const CountContext = createContext();
function useSelector(store, selectFn) {
const [state, setState] = useState(store.getSnapshot());
useEffect(() => {
return store.subscribe((newState) => setState(selectFn(newState)));
}, []);
return state;
}
function CountView() {
const countStore = useContext(CountContext);
const count = useSelector(countStore, (count) => count);
return (
<section>
<strong>Count: {count}</strong>
<button
onClick={() => {
countStore.send({ type: "inc" });
}}
>
Add
</button>
<button
onClick={() => {
countStore.send({ type: "dec" });
}}
>
Subtract
</button>
</section>
);
}
function useCount() {
const [state, send] = useReducer((state, event) => {
let currentCount = state;
if (event.type === "inc") {
currentCount = state + 1;
}
if (event.type === "dec") {
currentCount = state - 1;
}
if (event.type === "set") {
currentCount = event.value;
}
return Math.min(Math.max(0, currentCount), 10);
}, 0);
const listeners = useRef(new Set());
useEffect(() => {
listeners.current.forEach((listener) => listener(state));
}, [state]);
return {
send,
subscribe: (listener) => {
listeners.current.add(listener);
return () => {
listeners.current.delete(listener);
};
},
getSnapshot: () => state
};
}
function App() {
const countStore = useCount();
return (
<CountContext.Provider value={countStore}>
<CountView />
</CountContext.Provider>
);
}

Congrats, you just reinvented
🎉
Direct store
Direct store
Store via Context





stately.ai/docs/xstate-store
Store
npm i @xstate/store
import { createStore } from '@xstate/store';
const store = createStore({
context: {
count: 0
},
on: {
inc: (ctx) => ({
...ctx,
count: ctx.count + 1
})
}
});
store.subscribe(s => {
console.log(s.context.count);
});
store.trigger.inc();
// => 1
Store
npm i @xstate/store
import { useStore, useSelector } from '@xstate/store/react';
export function DonutShop() {
const donutStore = useStore({
context: {
donuts: [] as Donut[],
},
on: {
addDonut: (context, event: { donut: Donut }) => {
const orderedDonuts = context.donuts.filter(
(d) => d === event.donut
).length;
if (donutInventory[event.donut].quantity > orderedDonuts) {
return {
donuts: [...context.donuts, event.donut],
};
}
},
removeDonut: (context, event: { donutIndex: number }) => {
return {
donuts: context.donuts.filter(
(_, index) => index !== event.donutIndex
),
};
},
},
});
const donuts = useSelector(donutStore, (state) => state.context.donuts);
const total = donuts.reduce(
(acc, donut) => acc + donutInventory[donut].price,
0
);
return (
<div>
<h1>Donut Shop</h1>
<div>
{Object.keys(donutInventory).map((donut) => (
<div key={donut}>
<button
onClick={() =>
donutStore.trigger.addDonut({ donut: donut as Donut })
}
>
Add {donut}
</button>
</div>
))}
</div>
<div>
{donuts.map((donut, index) => (
<div key={donut}>
<p>{donut}</p>
<button
onClick={() =>
donutStore.trigger.removeDonut({ donutIndex: index })
}
>
Remove {donut}
</button>
</div>
))}
</div>
<div>
<p>Total: {total}</p>
</div>
</div>
);
}

less
import { useStore, useSelector } from '@xstate/store/react';
export function DonutShop() {
const donutStore = useStore({
context: {
donuts: [] as Donut[],
},
emits: {
outOfStock: (_: { donut: Donut }) => {},
},
on: {
addDonut: (context, event: { donut: Donut }, enq) => {
const orderedDonuts = context.donuts.filter(
(d) => d === event.donut
).length;
if (donutInventory[event.donut].quantity > orderedDonuts) {
return {
donuts: [...context.donuts, event.donut],
};
} else {
enq.emit.outOfStock({ donut: event.donut });
}
},
removeDonut: (context, event: { donutIndex: number }) => {
// ...
},
},
});
useEffect(() => {
const sub = donutStore.on('outOfStock', ({ donut }) => {
alert(`Out of stock: ${donut}`);
});
return sub.unsubscribe;
}, []);
// ...
}
import { createStore } from '@xstate/store';
import { useStore, useSelector } from '@xstate/store/react';
export const donutStore = createStore({
context: {
donuts: [] as Donut[],
},
emits: {
outOfStock: (_: { donut: Donut }) => {},
},
on: {
addDonut: (context, event: { donut: Donut }, enq) => {
// ...
},
removeDonut: (context, event: { donutIndex: number }) => {
// ...
},
},
});
export function DonutShop() {
const donuts = useSelector(donutStore, state => state.context.donuts);
useEffect(() => {
const sub = donutStore.on('outOfStock', ({ donut }) => {
alert(`Out of stock: ${donut}`);
});
return sub.unsubscribe;
}, []);
// ...
}
Store
npm i @xstate/store
Effect management
is state management.
(state, event) => (nextState, )
effects
(state, event) => nextState
🇺🇸 State transition
🇫🇷 coup d'État
Third-party libraries
useState
- Use external stores for shared/global state
- Use selectors to prevent rerenders
- Make declarative effects whenever possible
Local-first state
useState

Fetch
data

Sync
data
import { useShape } from '@electric-sql/react'
const MyComponent = () => {
const { isLoading, data } = useShape<{title: string}>({
url: `http://localhost:3000/v1/shape`,
params: {
table: 'items'
}
})
if (isLoading) {
return <div>Loading ...</div>
}
return (
<div>
{data.map(item => <div>{item.title}</div>)}
</div>
)
}




LiveStore
Local-first state
useState
- Use local-first state solutions (sync engines)
for offline-capable state & DB syncing - Use sync engines for realtime updates
useState
-
Use it for truly
component-internal UI state
- Ask yourself:
is there a better pattern?
Not all state is UI state!
There usually is, especially with React 19!
-
useState -
useRef
-
useReducer
-
useContext
-
useTransition
-
useActionState
-
useOptimistic
-
useSyncExternalStore
- use RSCs
- use query libraries
- use 3rd-party stores
- use local-first sync state
useState
Start with
Refactor to
better patterns
Focus on maintenance:
simple > easy
Merci React Paris!

David Khourshid · @davidkpiano
stately.ai
Goodbye, useState MN
By David Khourshid
Goodbye, useState MN
- 73