Storex

Elixir CIDownloads

Storex is a frontend store with state management handled on the backend. It allows you to update the store state both from the frontend and backend, with all communication occurring over WebSocket.

Important: Storex is currently under active development. We encourage you to report any issues or submit feature requests here.

Why Storex?

Features

Key Differences from Phoenix LiveView

Phoenix LiveView is a powerful tool for building rich, interactive web applications without writing custom JavaScript. However, as your application grows, managing complex client-side state across multiple LiveViews or components can become challenging. This is where Storex comes in.

For an overview of Storex in action, check out the example provided here.

Basic usage

Installation

Add storex to deps in mix.exs:

defp deps do
  [{:storex, "~> 0.5.0"}]
end

Also you need to add storex to package.json dependencies:

{
  "storex": "file:../deps/storex",
}

Add storex websocket handler

You need to add handler Storex.Handler.Plug or Storex.Handler.Cowboy.

Phoenix:

defmodule YourAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :your_app

  plug Storex.Plug, path: "/storex"

  # ...
end

Cowboy:

:cowboy_router.compile([
  {:_, [
    # ...
    {"/storex", Storex.Handler.Cowboy, []},
    # ...
  ]}
])

[!IMPORTANT] Cowboy doesn’t support the Node.js (HTTP Only) connector

Create store

To create a store you need to create new elixir module with init/2 which is called when a page is loaded, every time websocket is connected it generates session_id and passes it as the first argument, params are from Javascript store declaration. init/2 callback need to return one of this tuples:

Next, you can declare mutation/5 where the first argument is mutation name, second is data passed to mutation, next two params are same like in init/2, the last one is the current state of the store.

defmodule ExampleApp.Store.Counter do
  use Storex.Store

  def init(session_id, params) do
    {:ok, 0}
  end

  # `increase` is mutation name, `data` is payload from front-end, `session_id` is current session id of connecton, `initial_params` with which store was initialized, `state` is store current state.
  def mutation("increase", _data, _session_id, _initial_params, state) do
    state = state + 1

    {:noreply, state}
  end

  def mutation("decrease", _data, _session_id, _initial_params, state) do
    state = state - 1

    {:reply, "message", state}
  end

  def mutation("set", [number], _session_id, _initial_params, state) do
    {:noreply, number}
  end
end

Connect to store

You have to connect the newly created store with a frontend side to be able to synchronise the state: params are passed as second argument in store init/2 and as third in mutation/5. You can subscribe to changes inside store state by passing option subscribe with function as a value.

import useStorex from 'storex'

const store = useStorex({
  store: 'ExampleApp.Store.Counter',
  params: {}
})

Mutate store

You can mutate store from javascript with store instance:

store.commit("increase")
store.commit("decrease").then((response) => {
  response // Reply from elixir
})
store.commit("set", 10)

Or directly from elixir:

Storex.mutate(store, "increase", [])
Storex.mutate(store, "set", [10])
Storex.mutate(key, store, "increase", [])
Storex.mutate(key, store, "set", [10])

Subscribe to store changes

You can subscribe to store state changes in javascript with function subscribe:

store.subscribe((state) => {
  const state = state
})

You can also subscribe to events after store is created:

store.onConnected(() => {
  console.log('connected')
})

store.onError((error) => {
  console.log('error', error)
})

store.onDisconnected((closeEvent) => {
  console.log('disconnected', closeEvent)
})

Connectors

The default export of useStorex uses WebSocket connections only, you can extend it by using custom connector.

Websocket

import { prepare, socketConnector } from 'storex';

const connector = socketConnector({ address: 'wss://myapi.com/storex' });
const { useStorex } = prepare({ /* global params */ }, connector);

const myStore = useStorex<MyStateType>({
  store: &#39;myStoreName&#39;,
  params: { /* store-specific params */ }
});

Node.js (HTTP Only)

Node.js connector require Node.js installed on server which running application

import { prepare, httpConnector } from &#39;storex&#39;;

const connector = httpConnector({ address: &#39;http://myapi.com/storex&#39; });
const { useStorex } = prepare({}, connector);

const myStore = useStorex<MyStateType>({
  store: &#39;myStoreName&#39;,
  params: { /* store-specific params */ }
});

// Subscribe to state changes
myStore.subscribe((state) => {
  console.log(&#39;New state:&#39;, state);
});

// Handle errors
myStore.onError((error) => {
  console.error(&#39;An error occurred:&#39;, error);
});

[!IMPORTANT] Mutations are not supported in HTTP mode myStore.commit() will not work as expected

Configuration

Session id generation library

You can change library which generate session id for stores. Module needs to have generate/0 method.

config :storex, :session_id_library, Ecto.UUID

Default params

You can set default params for all stores when preparing the Storex instance. These params will be passed to each store.

const { useStorex } = prepare({ jwt: &#39;someJWT&#39; }, connector);

Custom store address

You can specify a custom address when creating the connector:

const connector = socketConnector({ address: &#39;wss://myapi.com/storex&#39; });
// OR
const connector = httpConnector({ address: &#39;http://myapi.com/storex&#39; });