You think you know state management?

Farzad YousefZadeh
Formerly:
Aerospace engineer 🚀 astrophysicist 🌌
farzadyz.com
it's a sunny monday at work
Finland? sunny? nevermind

Let's break it down a bit

Accounts
Emails
Thread

Fetch accounts
Wait for it to arrive
Accounts could be empty
Accounts could arrive with an error
Send a network request
Show a spinner
Show empty text
Show an error
type State = {
accountsPending: boolean
accounts: Account[]
accountsError: any
}
Select first account automatically
Accounts could be a valid list
Show accounts list
Select account on click

Fetch emails for selected account
Wait for it to arrive
Email could be empty
Emails could arrive with an error
Send a network request
Show a spinner
Show empty text
Show an error
type State = {
emailsPending: boolean
emailsError: any
emails: Email[]
filteredEmails: Email[]
filterQuery: string
}
Select email on click
Emails could be a valid list
Show emails list
Search in emails and filter the list
Fetch email thread for selected email
Wait for it to arrive
Thread could arrive with an error
Send a network request
Show a spinner
Show an error
type State = {
threadPending: boolean
threadError: any
thread: EmailTree
}
Thread could be a valid list
Show thread tree

Easy? Perhaps 🤔
Let's put this all together
All In
type State = {
// Accounts
accountsPending: boolean
accounts: Account[]
accountsError: any
// Emails
emailsPending: boolean
emailsError: any
emails: Email[]
filteredEmails: Email[]
filterQuery: string
// Thread
threadPending: boolean
threadError: any
thread: EmailTree
}
Shape of my state
(by Sting)
on account select, fetch its emails list
on email select, fetch its thread
to put them all together
{
...state,
selectedAccount: Account;
selectedEmail: Email;
}
It works!
Eureka moment
It works!
but only for the happiest user who happens to take the happiest path into your application, doing everything right, on the right moment, waiting for all the network requests to finish, has a very reliable and fast network connection, never double-clicks, respects disabled buttons and isn't drunk 🥴


Opening the application
Opening the application
Getting to an email thread
Getting to an email thread
Integrations are nasty
type State = {
// Accounts
accountsPending: boolean
accounts: Account[]
accountsError: any
// Emails
emailsPending: boolean
emailsError: any
emails: Email[]
filteredEmails: Email[]
filterQuery: string
// Thread
threadPending: boolean
threadError: any
thread: EmailTree
selectedAccount: Account
selectedEmail: Email
}
i. Email and Thread actions make sense only when accounts are a valid list
ii. Thread actions make sense only when accounts and emails are both valid lists
iii. To avoid race conditions, you need to abort fetching emails and possible threads when selected account changes.
iv. To avoid race conditions, you need to abort fetching the thread when selected email changes.
v. On searching in emails, you need to reset the shown thread
type State = {
// Accounts
accountsPending: boolean
accounts: Account[]
accountsError: any
// Emails
emailsPending: boolean
emailsError: any
emails: Email[]
filteredEmails: Email[]
filterQuery: string
// Thread
threadPending: boolean
threadError: any
thread: EmailTree
selectedAccount: Account
selectedEmail: Email
}
vi. On selecting a new account, reset all state related to emails and threads
viii. Should you abort fetching thread when the search happens while the fetch is pending?
vi. On selecting a new email, reset all state related to threads
...
Integrations are nasty
New Features are closer than they appear
(by: Text rear view mirrors)
i. Cache fetched data
ii. Sync state partially into URL for usability and staring (conditional default selected account)
iii. Delete emails
v. Select several emails in the thread for bulk operations
iv. Reply to emails
...
State is relational!
type State = {
// Accounts
accountsPending: boolean
accounts: Account[]
accountsError: any
// Emails
emailsPending: boolean
emailsError: any
emails: Email[]
filteredEmails: Email[]
filterQuery: string
// Thread
threadPending: boolean
threadError: any
thread: EmailTree
selectedAccount: Account
selectedEmail: Email
}
Data is fetched in a waterfall
Relational data results in relational state
Emails are a descendant of accounts
Threads are a descendant of emails
Filtered emails are descendant of the selected email and the selected account
Global actions, Flat State
type State = {
// Accounts
accountsPending: boolean
accounts: Account[]
accountsError: any
// Emails
emailsPending: boolean
emailsError: any
emails: Email[]
filteredEmails: Email[]
filterQuery: string
// Thread
threadPending: boolean
threadError: any
thread: EmailTree
selectedAccount: Account
selectedEmail: Email
}
// Action
fetchThread(
accountId: string, emailId: string
) {
return {
type: 'FETCH_THREAD',
data: {accountId, emailId}
}
}
// Reducer
case 'FETCH_THREAD':
return {
...state,
threadPending: true
}
// Action
fetchThread(accountId: string, emailId: string) {
if (
currentState.emailPending
|| currentState.emails.length === 0
|| currentState.emailsError
) {
return;
}
return {
type: 'FETCH_THREAD',
data: {accountId, emailId}
}
}
// Reducer
case 'FETCH_THREAD':
return {
...state,
threadPending: true
}
It's like the bathroom situation
1 start fetching accounts, abort all pending requests
2 while fetching, stay here ❌
3 if error, stay here ❌
4 if successfull ✅
5 if empty, stay here ❌
6 if not empty ✅
7.1 if there is an account if in url, pick it and select that account else
7.2 if not, select first account
8 on a new account selected, start from (1)
9 start fetching emails, reset filter query and abort all thread requests and
start fetching thread for this email
10 while fetching emails, stay here ❌
11 if error, stay here ❌
12 if successfull ✅
13 if empty, stay here ❌
14 if not empty ✅
15 on a new email selected, start from (9)
16 if there is a search query, filter emails list and abort all thread requests
17 start fetching threads for the selected email
...
Only if code understood human language
Only if code understood human language
type State =
| "accounts_pending"
| "accounts_error"
| "accounts_success.accounts_empty"
| "accounts_success.accounts_ok"
| "accounts_success.accounts_ok.no_selected_email.not_filtered"
| "accounts_success.accounts_ok.no_selected_email.filtered"
| "accounts_success.accounts_ok.selected_email.not_filtered"
| "accounts_success.accounts_ok.selected_email.filtered"
...
We have lots of tools to
Store state (one store, several stores)
Mutate state (mutable, immutable, reactive)
Transmit store through the app (context, hooks, reactions)
But, none of them deals with hierarchical state
Statecharts

Reducer with rules
Deterministic (event, state)
Relational state
Reason based on state, not data
Generic concept, use it with anything

We use it at Epic Games
Hear my funny story
Why should I care?
What are disadvantages?
Get started here
