<img src="assets/logo_text.svg" height="120" alt="Rephex Logo">
**Rephex**: Target to introduce the Power of [Redux-toolkit](https://redux-toolkit.js.org) to Phoenix LiveView.
*Experimental: [README.md via ChatGPT is available](https://chat.openai.com/g/g-btova0uNt-rephex-readme-md)*
By integrating Rephex into your Phoenix LiveView projects, you unlock a suite of capabilities designed to enhance the structure, readability, and maintainability of your code:
- **Decouple State Management from Views**: Achieve a clean separation between your application's state and its presentation layer, allowing for more manageable codebases and clearer state transitions.
- **Child-Driven Assigns**: Empower smaller components to define their required assigns, streamlining data flow and component hierarchy for a more intuitive development experience.
- **Clear State Demarcation**: Easily distinguish between global application states and the ephemeral local states of individual components, such as form inputs, enhancing the clarity of your component architecture.
- **Simplified Asynchronous Operations**: Rephex simplifies the handling of asynchronous operations, making it easier to manage data fetching, processing, and more with minimal boilerplate.
## State Example
<!-- MODULEDOC -->
```elixir
# Define state
defmodule RephexPgWeb.State do
@initial_state %{
count: 0,
}
use Rephex.State, initial_state: @initial_state
def add_count(socket, %{amount: amount} = _payload) when is_integer(amount) do
# You can use `update_state`, `update_state_in` and `put_state_in` to update state
update_state_in(socket, [:count], &(&1 + amount))
end
end
```
```elixir
# Use state in LiveView
defmodule RephexPgWeb.AccountLive.Index do
alias RephexPgWeb.State
use RephexPgWeb, :live_view
use Rephex.LiveView
alias Phoenix.LiveView.{AsyncResult, Socket}
alias RephexPgWeb.AccountLive.ComponentA
@impl true
def mount(_params, _session, %Socket{} = socket) do
{:ok, socket |> State.init()}
end
@impl true
def handle_event("add_count", %{"amount" => amount}, %Socket{} = socket) do
{am, _} = Integer.parse(amount)
{:noreply, socket |> State.add_count(%{amount: am})}
end
@impl true
def render(assigns) do
# At default, Rephex state is assigned at `:rpx`.
# You can change root key by config.
~H"""
<div>Count: <%= @rpx.count %></div>
<button class="border-2" phx-click="add_count" phx-value-amount={1}>
[Add Count 1]
</button>
<.live_component module={ComponentA} id="cmp_a" rpx={@rpx} />
"""
end
end
```
```elixir
# Use state in LiveComponent
defmodule RephexPgWeb.AccountLive.ComponentA do
use RephexPgWeb, :live_component
use Rephex.LiveComponent
alias Phoenix.LiveView.Socket
@initial_local_state %{}
@impl true
def mount(%Socket{} = socket) do
{:ok, socket |> assign(@initial_local_state)}
end
@impl true
def update(assigns, socket) do
{:ok,
socket
|> propagate_rephex(assigns)}
end
@impl true
def handle_event("add_count", %{"amount" => amount}, %Socket{} = socket) do
{am, _} = Integer.parse(amount)
{:noreply,
socket
|> call_in_root(fn socket ->
State.add_count(socket, %{amount: am})
end)}
end
@impl true
def render(assigns) do
~H"""
<button class="border-2" phx-click="add_count" phx-value-amount={2} phx-target={@myself}>
[Add Count 2]
</button>
"""
end
end
```
<!-- MODULEDOC -->
## Selector Example
`Rephex.Selector` is a module that functions similarly to a view in a database.
It manages display data derived from actual data, keeping the presentation
layer in sync with changes to the underlying data.
Selectors automatically update their values whenever the associated real data changes,
effectively decoupling the logic for updating display data from the logic for updating real data.
```elixir
defmodule RephexPg.StateWithSelector do
alias RephexPg.StateWithSelector.SelectDoubleV
alias Rephex.Selector
@initial_state %{
v: 1,
double_v: Selector.new(SelectDoubleV)
}
use Rephex.State, initial_state: @initial_state
# mutators --------------------------------
def set_v(socket, v) do
put_state_in(socket, [:v], v)
end
# selectors --------------------------------
defmodule SelectDoubleV do
@behaviour Rephex.Selector
@impl true
def args(state), do: {state.v}
@impl true
def eval({v}), do: v * 2
# if state.v is not changed, eval/1 will not be called.
end
end
```
## AsyncAction Example
Facilitates asynchronous operations in Phoenix LiveViews with enhanced state management.
`Rephex.AsyncAction` seamlessly integrates with `Phoenix.LiveView` to manage asynchronous tasks,
particularly useful for operations that require real-time feedback to the user,
such as loading data or performing long-running tasks.
```elixir
defmodule RephexPgWeb.State do
alias Phoenix.LiveView.AsyncResult
@initial_state %{
count: 0,
# AsyncAction requires AsyncResult.
# rpx.double_value.result: Result of AsyncAction.start_async
# rpx.double_value.loading: `{progress, _meta}` while AsyncAction is running. `progress/1` in `start_async/4` will update progress.
# In this case, rpx.double_value.loading will be `{{current, max}, _meta}`
# rpx.double_value.failed: Result of AsyncAction.start_async (if canceled or exception raised)
double_value: AsyncResult.ok(0)
}
use Rephex.State, initial_state: @initial_state
def add_count(socket, %{amount: amount} = _payload) when is_integer(amount) do
# You can use `update_state`, `update_state_in` and `put_state_in` to update state
update_state_in(socket, [:count], &(&1 + amount))
end
end
```
```elixir
defmodule RephexPgWeb.State.HeavyDoubleAsync do
use Rephex.AsyncAction, result_path: [:double_value]
@impl true
def initial_progress(_path, _payload) do
# optional but recommended
# `start/4` apply this progress synchronously.
# AsyncResult.loading will be `{progress, _meta_values}` before start_async.
{0, 100}
end
@impl true
def start_async(_state, _path, %{amount: amount} = _payload, progress) do
# required
# This function will be passed to Phoenix's `start_async`.
max = 100
progress.({0, max})
1..max
|> Enum.each(fn i ->
:timer.sleep(2)
# Update `rpx.double_value.loading` by AsyncResult.loading/2
progress.({i, max})
end)
amount * 2
# AsyncAction will call `AsyncResult.ok(prev, amount)` on `handle_event`.
end
end
```
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `rephex` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:rephex, "~> 0.2.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/rephex>.