timsy

Agnostic functional state machine with epic type support

Using Timsy with React

Make state explicit

import { createStates, States, match } from "timsy"

const [states] = createStates({
  NOT_LOADED: () => ({}),
  LOADING: () => ({}),
  LOADED: (data: Data) => ({ data }),
  ERROR: (error: Error) => ({ error }),
})

type DataState = States<typeof states>

const DataComponent: React.FC = () => {
  const [state, setState] = useState<DataState>(states.NOT_LOADED())

  return (
    <div>
      {match(state, {
        NOT_LOADED: () => (
          <button
            onClick={() => {
              fetch("/data")
                .then((response) => response.json())
                .then((data) => setState(states.LOADED(data)))
                .catch((error) => setState(states.ERROR(error)))
            }}>
            Load Data
          </button>
        ),
        LOADING: () => "Loading...",
        LOADED: ({ data }) => JSON.stringify(data),
        ERROR: ({ error }) => `ops, ${error.message}`,
      })}
    </div>
  )
}

Make state predictable

import { createStates, useMachine, match } from "timsy"

const [states, createMachine] = createStates({
  NOT_LOADED: () => ({}),
  LOADING: () => ({}),
  LOADED: (data: Data) => ({ data }),
  ERROR: (error: Error) => ({ error }),
})

const dataMachine = createMachine({
  NOT_LOADED: {
    load: () => () => states.LOADING(),
  },
  LOADING: {
    loadSuccess: (data: Data) => () => states.LOADED(data),
    loadError: (error: Error) => () => states.ERROR(error),
  },
  LOADED: {},
  ERROR: {},
})

const DataComponent: React.FC = () => {
  const [state, events, useTransitionEffect] = useMachine(() =>
    dataMachine(states.NOT_LOADED())
  )

  useTransitionEffect("LOADING", () => {
    fetch("/data")
      .then((response) => response.json())
      .then(events.loadSuccess)
      .catch(events.loadError)
  })

  return (
    <div>
      {match(state, {
        NOT_LOADED: () => (
          <button
            onClick={() => {
              events.load()
            }}>
            Load Data
          </button>
        ),
        LOADING: () => "Loading...",
        LOADED: ({ data }) => JSON.stringify(data),
        ERROR: ({ error }) => `ops, ${error.message}`,
      })}
    </div>
  )
}

Make promises safe

usePromise is a state machine handling any promise.

  • Explicit promise state
  • No over fetching
  • No expired component lifecycle setting state
  • No double render in React dev mode
  • Consume directly or subscribe to transitions

import { usePromise, match } from "timsy"

const DataComponent: React.FC = () => {
  const [state, load, useTransitionEffect] = usePromise(() =>
    fetch("/data").then((response) => response.json())
  )

  useTransitionEffect("RESOLVED", ({ value }) => {
    // Pass resolved data into other state stores or react
    // to transitions
  })

  return (
    <div>
      {match(state, {
        IDLE: () => (
          <button
            onClick={() => {
              load()
            }}>
            Load Data
          </button>
        ),
        PENDING: () => "Loading...",
        RESOLVED: ({ value }) => JSON.stringify(value),
        REJECTED: ({ error }) => `ops, ${error.message}`,
      })}
    </div>
  )
}

Core API

import { createStates } from "timsy"

const [states, createMachine] = createStates({
  FOO: () => ({}),
  BAR: () => ({}),
})

const runMachine = createMachine({
  FOO: {
    switch: () => () => states.BAR(),
  },
  BAR: {
    switch: () => () => states.FOO(),
  },
})

const machine = runMachine(states.FOO())

machine.events.switch()

const currentState = machine.getState()

const dispose = machine.subscribe((state, event, prevState) => {
  // Any change
})

const dispose = machine.subscribe("FOO", (state) => {
  // When entering state
  return () => {
    // When exiting state
  }
})

const dispose = machine.subscribe(["FOO", "BAR"], (state) => {
  // When first entering either state
  return () => {
    // When exiting to other state
  }
})

const dispose = machine.subscribe(
  "FOO",
  "switch",
  (state, eventParams) => {
    // When entering state by event
  }
)

const dispose = machine.subscribe(
  ["FOO", "BAR"],
  "switch",
  (state, eventParams) => {
    // When entering either state by event
  }
)

const dispose = machine.subscribe(
  "FOO",
  "switch",
  "BAR",
  (state, eventParams, prevState) => {
    // When entering state by event from state
  }
)

const dispose = machine.subscribe(
  ["FOO", "BAR"],
  "switch",
  "BAZ"
  (state, eventParams, prevState) => {
    // When entering either state by event from state
  }
)

Publish

yarn build
cd dist
# If you need to double-check what will be in the final publish package
npx npm-packlist
# publish via np or npm itself
npm publish

GitHub

View Github