{"id":13661089,"url":"https://github.com/jeffijoe/mobx-task","last_synced_at":"2025-04-04T15:11:26.537Z","repository":{"id":17855889,"uuid":"82856657","full_name":"jeffijoe/mobx-task","owner":"jeffijoe","description":"Makes async function state management in MobX fun.","archived":false,"fork":false,"pushed_at":"2024-06-21T23:21:08.000Z","size":967,"stargazers_count":242,"open_issues_count":1,"forks_count":6,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-28T14:11:12.471Z","etag":null,"topics":["async-functions","decorators","mobx","mobx-task"],"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/jeffijoe.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-02-22T21:58:07.000Z","updated_at":"2025-03-14T09:13:25.000Z","dependencies_parsed_at":"2024-01-15T02:30:18.363Z","dependency_job_id":"a42a3bd0-1716-4e50-a2bb-02cbfb101fb4","html_url":"https://github.com/jeffijoe/mobx-task","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffijoe%2Fmobx-task","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffijoe%2Fmobx-task/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffijoe%2Fmobx-task/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffijoe%2Fmobx-task/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jeffijoe","download_url":"https://codeload.github.com/jeffijoe/mobx-task/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247198465,"owners_count":20900081,"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":["async-functions","decorators","mobx","mobx-task"],"created_at":"2024-08-02T05:01:29.594Z","updated_at":"2025-04-04T15:11:26.520Z","avatar_url":"https://github.com/jeffijoe.png","language":"TypeScript","funding_links":[],"categories":["TypeScript","Awesome MobX"],"sub_categories":["Related projects and utilities"],"readme":"# mobx-task\n\n[![npm](https://img.shields.io/npm/v/mobx-task.svg?maxAge=1000)](https://www.npmjs.com/package/mobx-task)\n[![CI](https://github.com/jeffijoe/mobx-task/actions/workflows/ci.yml/badge.svg)](https://github.com/jeffijoe/mobx-task/actions/workflows/ci.yml)\n[![Coveralls](https://img.shields.io/coveralls/jeffijoe/mobx-task.svg?maxAge=1000)](https://coveralls.io/github/jeffijoe/mobx-task)\n[![npm](https://img.shields.io/npm/dt/mobx-task.svg?maxAge=1000)](https://www.npmjs.com/package/mobx-task)\n[![npm](https://img.shields.io/npm/l/mobx-task.svg?maxAge=1000)](https://github.com/jeffijoe/mobx-task/blob/master/LICENSE.md)\n\nTakes the suck out of managing state for async functions in MobX.\n\nTable of Contents\n=================\n\n- [mobx-task](#mobx-task)\n- [Table of Contents](#table-of-contents)\n- [Installation](#installation)\n- [What is it?](#what-is-it)\n- [Full example with classes and decorators](#full-example-with-classes-and-decorators)\n- [Full example with plain observables](#full-example-with-plain-observables)\n- [How does it work?](#how-does-it-work)\n- [Task Groups](#task-groups)\n- [API documentation](#api-documentation)\n  - [The `task` factory](#the-task-factory)\n  - [As a decorator](#as-a-decorator)\n  - [The `task` itself](#the-task-itself)\n    - [`state`](#state)\n    - [`pending`, `resolved`, `rejected`](#pending-resolved-rejected)\n    - [`result`](#result)\n    - [`args`](#args)\n    - [`error`](#error)\n    - [`match()`](#match)\n    - [`wrap()`](#wrap)\n    - [`setState()`](#setstate)\n    - [`bind()`](#bind)\n    - [`reset()`](#reset)\n  - [`TaskGroup`](#taskgroup)\n- [Gotchas](#gotchas)\n  - [Wrapping the task function](#wrapping-the-task-function)\n  - [Using the decorator on React Components](#using-the-decorator-on-react-components)\n  - [Using the decorator with `autobind-decorator`](#using-the-decorator-with-autobind-decorator)\n  - [Using with `typescript`](#using-with-typescript)\n- [Author](#author)\n\n# Installation\n\n```\nnpm install --save mobx-task\n```\n\n# What is it?\n\n`mobx-task` removes the boilerplate of maintaining loading and error state of async functions in MobX.\n\n**Your code before:**\n\n```js\nclass TodoStore {\n  @observable fetchTodosRunning = true\n  @observable fetchTodosError\n\n  async fetchTodos () {\n    try {\n      runInAction(() =\u003e {\n        this.fetchTodosRunning = true\n      })\n      // ...\n      await fetch('/todos')\n    } catch (err) {\n      runInAction(() =\u003e {\n        this.fetchTodosError = err\n      })\n      throw err\n    } finally {\n      runInAction(() =\u003e {\n        this.fetchTodosRunning = false\n      })\n    }\n  }\n}\n```\n\n**Your code with `mobx-task`**\n\n```js\nimport { task } from 'mobx-task'\n\nclass TodoStore {\n  @task async fetchTodos () {\n    await fetch('/todos')\n  }\n}\n```\n\n# Full example with classes and decorators\n\n```js\nimport { observable, action } from 'mobx'\nimport { task } from 'mobx-task'\nimport React from 'react'\nimport { observer } from 'mobx-react'\n\nclass TodoStore {\n  @observable todos = []\n\n  @task async fetchTodos () {\n    await fetch('/todos')\n      .then(r =\u003e r.json())\n      .then(action(todos =\u003e this.todos.replace(todos)))\n  }\n}\n\nconst store = new TodoStore()\n\n// Start the task.\nstore.fetchTodos()\n\n// and reload every 3 seconds, just cause..\nsetInterval(() =\u003e {\n  store.fetchTodos()\n}, 3000)\n\nconst App = observer(() =\u003e {\n  return (\n    \u003cdiv\u003e\n      {store.fetchTodos.match({\n        pending: () =\u003e \u003cdiv\u003eLoading, please wait..\u003c/div\u003e,\n        rejected: (err) =\u003e \u003cdiv\u003eError: {err.message}\u003c/div\u003e,\n        resolved: () =\u003e (\n          \u003cul\u003e\n            {store.todos.map(todo =\u003e\n              \u003cdiv\u003e{todo.text}\u003c/div\u003e\n            )}\n          \u003c/ul\u003e\n        )\n      })}\n    \u003c/div\u003e\n  )\n})\n```\n\n# Full example with plain observables\n\n```js\nimport { observable, action } from 'mobx'\nimport { task } from 'mobx-task'\nimport React from 'react'\nimport { observer } from 'mobx-react'\n\nconst store = observable({\n  todos: [],\n  fetchTodos: task(async () =\u003e {\n    await fetch('/todos')\n      .then(r =\u003e r.json())\n      .then(action(todos =\u003e store.todos.replace(todos)))\n  })\n})\n\n// Start the task.\nstore.fetchTodos()\n\n// and reload every 3 seconds, just cause..\nsetInterval(() =\u003e {\n  store.fetchTodos()\n}, 3000)\n\nconst App = observer(() =\u003e {\n  return (\n    \u003cdiv\u003e\n      {store.fetchTodos.match({\n        pending: () =\u003e \u003cdiv\u003eLoading, please wait..\u003c/div\u003e,\n        rejected: (err) =\u003e \u003cdiv\u003eError: {err.message}\u003c/div\u003e,\n        resolved: () =\u003e (\n          \u003cul\u003e\n            {store.todos.map(todo =\u003e\n              \u003cdiv\u003e{todo.text}\u003c/div\u003e\n            )}\n          \u003c/ul\u003e\n        )\n      })}\n    \u003c/div\u003e\n  )\n})\n```\n\n# How does it work?\n\n`mobx-task` wraps the given function in another function which\ndoes the state maintenance for you using MobX observables and computeds.\nIt also exposes the state on the function.\n\n```js\nconst func = task(() =\u003e 42)\n\n// The default state is `pending`.\nconsole.log(func.state) // pending\nconsole.log(func.pending) // true\n\n// Tasks are always async.\nfunc().then((result) =\u003e {\n  console.log(func.state) // resolved\n  console.log(func.resolved) // true\n  console.log(func.pending) // false\n\n  console.log(result) // 42\n\n  // The latest result is also stored.\n  console.log(func.result) // 42\n})\n```\n\nIt also maintains error state.\n\n```js\nconst func = task(() =\u003e {\n  throw new Error('Nope')\n})\n\nfunc().catch(err =\u003e {\n  console.log(func.state) // rejected\n  console.log(func.rejected) // true\n  console.log(err) // Error('Nope')\n  console.log(func.error) // Error('Nope')\n})\n```\n\nAnd it's fully reactive.\n\n```js\nimport { autorun } from 'mobx'\n\nconst func = task(async () =\u003e {\n  return await fetch('/api/todos').then(r =\u003e r.json())\n})\n\nautorun(() =\u003e {\n  // Very useful for functional goodness (like React components)\n  const message = func.match({\n    pending: () =\u003e 'Loading todos...',\n    rejected: (err) =\u003e `Error: ${err.message}`,\n    resolved: (todos) =\u003e `Got ${todos.length} todos`\n  })\n\n  console.log(message)\n})\n\n// start!\nfunc().then(todos =\u003e { /*...*/ })\n```\n\n# Task Groups\n\n\u003csmall\u003esince `mobx-task v2.0.0` \u003c/small\u003e\n\nA `TaskGroup` is useful when you want to track pending, resolved and rejected state for multiple tasks but treat them as one.\n\nUnder the hood, a `TaskGroup` reacts to the start of any of the tasks (when `pending` flips to `true`), tracks the latest started task, and proxies all getters\nto it. The first `pending` task (or the first task in the input array, if none are `pending`) is used as the initial task to proxy to.\n\n**IMPORTANT**: Running the tasks concurrently will lead to wonky results. The intended use is for\ntracking `pending`, `resolved` and `rejected` states of the _last run task_. You should prevent your users from \nconcurrently running tasks in the group.\n\n```js\nimport { task, TaskGroup } from 'mobx-task'\n\nconst toggleTodo = task.resolved((id) =\u003e api.toggleTodo(id))\nconst deleteTodo = task.resolved((id) =\u003e { throw new Error('deleting todos is for quitters') })\n\nconst actions = TaskGroup([\n  toggleTodo,\n  deleteTodo\n])\n\nautorun(() =\u003e {\n  const whatsGoingOn = actions.match({\n    pending: () =\u003e 'Todo is being worked on',\n    resolved: () =\u003e 'Todo is ready to be worked on',\n    rejected: (err) =\u003e `Something failed on the todo: ${err.message}`\n  })\n  console.log(whatsGoingOn)\n})\n\n// initial log from autorun setup\n// \u003c- Todo is ready to be worked on\n\nawait toggleTodo('some-id')\n\n// \u003c- Todo is being worked on\n// ...\n// \u003c- Todo is ready to be worked on\n\nawait deleteTodo('some-id')\n// \u003c- Todo is being worked on\n// ...\n// \u003c- Something failed on the todo: deleting todos is for quitters\n```\n\n# API documentation\n\nThere's only a single exported member; `task`.\n\n**ES6:**\n\n```js\nimport { task } from 'mobx-task'\n```\n\n**CommonJS:**\n\n```js\nconst { task } = require('mobx-task')\n```\n\n## The `task` factory\n\nThe top-level `task` creates a new task function and initializes it's state.\n\n```\nconst myAwesomeFunc = task(async () =\u003e {\n  return await doAsyncWork()\n})\n\n// Initial state is `pending`\nconsole.log(myAwesomeFunc.state) // \"pending\"\n```\n\nLet's try to run it\n\n```js\nconst promise = myAwesomeFunc()\nconsole.log(myAwesomeFunc.state) // \"pending\"\n\npromise.then((result) =\u003e {\n  console.log('nice', result)\n  console.log(myAwesomeFunc.state) // \"resolved\"\n})\n```\n\nParameters:\n\n- `fn` - the function to wrap in a task.\n- `opts` - options object. All options are _optional_.\n  - `opts.state` - the initial state, default is `'pending'`.\n  - `opts.error` - initial error object to set.\n  - `opts.result` - initial result to set.\n  - `opts.swallow` - if `true`, does not throw errors after catching them.\n\nAdditionally, the top-level `task` export has shortcuts for the `opts.state` option (except pending, since its the default).\n\n- `task.resolved(func, opts)`\n- `task.rejected(func, opts)`\n\nFor example:\n\n```js\nconst func = task.resolved(() =\u003e 42)\nconsole.log(func.state) // resolved\n```\n\nIs the same as doing:\n\n```js\nconst func = task(() =\u003e 42, { state: 'resolved' })\nconsole.log(func.state) // resolved\n```\n\n## As a decorator\n\nThe `task` function also works as a decorator.\n\n\u003e Note: you need to add `babel-plugin-transform-decorators-legacy` to your babel config for this to work.\n\nExample:\n\n```js\nclass Test {\n  @task async load () {\n\n  }\n\n  // shortcuts, too\n  @task.resolved async save () {\n\n  }\n\n  // with options\n  @task({ swallow: true }) async dontCareIfIThrow() {\n\n  }\n\n  // options for shortcuts\n  @task.rejected({ error: 'too dangerous lol' }) async whyEvenBother () {\n\n  }\n}\n```\n\n## The `task` itself\n\nThe thing that `task()` returns is the wrapped function including all that extra goodness.\n\n### `state`\n\nAn observable string maintained while running the task.\n\nPossible values:\n\n- `\"pending\"` - waiting to complete or didn't start yet (default)\n- `\"resolved\"` - done\n- `\"rejected\"` - failed\n\n### `pending`, `resolved`, `rejected`\n\nComputed shorthands for `state`. E.g. `pending = state === 'pending'`\n\n### `result`\n\nSet after the task completes. If the task fails, it is set to `undefined`.\n\n### `args`\n\nAn array of arguments that were used when the task function was invoked last.\n\n### `error`\n\nSet if the task fails. If the task succeeds, it is set to `undefined`.\n\n### `match()`\n\nUtility for pattern matching on the state.\n\nExample:\n\n```\nconst func = task((arg1, arg2, arg3, ..asManyAsYouWant) =\u003e 42)\n\nconst result = func.match({\n  pending: (arg1, arg2, arg3, ...asManyAsYouWant) =\u003e 'working on it',\n  rejected: (err) =\u003e 'failed: ' + err.message,\n  resolved: (answer) =\u003e `The answer to the universe and everything: ${answer}`\n})\n```\n\n### `wrap()`\n\nUsed to wrap the task in another function while preserving access to the state - aka. _Higher Order Functions_.\n\n**Returns the new function, does not modify the original function.**\n\n```js\n// Some higher-order-function...\nconst addLogging = function (inner) {\n  return function wrapped () {\n    console.log('Started')\n    return inner.apply(this, arguments).then(result =\u003e {\n      console.log('Done!')\n      return result\n    })\n  }\n}\n\nconst func = task(() =\u003e 42)\nconst funcWithLogging = func.wrap(addLogging)\n```\n\n### `setState()`\n\nLets you set the internal state at any time for whatever reason you may have. Used internally as well.\n\nExample:\n\n```\nconst func = task(() =\u003e 42)\n\nfunc.setState({ state: 'resolved', result: 1337 })\nconsole.log(func.state) // 'resolved'\nconsole.log(func.resolved) // true\nconsole.log(func.result) // 1337\n```\n\n### `bind()`\n\nThe wrapped function patches `bind()` so the bound function contains the task state, too.\nOther than that it functions exactly like `Function.prototype.bind`.\n\n```\nconst obj = {\n  value: 42,\n  doStuff: task(() =\u003e this.value)\n}\n\nconst bound = obj.doStuff.bind(obj)\nbound()\nconsole.log(bound.pending) // true\n```\n\n### `reset()`\n\nResets the state to what it was when the task was initialized.\n\nThis means if you use `const t = task.resolved(fn)`, calling `t.reset()` will set the state to `resolved`.\n\n## `TaskGroup`\n\nCreates a `TaskGroup`. Takes an array of tasks to track. Implements the readable parts of the `Task`.\n\nUses the first task in the array as the proxy target.\n\n```js\nimport { task, TaskGroup }  from 'mobx-task'\n\nconst task1 = task(() =\u003e 42)\nconst task2 = task(() =\u003e 1337)\nconst group = TaskGroup([\n  task1,\n  task2\n])\n\nconsole.log(group.state)\nconsole.log(group.resolved)\nconsole.log(group.result)\n```\n\n# Gotchas\n\n## Wrapping the task function\n\nIt's important to remember that if you wrap the task in something else, you will loose the state.\n\n**Bad:**\n\n```\nimport once from 'lodash/once'\n\nconst func = task(() =\u003e 42)\nconst funcOnce = once(func)\nconsole.log(funcOnce.pending) // undefined\n```\n\nThis is nothing special, but it's a common gotcha when you like to compose your functions. We can make this work though,\nby using `.wrap(fn =\u003e once(fn))`. See the [`wrap()`](#wrap) documentation.\n\n**Good:**\n\n```\nimport once from 'lodash/once'\n\nconst func = task(() =\u003e 42)\nconst funcOnce = func.wrap(once)\nconsole.log(funcOnce.pending) // true\n```\n\n## Using the decorator on React Components\n\nUsing the `@task` decorator on React components is absolutely a valid use case, but if you use **React Hot Loader** or\nany HMR technology that patches functions on components, you will loose access to the task state.\n\nA workaround is to not use the decorator, but a property initializer:\n\n```js\nclass Awesome extends React.Component {\n  fetchTodos = task(() =\u003e {\n    return fetch('/api/todos')\n  })\n\n  render () {\n    return (\n      \u003cdiv\u003e\n        {this.fetchTodos.match(...)}\n      \u003c/div\u003e\n    )\n  }\n}\n```\n\n## Using the decorator with `autobind-decorator`\n\nBecause of the way the `autobind-decorator` class decorator works, it won't pick up any `@task`-decorated\nclass methods because `@task` rewrites `descriptor.value` to `descriptor.get` which `autobind-decorator` does\nnot look for. This is due to the fact that `autobind-decorator` does not (and _should not_) evaluate getters.\n\nYou can either bind the tasks in the constructor, use field initializers, or apply the `@autobind` **method decorator** _before_ the `@task` decorator. `@task @autobind method() {}` is the correct order.\n\n```js\nimport autobind from 'autobind-decorator'\n\n@autobind\nclass Store {\n  value = 42\n\n  // Using decorator\n  @task boo () {\n    return this.value\n  }\n\n  // Using field initializer\n  woo = task(() =\u003e {\n    return this.value\n  })\n\n  // Decorator with autobind applied first\n  @task @autobind woohoo () {\n    return this.value\n  }\n}\n\n// Nay\nconst store = new Store()\nstore.boo() // 42\n\nconst boo = store.boo\nboo() // Error: cannot read property \"value\" of undefined\n\n// Yay\nstore.woo() // 42\n\nconst woo = store.woo\nwoo() // 42\n\nconst woohoo = store.woohoo\nwoohoo() // 42\n```\n\nAlternatively, use `this.boo = this.boo.bind(this)` in the constructor.\n\n## Using with `typescript`\n\nBest way to work with typescript is to install `@types/mobx-task`. Definitions covers most use cases. The tricky part is decorators because they are not able to change the type of the decorated target. You will have to do type assertion or use plain observables.\n\n```\nnpm install --save-dev @types/mobx-task\n```\n\nExample:\n\n```ts\nclass Test {\n  @task taskClassMethod(arg1: string, arg2: number) {\n    let result: boolean\n    ...\n    return result\n  }\n  \n  @task assertTypeHere = \u003cTask\u003cboolean, [string, number]\u003e\u003e((arg1: string, arg2: number) =\u003e {\n    let result: boolean\n    ...\n    return result\n  })\n  \n  @task assertTypeHereWithAs = ((arg1: string, arg2: number) =\u003e {\n    let result: boolean\n    ...\n    return result\n  }) as Task\u003cboolean, [string, number]\u003e\n}\n\nconst test = new Test()\n\n// dont care about task methods, props and return value and type\nconst doSomething = async () =\u003e {\n  await test.taskClassMethod('a', 1)\n  ...\n}\n\n// want to use task props and returned promise\n(test.taskClassMethod as Task)(\"one\", 2).then(...) // Task\u003cany, any[]\u003e\nconst {result} = \u003cTask\u003cResult\u003e\u003etest.taskClassMethod // Task\u003cResult, any[]\u003e\nconst {args} = test.taskClassMethod as Task\u003cvoid, [string]\u003e\n```\n\n# Author\n\nJeff Hansen - [@Jeffijoe](https://twitter.com/Jeffijoe)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeffijoe%2Fmobx-task","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjeffijoe%2Fmobx-task","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeffijoe%2Fmobx-task/lists"}