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

https://xstate.tips/ (coming soon)

KIITOS ❤️