Skip to content

Basics

Stores

Store can be a single class or multiple ones. It's suggested keeping stores small, in terms of property sizes. Any piece of store you have, must use a class that extends Exome.

import { Exome } from "exome"
 
class TodoStore extends Exome {
}

Any store can be initialized (multiple times if needed) becoming an instance of a store.

const todoStore = new TodoStore()

Listen to changes

There are multiple ways to listen to changes in store, but one default method is subscribing to store instance using subscribe method.

subscribe(todoStore, () => {
  console.log("todoStore had changes")
})

Properties

Remember that this is quite a regular class (with some behind the scenes work). So you can write you data inside properties however you like. Properties can be public, private, object, arrays, getters, setters, static etc. It's all just a value.

import { Exome } from "exome"
 
class TodoStore extends Exome {
  public todo: { text: string, done: boolean }[] = [] 
}

Properties can also be defined inside constructor, just like any regular class:

import { Exome } from "exome"
 
class TodoStore extends Exome {
  constructor(
    public todo: { text: string, done: boolean }[] = [] 
  ) {
    super()
  }
}

For dynamic/computed properties you can easily use getters:

import { Exome } from "exome"
 
class TodoStore extends Exome {
  public todo: { text: string, done: boolean }[] = []
 
  public get countDone() { 
    return this.todo.filter(({ done }) => done).length
  } 
}

Actions

Every method in class is considered as an action. They should be only used for changing store instance properties. Whenever any method is called in Exome it triggers update to middleware (e.g. updates UI components). Actions can be regular sync methods and even async ones.

import { Exome } from "exome"
 
class TodoStore extends Exome {
  public todo: { text: string, done: boolean }[] = []
 
  public addTodo(todo: TodoStore["todo"][number]) { 
    this.todo.push(todo) 
  } 
}

If you want to get something from state via method, use getters or properties as functions (those will not be tracked for changes).

import { Exome } from "exome"
 
class TodoStore extends Exome {
  public todo: { text: string, done: boolean }[] = []
 
`getTodo` is NOT an action if arrow function is used
public getTodo = (index: number) => { return this.todo[index] } }

getTodo is not an action since it's defined as a property. This can be useful to get data by passing some argument. Since getting data doesn't need to trigger updates, no need to define it as action.

Async Actions

Async actions are async methods. These are methods that return Promise. Update for instance is issued only after this Promise is fulfilled or rejected.

import { Exome } from "exome"
 
class TodoStore extends Exome {
  public todo: { text: string, done: boolean }[] = []
 
  public async fetchTodoList() { 
    this.todo = await fetch(`data:,[{"text":"Wash dishes",done:true}]`) 
      .then((res) => res.json()) 
  } 
}

Getting Action Status

Every action can have a special utility applied. This especially useful for async actions as the utility getActionStatus provides loading and error state for particular actions.

import { Exome } from "exome"
import { getActionStatus } from "exome/utils"
 
class TodoStore extends Exome {
  public todo: { text: string, done: boolean }[] = []
 
  public get status() { 
    return getActionStatus(this, "fetchTodoList"); 
  } 
 
  public async fetchTodoList() { 
    this.todo = await fetch(`data:,[{"text":"Wash dishes",done:true}]`) 
      .then((res) => res.json()) 
  } 
}

Use it in a getter as it returns cached object with this interface:

interface ActionStatus<E = Error> {
  loading: boolean
  error: false | E
  unsubscribe: () => void
}
  • loading - if action is in progress;
  • error - if and what did the action throw;
  • unsubscribe - stop listening to this status.

Effects

Effects are a great way to do something extra based on actions called. For example to implement logging. Or sending & receiving state from and to server.

import { onAction } from "exome"
 
onAction(TodoStore, "addTodo", (instance, action, args) => { 
  console.log("New item:", args[0].text) 
  console.log("Item count:", instance.todo.length) 
}) 
 
const todoStore = new TodoStore()
 
todoStore.addTodo({ text: "Workout", done: false })
log: "New item: Workout"
log: "Item count: 1"

By default all onAction callbacks are being called after action is finished. But it's possible to trigger callback before action is triggered too.

onAction(TodoStore, "addTodo", (instance, action, args) => { 
  console.log("New item:", args[0].text) 
  console.log("Item count:", instance.todo.length) 
}, "before") 
 
const todoStore = new TodoStore()
 
todoStore.addTodo({ text: "Workout", done: false })
log: "New item: Workout"
log: "Item count: 0"

Get result from action

Effects can also detect if action failed or succeeded and with what response.

import { onAction } from "exome"
 
onAction(TodoStore, "addTodo", (
  instance,
  action,
  args,
  error, 
  response, 
) => {
  if (error) { 
    // action did throw error
  } 

  console.log(response) // action returned something
})

Live code playground

This is only possible when onAction is set to receive event after action is triggered (that is the default behaviour).

If actions are async, then response and throw returns awaited response, not promise.