David Khourshid → @davidkpiano

stately.ai 

State management
with

Managing state
is difficult.

function Conference() {
  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('');

  // ...
}
const [count, setCount] = useState(0);
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');

let count = 0;

incButton.addEventListener('click', () => {
  count++;
  counter.textContent = count.toString();
}

decButton.addEventListener('click', () => {
  count--;
  counter.textContent = count.toString();
}
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');

let count = 0;

incButton.addEventListener('click', () => {
  if (count < 10) {
    count++;
    counter.textContent = count.toString();
  }
});

decButton.addEventListener('click', () => {
  if (count > 0) {
    count--;
    counter.textContent = count.toString();
  }
});
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');

let count = 0;

incButton.addEventListener('click', () => {
  if (count < 10) {
    count++;
    counter.textContent = count.toString();
  }
});

decButton.addEventListener('click', () => {
  if (count > 0) {
    count--;
    counter.textContent = count.toString();
  }
});

counter.addEventListener('keyup', (event) => {
  if (event.key === 'ArrowUp' && count < 10) {
    count++;
    counter.textContent = count.toString();
  } else if (event.key === 'ArrowDown' && count > 0) {
    count--;
    counter.textContent = count.toString();
  }
});

counter.addEventListener('keydown', (event) => {
  if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
    event.preventDefault();
  }
});
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');

let count = 0;

function incrementCount() {
  if (count < 10) {
    count++;
    counter.textContent = count.toString();
  }
}

function decrementCount() {
  if (count > 0) {
    count--;
    counter.textContent = count.toString();
  }
}

incButton.addEventListener('click', incrementCount);

decButton.addEventListener('click', decrementCount);

counter.addEventListener('keyup', (event) => {
  if (event.key === 'ArrowUp') {
    incrementCount();
  } else if (event.key === 'ArrowDown') {
    decrementCount();
  }
});

counter.addEventListener('keydown', (event) => {
  if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
    event.preventDefault();
  }
});
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');
const output = document.querySelector('#output');

function createObservable(initialValue) {
  let value = initialValue;
  const listeners = new Set();

  return {
    get() {
      return value;
    },
    set(newValue) {
      value = newValue;
      listeners.forEach(listener => listener(value));
    },
    subscribe(listener) {
      listeners.add(listener);
      return { unsubscribe: () => listeners.delete(listener) }
    },
  };
}

const countObservable = createObservable(0);

incButton.addEventListener('click', () => {
  if (count < 10) {
    countObservable.set(count + 1);
  }
});

decButton.addEventListener('click', () => {
  if (count > 0) {
    countObservable.set(count - 1);
  }
});

counter.addEventListener('keyup', (event) => {
  if (event.key === 'ArrowUp') {
    countObservable.set(count + 1);
  } else if (event.key === 'ArrowDown') {
    countObservable.set(count - 1);
  }
});

counter.addEventListener('keydown', (event) => {
  if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
    event.preventDefault();
  }
});

countObservable.subscribe(count => {
  counter.textContent = count;
  output.textContent = count;
});
const counter = document.querySelector('#counter');
const incButton = document.querySelector('#inc');
const decButton = document.querySelector('#dec');
const output = document.querySelector('#output');

function createObservable(transitionFn, initialValue) {
  let value = initialValue;
  const listeners = new Set();

  return {
    get() {
      return value;
    },
    send(event) {
      value = transitionFn(value, event);
      listeners.forEach((listener) => listener(value));
    },
    subscribe(listener) {
      listeners.add(listener);
      return { unsubscribe: () => listeners.delete(listener) }
    },
  };
}

const countObservable = createObservable((count, event) => {
  switch (event.type) {
    case 'inc':
      return Math.min(10, count + 1);
    case 'dec':
      return Math.max(0, count - 1);
    default:
      return count;
  }
}, 0);

incButton.addEventListener('click', () => {
  countObservable.send({ type: 'inc' });
});

decButton.addEventListener('click', () => {
  countObservable.send({ type: 'dec' });
});

counter.addEventListener('keyup', (event) => {
  if (event.key === 'ArrowUp') {
    countObservable.send({ type: 'inc' });
  } else if (event.key === 'ArrowDown') {
    countObservable.send({ type: 'dec' });
  }
});

counter.addEventListener('keydown', (event) => {
  if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
    event.preventDefault();
  }
});

countObservable.subscribe((count) => {
  counter.textContent = count;
  output.textContent = count;
});

You just
reinvented

🎉

I don't need a
state management library.

I don't need a
state management library.

I should have used a
state management library.

Mutable state is fast,
immutable state is maintainable.

let count = 0;
count++;
const count = 0;
const nextCount = count + 1;

Effect management
is state management.

(state, event) => (nextState,        )
effects
(state, event) => nextState
switch (state) {
  case 'mini':
    if (event.type === 'toggle') {
      playVideo();
      return 'full';
    }
    break;
  case 'full':
    if (event.type === 'toggle') {
      pauseVideo();
      return 'mini';
    }
    break;
  default:
    break;
}
  • Recoil
  • Valtio
  • MobX
  • Jotai
  • XState

Multi-store

  • Redux
  • Zustand
  • Vuex
  • Pinia

Single-store

Single-store is convenient,
multi-store is realistic.

👩‍💻

🧔

👩‍🍳

☕️❔

📝

💶❓

💶

📄✅

☕️

☕️

👩‍💻

🧔

I would like a coffee...

What would you like?

 Actor 

I would like a coffee, please.

 Actor 

💭

💬

💬

Here you go. ☕️

Thanks!

The actor model

someActor.send({
  type: 'greet',
  value: 'Buenos',
  from: self
});

// ...

switch (event.type) {
  case 'greet':
    message.from.send({
      type: 'greet',
      value: 'Que tal',
      from: self
    });
  // ...
}

All computation is performed
within an actor

Actors can communicate
only through messages

In response to a message,
an actor can:

Change its state/behavior

In response to a message,
an actor can:

Change its state/behavior

Send messages to
other actors

In response to a message,
an actor can:

Change its state/behavior

Send messages to
other actors

Spawn new actors

Actor logic

{
  orders: [/* ... */],
  inventory: {
    // ...
  },
  sales: 134.65
}

Finite state (behavior)

Extended state (contextual data)

The actor model is a great way
to model application logic.

Direct state management is easy,
indirect state management is simple.

Indirect

send(event)

Event-based

Direct

setState(value)

Value-based

// Read state
actor.subscribe((state) => {
  console.log(state);
});

// Update state (indirectly)
actor.send({ type: 'inc' });

Everything is a
graph

Everything is a
graph

  • Make API call to auth provider
  • Initiate OAuth flow
  • Ensure token is valid
  • Persist token as cookie
  • Redirect to logged in view

Given a user is logged out,

When the user logs in with correct credentials

Then the user should be logged in

function transition(state, event) {
  switch (state.value) {
    case 'cart':
      if (event.type === 'CHECKOUT') {
        return { value: 'shipping' };
      }
      return state;

    case 'shipping':
      // ...

    default:
      return state;
  }
}

State machines

with switch statements

State

Event

const machine = {
  initial: 'cart',
  states: {
    cart: {
      on: {
        CHECKOUT: 'shipping'
      }
    },
    shipping: {
      on: {
        NEXT: 'contact'
      }
    },
    contact: {
      // ...
    },
    // ...
  }
}

State machines

with object lookup

State machines

with object lookup

function transition(state, event) {
  const nextState = machine
    .states[state]
    .on?.[event.type]
    ?? state;
}

transition('cart', { type: 'CHECKOUT' });
// => 'shipping'

transition('cart', { type: 'UNKNOWN' });
// => 'cart'
import { createMachine } from "xstate";

export const machine = createMachine({
  initial: 'cart',
  states: {
    cart: {
      on: {
        CHECKOUT: { target: 'shipping' }
      }
    },
    shipping: {
      on: {
        NEXT: { target: 'contact' }
      }
    },
    contact: {
      // ...
    },
    // ...
  }
})
import { createActor } from 'xstate';
import { machine } from './ghostMachine';

const actor = createActor(machine);

actor.subscribe(state => {
  console.log(state);
});

actor.start();
// { value: 'checkout', ... }

actor.send({ type: 'CHECKOUT' });
// { value: 'shipping', ... }

cart

shipping

contact

payment

confirmation

CHECKOUT

NEXT

NEXT

ORDER

PAYPAL

BACK

BACK

CANCEL

Identify
logical flaws

cart

shipping

contact

payment

confirmation

CHECKOUT

NEXT

NEXT

ORDER

PAYPAL

As a user, when I'm in the cart and I click the checkout button, I should be on the shipping page.

cart

shipping

contact

payment

confirmation

CHECKOUT

NEXT

NEXT

ORDER

PAYPAL

As a user, when I'm in the cart and I checkout via PayPal, I should be taken directly to the payment screen.

cart

shipping

contact

payment

confirmation

CHECKOUT

NEXT

NEXT

ORDER

PAYPAL

Shortest path

confirmation state

cart

shipping

contact

payment

confirmation

CHECKOUT

NEXT

NEXT

ORDER

PAYPAL

Shortest path

confirmation state where
shipping address is provided

  • Mutable vs. immutable

  • Effect management

  • Single vs. multi store

  • Direct vs. indirect
     

None of this matters (directly)

Correctness

Velocity

Maintenance

Bug-free

Intuitive UX

Accessible

No race conditions

Adheres to specifications

Verifiable logic

Adding features

Changing features

Removing features

Fixing bugs

Adjusting tests

Onboarding

Documentation

Understanding bug root causes

Performance

Testability

Stability at complexity scale

Which state management is
best for app logic?

The one that your entire team
can understand

Map requirements to code

1

Code independently of frameworks

2

Make interfaces simple (read, send)

3

Use a common visual language

4

Effective state management

Independence can be achieved
with any state management solution

Thank you!

Resources

David Khourshid → @davidkpiano
stately.ai

Stately and XState

By David Khourshid

Stately and XState

  • 326