Giter VIP home page Giter VIP logo

mobx-task's Introduction

mobx-task

npm CI Coveralls npm npm

Takes the suck out of managing state for async functions in MobX.

Table of Contents

Installation

npm install --save mobx-task

What is it?

mobx-task removes the boilerplate of maintaining loading and error state of async functions in MobX.

Your code before:

class TodoStore {
  @observable fetchTodosRunning = true
  @observable fetchTodosError

  async fetchTodos () {
    try {
      runInAction(() => {
        this.fetchTodosRunning = true
      })
      // ...
      await fetch('/todos')
    } catch (err) {
      runInAction(() => {
        this.fetchTodosError = err
      })
      throw err
    } finally {
      runInAction(() => {
        this.fetchTodosRunning = false
      })
    }
  }
}

Your code with mobx-task

import { task } from 'mobx-task'

class TodoStore {
  @task async fetchTodos () {
    await fetch('/todos')
  }
}

Full example with classes and decorators

import { observable, action } from 'mobx'
import { task } from 'mobx-task'
import React from 'react'
import { observer } from 'mobx-react'

class TodoStore {
  @observable todos = []

  @task async fetchTodos () {
    await fetch('/todos')
      .then(r => r.json())
      .then(action(todos => this.todos.replace(todos)))
  }
}

const store = new TodoStore()

// Start the task.
store.fetchTodos()

// and reload every 3 seconds, just cause..
setInterval(() => {
  store.fetchTodos()
}, 3000)

const App = observer(() => {
  return (
    <div>
      {store.fetchTodos.match({
        pending: () => <div>Loading, please wait..</div>,
        rejected: (err) => <div>Error: {err.message}</div>,
        resolved: () => (
          <ul>
            {store.todos.map(todo =>
              <div>{todo.text}</div>
            )}
          </ul>
        )
      })}
    </div>
  )
})

Full example with plain observables

import { observable, action } from 'mobx'
import { task } from 'mobx-task'
import React from 'react'
import { observer } from 'mobx-react'

const store = observable({
  todos: [],
  fetchTodos: task(async () => {
    await fetch('/todos')
      .then(r => r.json())
      .then(action(todos => store.todos.replace(todos)))
  })
})

// Start the task.
store.fetchTodos()

// and reload every 3 seconds, just cause..
setInterval(() => {
  store.fetchTodos()
}, 3000)

const App = observer(() => {
  return (
    <div>
      {store.fetchTodos.match({
        pending: () => <div>Loading, please wait..</div>,
        rejected: (err) => <div>Error: {err.message}</div>,
        resolved: () => (
          <ul>
            {store.todos.map(todo =>
              <div>{todo.text}</div>
            )}
          </ul>
        )
      })}
    </div>
  )
})

How does it work?

mobx-task wraps the given function in another function which does the state maintenance for you using MobX observables and computeds. It also exposes the state on the function.

const func = task(() => 42)

// The default state is `pending`.
console.log(func.state) // pending
console.log(func.pending) // true

// Tasks are always async.
func().then((result) => {
  console.log(func.state) // resolved
  console.log(func.resolved) // true
  console.log(func.pending) // false

  console.log(result) // 42

  // The latest result is also stored.
  console.log(func.result) // 42
})

It also maintains error state.

const func = task(() => {
  throw new Error('Nope')
})

func().catch(err => {
  console.log(func.state) // rejected
  console.log(func.rejected) // true
  console.log(err) // Error('Nope')
  console.log(func.error) // Error('Nope')
})

And it's fully reactive.

import { autorun } from 'mobx'

const func = task(async () => {
  return await fetch('/api/todos').then(r => r.json())
})

autorun(() => {
  // Very useful for functional goodness (like React components)
  const message = func.match({
    pending: () => 'Loading todos...',
    rejected: (err) => `Error: ${err.message}`,
    resolved: (todos) => `Got ${todos.length} todos`
  })

  console.log(message)
})

// start!
func().then(todos => { /*...*/ })

Task Groups

since mobx-task v2.0.0

A TaskGroup is useful when you want to track pending, resolved and rejected state for multiple tasks but treat them as one.

Under 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 to 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.

IMPORTANT: Running the tasks concurrently will lead to wonky results. The intended use is for tracking pending, resolved and rejected states of the last run task. You should prevent your users from concurrently running tasks in the group.

import { task, TaskGroup } from 'mobx-task'

const toggleTodo = task.resolved((id) => api.toggleTodo(id))
const deleteTodo = task.resolved((id) => { throw new Error('deleting todos is for quitters') })

const actions = TaskGroup([
  toggleTodo,
  deleteTodo
])

autorun(() => {
  const whatsGoingOn = actions.match({
    pending: () => 'Todo is being worked on',
    resolved: () => 'Todo is ready to be worked on',
    rejected: (err) => `Something failed on the todo: ${err.message}`
  })
  console.log(whatsGoingOn)
})

// initial log from autorun setup
// <- Todo is ready to be worked on

await toggleTodo('some-id')

// <- Todo is being worked on
// ...
// <- Todo is ready to be worked on

await deleteTodo('some-id')
// <- Todo is being worked on
// ...
// <- Something failed on the todo: deleting todos is for quitters

API documentation

There's only a single exported member; task.

ES6:

import { task } from 'mobx-task'

CommonJS:

const { task } = require('mobx-task')

The task factory

The top-level task creates a new task function and initializes it's state.

const myAwesomeFunc = task(async () => {
  return await doAsyncWork()
})

// Initial state is `pending`
console.log(myAwesomeFunc.state) // "pending"

Let's try to run it

const promise = myAwesomeFunc()
console.log(myAwesomeFunc.state) // "pending"

promise.then((result) => {
  console.log('nice', result)
  console.log(myAwesomeFunc.state) // "resolved"
})

Parameters:

  • fn - the function to wrap in a task.
  • opts - options object. All options are optional.
    • opts.state - the initial state, default is 'pending'.
    • opts.error - initial error object to set.
    • opts.result - initial result to set.
    • opts.swallow - if true, does not throw errors after catching them.

Additionally, the top-level task export has shortcuts for the opts.state option (except pending, since its the default).

  • task.resolved(func, opts)
  • task.rejected(func, opts)

For example:

const func = task.resolved(() => 42)
console.log(func.state) // resolved

Is the same as doing:

const func = task(() => 42, { state: 'resolved' })
console.log(func.state) // resolved

As a decorator

The task function also works as a decorator.

Note: you need to add babel-plugin-transform-decorators-legacy to your babel config for this to work.

Example:

class Test {
  @task async load () {

  }

  // shortcuts, too
  @task.resolved async save () {

  }

  // with options
  @task({ swallow: true }) async dontCareIfIThrow() {

  }

  // options for shortcuts
  @task.rejected({ error: 'too dangerous lol' }) async whyEvenBother () {

  }
}

The task itself

The thing that task() returns is the wrapped function including all that extra goodness.

state

An observable string maintained while running the task.

Possible values:

  • "pending" - waiting to complete or didn't start yet (default)
  • "resolved" - done
  • "rejected" - failed

pending, resolved, rejected

Computed shorthands for state. E.g. pending = state === 'pending'

result

Set after the task completes. If the task fails, it is set to undefined.

args

An array of arguments that were used when the task function was invoked last.

error

Set if the task fails. If the task succeeds, it is set to undefined.

match()

Utility for pattern matching on the state.

Example:

const func = task((arg1, arg2, arg3, ..asManyAsYouWant) => 42)

const result = func.match({
  pending: (arg1, arg2, arg3, ...asManyAsYouWant) => 'working on it',
  rejected: (err) => 'failed: ' + err.message,
  resolved: (answer) => `The answer to the universe and everything: ${answer}`
})

wrap()

Used to wrap the task in another function while preserving access to the state - aka. Higher Order Functions.

Returns the new function, does not modify the original function.

// Some higher-order-function...
const addLogging = function (inner) {
  return function wrapped () {
    console.log('Started')
    return inner.apply(this, arguments).then(result => {
      console.log('Done!')
      return result
    })
  }
}

const func = task(() => 42)
const funcWithLogging = func.wrap(addLogging)

setState()

Lets you set the internal state at any time for whatever reason you may have. Used internally as well.

Example:

const func = task(() => 42)

func.setState({ state: 'resolved', result: 1337 })
console.log(func.state) // 'resolved'
console.log(func.resolved) // true
console.log(func.result) // 1337

bind()

The wrapped function patches bind() so the bound function contains the task state, too. Other than that it functions exactly like Function.prototype.bind.

const obj = {
  value: 42,
  doStuff: task(() => this.value)
}

const bound = obj.doStuff.bind(obj)
bound()
console.log(bound.pending) // true

reset()

Resets the state to what it was when the task was initialized.

This means if you use const t = task.resolved(fn), calling t.reset() will set the state to resolved.

TaskGroup

Creates a TaskGroup. Takes an array of tasks to track. Implements the readable parts of the Task.

Uses the first task in the array as the proxy target.

import { task, TaskGroup }  from 'mobx-task'

const task1 = task(() => 42)
const task2 = task(() => 1337)
const group = TaskGroup([
  task1,
  task2
])

console.log(group.state)
console.log(group.resolved)
console.log(group.result)

Gotchas

Wrapping the task function

It's important to remember that if you wrap the task in something else, you will loose the state.

Bad:

import once from 'lodash/once'

const func = task(() => 42)
const funcOnce = once(func)
console.log(funcOnce.pending) // undefined

This is nothing special, but it's a common gotcha when you like to compose your functions. We can make this work though, by using .wrap(fn => once(fn)). See the wrap() documentation.

Good:

import once from 'lodash/once'

const func = task(() => 42)
const funcOnce = func.wrap(once)
console.log(funcOnce.pending) // true

Using the decorator on React Components

Using the @task decorator on React components is absolutely a valid use case, but if you use React Hot Loader or any HMR technology that patches functions on components, you will loose access to the task state.

A workaround is to not use the decorator, but a property initializer:

class Awesome extends React.Component {
  fetchTodos = task(() => {
    return fetch('/api/todos')
  })

  render () {
    return (
      <div>
        {this.fetchTodos.match(...)}
      </div>
    )
  }
}

Using the decorator with autobind-decorator

Because of the way the autobind-decorator class decorator works, it won't pick up any @task-decorated class methods because @task rewrites descriptor.value to descriptor.get which autobind-decorator does not look for. This is due to the fact that autobind-decorator does not (and should not) evaluate getters.

You 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.

import autobind from 'autobind-decorator'

@autobind
class Store {
  value = 42

  // Using decorator
  @task boo () {
    return this.value
  }

  // Using field initializer
  woo = task(() => {
    return this.value
  })

  // Decorator with autobind applied first
  @task @autobind woohoo () {
    return this.value
  }
}

// Nay
const store = new Store()
store.boo() // 42

const boo = store.boo
boo() // Error: cannot read property "value" of undefined

// Yay
store.woo() // 42

const woo = store.woo
woo() // 42

const woohoo = store.woohoo
woohoo() // 42

Alternatively, use this.boo = this.boo.bind(this) in the constructor.

Using with typescript

Best 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.

npm install --save-dev @types/mobx-task

Example:

class Test {
  @task taskClassMethod(arg1: string, arg2: number) {
    let result: boolean
    ...
    return result
  }
  
  @task assertTypeHere = <Task<boolean, [string, number]>>((arg1: string, arg2: number) => {
    let result: boolean
    ...
    return result
  })
  
  @task assertTypeHereWithAs = ((arg1: string, arg2: number) => {
    let result: boolean
    ...
    return result
  }) as Task<boolean, [string, number]>
}

const test = new Test()

// dont care about task methods, props and return value and type
const doSomething = async () => {
  await test.taskClassMethod('a', 1)
  ...
}

// want to use task props and returned promise
(test.taskClassMethod as Task)("one", 2).then(...) // Task<any, any[]>
const {result} = <Task<Result>>test.taskClassMethod // Task<Result, any[]>
const {args} = test.taskClassMethod as Task<void, [string]>

Author

Jeff Hansen - @Jeffijoe

mobx-task's People

Contributors

cyclops26 avatar dependabot[bot] avatar invictusmb avatar jeffijoe avatar julianwielga avatar skychik avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

mobx-task's Issues

Hooks support

It looks like all the task observables and helpers aren't working out of the box with the useObserver hook.

Example:

const MyComponent = () => {
    const {
        login
      } = useObserver(() => ({
        login: myStore.login
     }));

    useEffect(() => {
         login();
     }, [login]);
    
    return login.state // Always set to pending
    return login.match(...) // Always renders the pending match
    return login.result // always undefined
}

The only way to get it working is by using the following workaround:

const {
        login,
        loginState,
      } = useObserver(() => ({
        login: myStore.login
        loginState: myStore.login.state
     }));


Support mobx v6

When can you upgrade this library to support mobx v6?
Thanks to make awesome library.

Tasks are shared across instances when using decorators

class Test {
  @task test() {
    return Promise.resolve(32);
  }
}
const a = new Test();
const b = new Test();
a.test().then(() => {
  console.log(a.test.result);
  console.log(b.test.result);
})

This prints 32 and 32 which shows that the task is shared across instances of Test. This is undocumented but I guess I would expect each task object to be part of the instance itself.

Using the task function ie test = task(() => ...) works as expected so this is a viable workaround

Track started/unstarted state of task

Thanks for writing this library. It saves my time a lot. There is a small problem that task does not have a state to track if task started. The scenario that I am working on is: showing a form to allow user input information. The save button triggers to start the task. After saving, we show a message to user. So the problem is: we don't know if task started (to disable form and show spinner).

The temporary solution I am working on is add a state 'initial'. But this state is not official state.

class TodoStore {
  @task({ state: 'initial' })
  submitTodo() {
    // send to server and update local array
  }
}

@observer
class AddTodo extends PureComponent {
  render() {
    const submitTodoTask = todoStore.submitTodo;

    if (submitTodoTask.state === 'initial') {
       return (<TodoForm state={this.state.formState} onChange={(formState) => {this.setState({ formState })}}>)
    }

    if (submitTodoTask.state === 'pending') {
      return (<Spinner>);
    }

    if (submitTodoTask.state === 'resolved') {
      return (<p>Added successfully!</p>);
    }
  }
};

Let me know what do you think?

More complicated scenarios

Love the idea! Trying to think through something like this.
However I quickly came across scenarios I couldn't see a solution for with this.
Was wondering if you had any thoughts on how the library would be used for the below.

Most of our components will have multiple actions (say load and update)

  • How would you combine these so render knows if it needs to display load or update errors?
  • How would you allow for UI that displays errors with the actual content?

Typescript support

Hi there,

Right now, we have to manually implement index.d.ts in our project and I cannot figure out how to declare the decorator @task with options so that it works in typescript. My current workaround is

// @ts-ignore
@task({ swallow: true })

our current index.d.ts file:


declare module "mobx-task" {
  type NoArgWorker<U> = () => U
  type OneArgWorker<T1, U> = (a: T1) => U
  type TwoArgWorker<T1, T2, U> = (a: T1, b: T2) => U
  type ThreeArgWorker<T1, T2, T3, U> = (a: T1, b: T2, c: T3) => U

  interface TaskStatusAware<U> {
    match: any

    result: U

    pending: boolean
    resolved: boolean
    rejected: boolean
  }

  interface NoArgTask<U> extends TaskStatusAware<U> {
    (): U
  }

  interface OneArgTask<T1, U> extends TaskStatusAware<U> {
    (a: T1): U
  }

  interface TwoArgTask<T1, T2, U> extends TaskStatusAware<U> {
    (a: T1, b: T2): U
  }

  export function task<U>(worker: NoArgWorker<U>, options?: Object): NoArgTask<U>
  export function task<T1, U>(worker: OneArgWorker<T1, U>, options?: Object): OneArgTask<T1, U>
  export function task<T1, T2, U>(
    worker: TwoArgWorker<T1, T2, U>,
    options?: Object
  ): TwoArgTask<T1, T2, U>


}

mobx packages from package.json:

    "mobx": "^4.1.1",
    "mobx-react": "^5.0.0",
    "mobx-react-router": "^4.0.2",
    "mobx-task": "^1.0.0-alpha.0",

No unstarted state?

Why isn't there an unstarted state? I'd like to be able to determine whether or not the task has started.

My use-case:
I'd like to show a login request loading spinner when a task is pending, but because the task is pending before it is called, the loading spinner shows immediately.

When is a task started?

This lib looks very good

In the docs it says your code before..
async fetchTodos () { ... }

fetchTodos isn't started until you explicitly call the function.

Is a @task started as soon as it is created? Or upon accessing store.fetchTodos? It's not stated in the docs right now. It would be good to point out this difference from your-code-before. Thanks!

Can I use mobx-task with mobx-state-tree?

I am trying to use mobx-task to wrap a mobx-state-tree action with mobx-task like this :

const validateNewCustomer = task(flow(function*(pin) {
   const response = yield validateNewCustomerWithPinApi(
    self.customer.id,
    pin
  )
  if (response.data === true) {
    return true
  }
  return false
}))

But when I try to access the task status from within a component I get undefined?

Has anyone tried something similar? I would really love to avoid all the boilerplate of tracking the promise state manually if possible

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.