Frontend Masters
Welcome to the workshop
Principle 1
Events are the most accurate representation of state
- Everything else is a useful but lossy abstraction
- Show: User clicked vs. button is disabled
Write code as if you're going to change frameworks
- Socket/port architecture keeps code organized
- Business logic separate from React-specific code
Pure functions are the easiest thing to test
- All app logic should be in pure functions
- Show: calculateTripTotal() example
Normalization is key; think twice before nesting
- Flat data structures vs. deeply nested objects
- Show: itinerary with nested destinations (bad) vs. normalized (good)
Think in state machines
- They exist whether explicit or not
- Show: booking flow implicit states vs. explicit state machine
-
-
Make side effects declarative
- Separate what happens from when it happens
- Show: imperative API call vs. declarative effect
Slide 8: How These Principles Connect
- Visual diagram showing how they reinforce each other
- Events → Pure Functions → State Machines → Declarative Effects
Reacting to input with state
https://react.dev/learn/reacting-to-input-with-state
Choosing the State Structure
https://react.dev/learn/choosing-the-state-structure
Sharing state between components
https://react.dev/learn/sharing-state-between-components
Extracting state logic into a reducer
https://react.dev/learn/extracting-state-logic-into-a-reducer
Setting up with reducer and context
https://react.dev/learn/scaling-up-with-reducer-and-context
Slide 9: The useState/useEffect Trap
- Common patterns that seem fine but break at scale
Slide 10-12: useState Limitations
- Race conditions in flight search
- State synchronization issues
- Multiple sources of truth
Slide 13-15: useEffect Pitfalls
- Dependency array confusion
- Cleanup function mistakes
- Thinking in lifecycle vs. synchronization
Slide 16-17: Live Demo Setup
- Introduce the travel app
- Show the "before" version with useState problems
State Modeling & Visualization (6-8 slides)
Slide 18: Thinking Before Coding
- Why we diagram first
- Different types of state (UI, server, form, URL)
Slide 19-21: Diagramming Techniques
- State diagrams for booking flow
- Sequence diagrams for user interactions
- ERDs for data relationships
State diagrams
Sequence diagrams
ERDs
Slide 22-24: From Diagrams to Code
- How state diagrams become TypeScript types
- Impossible states become impossible
Slide 25: Exercise Setup
- "Draw the hotel booking state machine"
Event-Driven Architecture (8-10 slides)
Slide 26: Events vs. Direct State Manipulation
- "What happened" vs. "Change this thing"
- Event sourcing concepts
Slide 27-29: Event Examples in Travel App
-
FlightSelected
vs.setSelectedFlight()
-
BookingFailed
vs.setError()
+setLoading(false)
-
CommentAdded
vs. multiple state updates
Slide 30-32: Benefits of Events
- Audit trail
- Easier debugging
- Decoupling components
Slide 33-35: Implementation Patterns
- Event types with TypeScript
- Event handlers as pure functions
- Connecting events to state changes
Alternative State Approaches (6-8 slides)
Slide 36: When to Use What
- Decision tree for state management approaches
Slide 37-40: The Alternatives
- URL state (nuqs) - shareable state
- Server Components - server-derived state
- useReducer - complex local state
- Global stores - shared application state
Slide 41-43: Live Coding Preview
- Converting search filters to URL state
- Comparing useState vs. useReducer for booking flow
Advanced Patterns (8-10 slides)
Slide 44-46: Server State Management
- Optimistic updates
- Error handling and rollback
- Synchronizing multiple sources
Slide 47-49: Performance Patterns
- State normalization for performance
- Render optimization
- When to memoize
Slide 50-52: Testing & Debugging
- Testing pure state functions
- State visualization tools
- Time-travel debugging
Slide 53: Maintenance & Scaling
- Adding features to existing state architecture
- Refactoring patterns
Workshop Exercises & Wrap-up (3-5 slides)
Slide 54-55: Practice Challenges
- Hands-on exercises for attendees
- Solutions and discussion
Slide 56-57: Key Takeaways
- The principles recap
- Resources for continued learning
Who am I?
Doesn't matter
We have too much to talk about
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
Frontend Masters State
By David Khourshid
Frontend Masters State
- 101