The two ways

David Khourshid β†’ @davidkpiano

stately.aiΒ 

to manage state

JSHeroes 2023

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: 'Buna ziua',
  from: self
});

// ...

switch (event.type) {
  case 'greet':
    message.from.send({
      type: 'greet',
      value: '...',
      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'

Demo
(please work conference wifi πŸ™)

Play this if the demo fails

v5 beta

yarn add xstate@beta

v5 beta

createMachine({
  initial: 'asleep',
  states: {
    asleep: {
      on: {
        espresso: 'awake'
      }
    },
    awake: {/* ... */}
  }
});

State machines

fromTransition((state, event) => {
  // ...
  return nextState;
}, initialState);

Transition functions

fromPromise(async () => {
  const data = await getData();
  return data;
});

Promises

import { createMachine, interpret, assign } from 'xstate';

const counterMachine = createMachine({
  context: { count: 0 },
  on: {
    inc: {
      actions: assign({ count: ({ context }) => context.count + 1 })
    },
    dec: {
      actions: assign({ count: ({ context }) => context.count - 1 })
    }
  }
});

const counterActor = interpret(counterMachine).start();

counterActor.send({ type: 'inc' });
counterActor.send({ type: 'inc' });
counterActor.send({ type: 'dec' });

counterActor.getSnapshot().context.count;
// => 1
  • 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

UI = fn(state)

[state, effects] =

fn(prevState, event)

UI framework

State management

Independence can be achieved
with any state management solution

Two ways to manage state:
Easy vs. simple

Which will you choose?

MulΘ›umesc JSHeroes!

Resources

JSHeroes 2023

David Khourshid β†’ @davidkpiano
stately.ai