{"id":13727171,"url":"https://github.com/cevr/xsfp","last_synced_at":"2025-05-07T22:30:53.110Z","repository":{"id":57402267,"uuid":"295288876","full_name":"cevr/xsfp","owner":"cevr","description":"A functional API to create xstate machines ","archived":true,"fork":false,"pushed_at":"2020-09-21T21:44:21.000Z","size":342,"stargazers_count":29,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-25T16:47:30.974Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cevr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-09-14T03:01:48.000Z","updated_at":"2024-11-05T00:32:08.000Z","dependencies_parsed_at":"2022-09-17T06:42:44.404Z","dependency_job_id":null,"html_url":"https://github.com/cevr/xsfp","commit_stats":null,"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Fxsfp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Fxsfp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Fxsfp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cevr%2Fxsfp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cevr","download_url":"https://codeload.github.com/cevr/xsfp/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252965219,"owners_count":21832840,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T01:03:42.607Z","updated_at":"2025-05-07T22:30:52.531Z","avatar_url":"https://github.com/cevr.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"An API to use [xstate](https://xstate.js.org/docs/) in a composable way!\n\n[View API](#API)\n\n## Installation\n\n```bash\nyarn add xstate xsfp\n```\n\n```bash\nnpm install xstate xsfp\n```\n\n## Overview\n\n```js\nimport { interpret } from 'xstate';\nimport * as x from 'xsfp';\n\n// Stateless machine definition\n// machine.transition(...) is a pure function used by the interpreter.\n\n// Original way\n// const toggleMachine = createMachine({\n//   id: 'toggle',\n//   initial: 'inactive',\n//   states: {\n//     inactive: { on: { TOGGLE: 'active' } },\n//     active: { on: { TOGGLE: 'inactive' } },\n//   },\n// });\n\n// xsfp way\nconst toggleEvent = targetName =\u003e x.on('TOGGLE', targetName);\nconst toggleState = (stateName, targetName) =\u003e\n  x.state(stateName, toggleEvent(targetName));\n\nconst toggleMachine = x.createMachine(\n  x.id('toggle'),\n  x.states(\n    // first state is implicitly the initial state\n    x.state('inactive', x.on('TOGGLE', 'active')),\n    x.state('active', x.on('TOGGLE', 'inactive')),\n    // composed! both are equivalent\n    toggleState('inactive', 'active'),\n    toggleState('active', 'inactive')\n  )\n);\n\n// Machine instance with internal state\nconst toggleService = interpret(toggleMachine)\n  .onTransition(state =\u003e console.log(state.value))\n  .start();\n// =\u003e 'inactive'\n\ntoggleService.send('TOGGLE');\n// =\u003e 'active'\n\ntoggleService.send('TOGGLE');\n// =\u003e 'inactive'\n```\n\n## Promise example\n\n```js\nimport { interpret } from 'xstate';\nimport * as x from 'xsfp';\n\nconst fetchTransition = x.on('FETCH', 'loading');\n\nconst fetchMachine = x.createMachine(\n  x.id('SWAPI'),\n  x.context({ user: null }),\n  x.states(\n    x.initialState('idle', fetchTransition),\n    x.state(\n      'loading',\n      x.invoke(\n        (context, event) =\u003e\n          fetch('https://swapi.dev/api/people/1').then(res =\u003e res.data),\n        x.id('fetchLuke'),\n        x.onDone('resolved', x.assign({ user: (_, event) =\u003e event.data })),\n        x.onError('rejected')\n      ),\n      x.on('CANCEL', 'idle')\n    ),\n    x.state('rejected', fetchTransition),\n    x.finalState('resolved')\n  )\n);\n\nconst swService = interpret(fetchMachine)\n  .onTransition(state =\u003e console.log(state.value))\n  .start();\n\nswService.send('FETCH');\n```\n\n## Finite State Machines\n\n```js\nimport * as x from 'xsfp';\n\nconst timerTransition = targetState =\u003e x.on('TIMER', targetState);\nconst timerState = (stateName, targetState) =\u003e\n  x.state(stateName, timerTransition(targetState));\n\nconst lightMachine = x.createMachine(\n  x.id('light'),\n  x.states(\n    timerState('green', 'yellow'),\n    timerState('yellow', 'red'),\n    timerState('red', 'green')\n  )\n);\n\nconst currentState = 'green';\n\nconst nextState = lightMachine.transition(currentState, 'TIMER').value;\n\n// =\u003e 'yellow'\n```\n\n## Hierarchical (Nested) State Machines\n\n```js\nimport * as x from 'xsfp';\n\nconst pedTimerTransition = targetState =\u003e x.on('PED_TIEMR', targetState);\n\nconst pedestrianStates = x.states(\n  x.state('walk', pedTimerTransition('wait')),\n  x.state('wait', pedTimerTransition('stop')),\n  x.state('stop')\n);\n\nconst timerTransition = targetState =\u003e x.on('TIMER', targetState);\n\nconst lightMachine = x.createMachine(\n  x.id('light'),\n  x.state('green', timerTransition('yellow')),\n  x.state('yellow', timerTransition('red')),\n  x.state('red', timerTransition('green'), pedestrianStates)\n);\n\nconst currentState = 'yellow';\n\nconst nextState = lightMachine.transition(currentState, 'TIMER').value;\n// =\u003e {\n//   red: 'walk'\n// }\n\nlightMachine.transition('red.walk', 'PED_TIMER').value;\n// =\u003e {\n//   red: 'wait'\n// }\n```\n\n## Parallel State Machines\n\n```js\nconst toggleStates = (toggleEvent: string) =\u003e\n  x.states(\n    x.state('on', x.on(toggleEvent, 'off')),\n    x.state('off', x.on(toggleEvent, 'on'))\n  );\n\nconst wordMachine = x.createMachine(\n  x.id('word'),\n  x.parallelStates(\n    x.state('bold', toggleStates('TOGGLE_BOLD')),\n    x.state('underline', toggleStates('TOGGLE_UNDERLINE')),\n    x.state('italics', toggleStates('TOGGLE_ITALICS')),\n    x.state(\n      'list',\n      x.states(\n        x.state('none', x.on('BULLETS', 'bullets'), x.on('NUMBERS', 'numbers')),\n        x.state('bullets', x.on('NONE', 'none'), x.on('NUMBERS', 'numbers')),\n        x.state('numbers', x.on('BULLETS', 'bullets'), x.on('NONE', 'none'))\n      )\n    )\n  )\n);\n\nconst boldState = wordMachine.transition('bold.off', 'TOGGLE_BOLD').value;\n\n// {\n//   bold: 'on',\n//   italics: 'off',\n//   underline: 'off',\n//   list: 'none'\n// }\n\nconst nextState = wordMachine.transition(\n  {\n    bold: 'off',\n    italics: 'off',\n    underline: 'on',\n    list: 'bullets',\n  },\n  'TOGGLE_ITALICS'\n).value;\n\n// {\n//   bold: 'off',\n//   italics: 'on',\n//   underline: 'on',\n//   list: 'bullets'\n// }\n```\n\n## History States\n\n```js\nconst paymentMachine = x.createMachine(\n  x.id('payment'),\n  x.states(\n    x.state(\n      'method',\n      x.on('NEXT', 'review'),\n      x.state('cash', x.on('SWITCH_CHECK', 'check')),\n      x.state('check', x.on('SWITCH_CASH', 'cash')),\n      x.historyState('hist')\n    ),\n    x.state('review', x.on('PREVIOUS', 'method.hist'))\n  )\n);\n\nconst checkState = paymentMachine.transition('method.cash', 'SWITCH_CHECK');\n\n// =\u003e State {\n//   value: { method: 'check' },\n//   history: State { ... }\n// }\n\nconst reviewState = paymentMachine.transition(checkState, 'NEXT');\n\n// =\u003e State {\n//   value: 'review',\n//   history: State { ... }\n// }\n\nconst previousState = paymentMachine.transition(reviewState, 'PREVIOUS').value;\n\n// =\u003e { method: 'check' }\n```\n\n## Contribution\n\nPlease feel free to make issues and PRs!\n\n## API\n\nThe API will not go into too much details, as the library expects the user to have an understanding of how [xstate](https://xstate.js.org/docs/) works.\n\n### states | parallelStates\n\n`states` takes `state` | `initialState` | `finalState` | `historyState` as arguments.\n\n`parallelStates` takes `state` | `finalState` | `historyState` as arguments.\n\n`states` also accepts a string shorthand. Useful for nested states with no transitions\n\n```js\nstates('clean', 'error', 'success');\n```\n\nThe `initialState` function OR the first `state` argument determines the initial state\n\n```js\nstates(\n  initialState('initial'),\n  state('second'),\n  historyState('hist'),\n  finalState('final')\n);\n```\n\n### state | initialState | historyState | finalState\n\n`state` | `initialState` is a function expects that all the same arguments as `createMachine`.\n\n```ts\nstate(\n  'name',\n  states(),\n  parallelStates(),\n  id(),\n  context(),\n  history(),\n  on(),\n  invoke(),\n  entry(),\n  exit(),\n  after(),\n  always(),\n  activties(),\n  meta(),\n  data(),\n  delimiter()\n);\n\nfunction finalState(stateName: string);\nfunction historyState(\n  stateName: string,\n  type?: 'shallow' | 'deep' = 'shallow',\n  target?: string\n);\n```\n\n### on\n\n`on` is used to describe events and its transitions\n\n```ts\nfunction on(event: string, ...Transition);\n```\n\nA `guard` acts as the condition that determines whether the transition described before it will run\n\n```js\non('CLICK', 'open');\n\n// both below are equivalent\non(\n  'CLICK',\n  'open',\n  effect((context, event) =\u003e {\n    context.refs[event.name]?.open();\n  })\n);\non(\n  'CLICK',\n  transition(\n    'open',\n    effect((context, event) =\u003e {\n      context.refs[event.name]?.open();\n    })\n  )\n);\n\n// both are equiavalent\non(\n  'CLICK',\n  'open',\n  effect((context, event) =\u003e {\n    context.refs[event.name]?.open();\n  }),\n  guard((context, event) =\u003e {\n    // if false, the effect before will not run\n    return context.canOpen;\n  })\n);\n\non(\n  'CLICK',\n  transition(\n    'open',\n    effect((context, event) =\u003e {\n      context.refs[event.name]?.open();\n    }),\n    guard((context, event) =\u003e {\n      // if false, the effect before will not run\n      return context.canOpen;\n    })\n  )\n);\n```\n\nA `guard` will also implicitly act as boundaries between different transitions\n\n```js\non(\n  'BLUR',\n  'error',\n  guard((context, event) =\u003e !event.value),\n  // this will run only if the guard above is false\n  'idle',\n  assign({ value: (context, event) =\u003e event.value })\n);\n\n// transitions make this explicit\non(\n  'BLUR',\n  transition(\n    'error',\n    guard((context, event) =\u003e !event.value)\n  ),\n  // this will run only if the guard above is false\n  transition('idle', assign({ value: (context, event) =\u003e event.value }))\n);\n```\n\n### transition\n\n`transition` is a function that describes an event transition. If a target is specified, it must be the first argument.\n\nIt accepts the target state and `assign` | `effect` | `action` | `choose` | `guard` as arguments.\n\nIf a `guard` is specified it must be the last argument.\n\n```js\ntransition('idle');\n\ntransition('idle', assign({ value: (context, event) =\u003e event.value }));\n\ntransition(\n  action('setValue'),\n  effect((context, event) =\u003e {\n    context.refs[event.name]?.focus();\n  }),\n  guard((context, event) =\u003e Boolean(event.value))\n);\n```\n\n### action\n\n`action` is a function that accepts an action config key as the argument\n\n```js\naction('setValue');\n```\n\n### effect\n\n`effect` is a function that accepts an action function as the argument\n\nAlso accepts a cleanup function that will be called within `activities`\n\n```ts\nfunction effect\u003cTContext, TEvent\u003e((context: TContext, event: TEvent) =\u003e void | (() =\u003e void))\n\neffect((context, event) =\u003e {\n  context.refs[event.name]?.focus();\n});\n\nactivities(effect((context) =\u003e {\n  const intervalId = setInterval(() =\u003e {\n    // ...\n  }, context.timeout)\n  return () = {\n    clearInterval(intervalId)\n  }\n}))\n```\n\n### guard\n\n`guard` is a function that returns a boolean. To be used within `on` or as the last argument of `transition`\n\n```ts\nfunction guard\u003cTContext, TEvent\u003e((context: TContext, event: TEvent) =\u003e boolean)\n\nguard(context, event =\u003e Boolean(context.values[event.name]))\n```\n\n### entry | exit | always\n\n`entry` | `exit` | `always` are functions that expect the same arguments as `on` (minus the event name)\n\n```ts\nentry(\n  'error',\n  guard((context, event) =\u003e !event.value),\n  // this will run only if the guard above is false\n  'idle',\n  assign({ value: (context, event) =\u003e event.value })\n);\n\nexit(\n  'error',\n  guard((context, event) =\u003e !event.value),\n  // this will run only if the guard above is false\n  'idle',\n  assign({ value: (context, event) =\u003e event.value })\n);\n\nalways(\n  'error',\n  guard((context, event) =\u003e !event.value),\n  // this will run only if the guard above is false\n  'idle',\n  assign({ value: (context, event) =\u003e event.value })\n);\n```\n\n### choose\n\n`choose` accepts the same arguments as `transition` (except for a target state)\n`Guard` acts as action boundaries as well\n\n```js\nx.choose(\n  x.action('onSave'),\n  x.guard(context =\u003e Boolean(context.value)),\n  // will do action below if guard is false\n  x.assign({\n  value: (context) =\u003e context.initialValue,\n});\n);\n```\n\n### choice\n\n`choice` accepts the same arguments as `transition` (except for a target state).\nUseful for explicitly setting boundaries\n(analogous to `transition` within `on`)\n\n```js\nx.choose(\n  x.choice(\n    x.action('onSave'),\n    x.guard(context =\u003e Boolean(context.value))\n  ),\n  // will do action below if guard is false\n  x.choice(\n    x.assign({\n      value: context =\u003e context.initialValue,\n    })\n  )\n);\n```\n\n### after\n\n`after` is a function that takes accepts `delay` arguments\n\n```ts\nafter(\n  2000,\n  effect((context, event) =\u003e {\n    context.refs[event.name]?.focus();\n  })\n);\n\nafter(\n  (context, event) =\u003e {\n    context.values[event.name] ? 2000 : 3000;\n  },\n  effect((context, event) =\u003e {\n    context.refs[event.name]?.focus();\n  })\n);\n```\n\n### delay\n\n```ts\nfunction delay\u003cTContext, TEvent\u003e(\n  delay: number | ((context: TContext, event: TEvent) =\u003e number),\n  ...Effects\n);\n```\n\n### invoke\n\n`invoke` accepts the `src` type as the first argument, and `onDone` | `onError` | `id` | `data` | `autoForward`\n\n```js\nx.invoke(\n  (context, event) =\u003e\n    fetch('https://swapi.dev/api/people/1').then(res =\u003e res.data),\n  x.id('fetchLuke'),\n  x.onDone('resolved', x.assign({ user: (_, event) =\u003e event.data })),\n  x.onError('rejected')\n);\n```\n\n### id\n\n`id` is a function that takes a `string`\n\n```ts\nfunction id(id: string);\n```\n\n### context\n\n`context` is a function that takes the context of the machine or a function that returns the context of the machine\n\n```ts\nfunction context\u003cTContext\u003e(context: TContext | () =\u003e TContext)\n```\n\n### history\n\n`history` is a function that takes the history types\n\n```ts\nfunction history(type: 'shallow' | 'deep' | 'none');\n```\n\n### merge\n\n`merge` accepts `on` OR `assign` | `action` | `effect` and merges them\n\n```js\nconst reset = merge(\n  assign({\n    value: '',\n  }),\n  assign({\n    open: false,\n  })\n  // ignores function assignment\n  assign((context) =\u003e ({\n    open: !context.open\n  }))\n);\n\nconst resetTransitions = merge(on('reset', reset), on('cancel', reset));\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcevr%2Fxsfp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcevr%2Fxsfp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcevr%2Fxsfp/lists"}