Farzad Yousefzadeh
Lead developer @epicgames. Core @statelyai. Coach @mentorcruise. ❤️ Statecharts, Finite State Machines, and XState.
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
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
}
on account select, fetch its emails list
on email select, fetch its thread
{
...state,
selectedAccount: Account;
selectedEmail: Email;
}
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
...
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
...
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
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
}
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
...
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"
...
By Farzad Yousefzadeh
This is a talk about how client side application's state is inherently relational and hierarchical. Current tooling in JS state management allows for flat model states and global actions availability which results in an increase in cyclomatic complexity. The flat shape of state also brings its own set of integration problems. these issues could be tackled using a modeling language that considers this relation, gives lifecycle to the parts of state and invalidates automatically, is deterministic and whitelists allowed transitions instead of blacklisting on runtime guards.
Lead developer @epicgames. Core @statelyai. Coach @mentorcruise. ❤️ Statecharts, Finite State Machines, and XState.