Skip to content

Commit 9d14607

Browse files
committed
feat: Add init_state/0 callback to reducer modules
Reducers now define their own initial state via init_state/0, making them fully self-contained and reusable. Changes: - Added init_state/0 callback to :reducer macro (defaults to %{}) - Modified :process init/1 to call each reducer's init_state/0 - Reducer slices are automatically initialized from reducer's init_state/0 - SessionProcess init_state/1 only needs to define non-reducer state Benefits: - Reducers are fully self-contained (state + actions + initialization) - Reusable across different SessionProcesses - Clear separation of concerns - Parent process doesn't need to know reducer's state structure Example: ```elixir defmodule UserReducer do use Phoenix.SessionProcess, :reducer def init_state do %{users: [], loading: false, query: nil} end def handle_action(%{type: "add-user", payload: user}, state) do %{state | users: [user | state.users]} end end defmodule MyApp.SessionProcess do use Phoenix.SessionProcess, :process def init_state(_), do: %{global_count: 0} # Only non-reducer state def combined_reducers do %{users: UserReducer} # UserReducer.init_state/0 called automatically end end ``` Tests: - Added test verifying reducer init_state is called - Updated reducers to define init_state/0 - All 230 tests passing (13 integration tests) Documentation: - Updated :reducer macro docs with init_state/0 - Added State Initialization section - Updated examples to show init_state/0 Refs: User feedback requesting init_state for reducers
1 parent 177c996 commit 9d14607

File tree

2 files changed

+71
-8
lines changed

2 files changed

+71
-8
lines changed

lib/phoenix/session_process.ex

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,10 @@ defmodule Phoenix.SessionProcess do
824824
defmodule MyApp.Reducers.UserReducer do
825825
use Phoenix.SessionProcess, :reducer
826826
827+
def init_state do
828+
%{users: [], loading: false, query: nil}
829+
end
830+
827831
@throttle {"fetch-list", "3000ms"}
828832
def handle_action(%{type: "fetch-list"}, state) do
829833
# Throttled: Only executes once per 3 seconds
@@ -848,6 +852,7 @@ defmodule Phoenix.SessionProcess do
848852
849853
## Callbacks
850854
855+
- `init_state/0` - Define initial state for this reducer's slice (optional, defaults to `%{}`)
851856
- `handle_action/2` - Handle synchronous actions, return updated state
852857
- `handle_async/3` - Handle async actions with dispatch callback, return updated state
853858
@@ -857,6 +862,12 @@ defmodule Phoenix.SessionProcess do
857862
- `@debounce {action_pattern, duration}` - Debounce action (delay execution, reset timer)
858863
859864
Duration format: `"500ms"`, `"1s"`, `"5m"`, `"1h"`
865+
866+
## State Initialization
867+
868+
Each reducer defines its initial state via `init_state/0`. When using `combined_reducers/0`,
869+
the SessionProcess automatically calls each reducer's `init_state/0` to build the complete
870+
initial state with each reducer's slice.
860871
"""
861872
defmacro __using__(:reducer) do
862873
quote do
@@ -873,6 +884,27 @@ defmodule Phoenix.SessionProcess do
873884
# Hook to capture attributes when functions are defined
874885
@on_definition {Phoenix.SessionProcess, :__on_reducer_definition__}
875886

887+
@doc """
888+
Initialize the reducer's state slice.
889+
890+
Override this function to provide the initial state for this reducer's slice.
891+
892+
## Returns
893+
894+
- `initial_state` - The initial state map for this reducer
895+
896+
## Examples
897+
898+
def init_state do
899+
%{users: [], loading: false, query: nil}
900+
end
901+
902+
def init_state do
903+
%{items: [], total: 0}
904+
end
905+
"""
906+
def init_state, do: %{}
907+
876908
@doc """
877909
Handle synchronous actions.
878910
@@ -931,7 +963,7 @@ defmodule Phoenix.SessionProcess do
931963
"""
932964
def handle_async(_action, _dispatch, state), do: state
933965

934-
defoverridable handle_action: 2, handle_async: 3
966+
defoverridable init_state: 0, handle_action: 2, handle_async: 3
935967
end
936968
end
937969

@@ -1004,13 +1036,26 @@ defmodule Phoenix.SessionProcess do
10041036
%{}
10051037
end
10061038

1039+
# Initialize state slices from each reducer's init_state
1040+
app_state =
1041+
Enum.reduce(combined, user_state, fn {slice_key, module}, acc_state ->
1042+
slice_initial_state =
1043+
if function_exported?(module, :init_state, 0) do
1044+
module.init_state()
1045+
else
1046+
%{}
1047+
end
1048+
1049+
Map.put(acc_state, slice_key, slice_initial_state)
1050+
end)
1051+
10071052
# Build reducer map from combined_reducers
10081053
redux_reducers = build_combined_reducers(combined)
10091054

10101055
# Wrap in Redux infrastructure
10111056
state = %{
1012-
# User's application state
1013-
app_state: user_state,
1057+
# User's application state (with initialized reducer slices)
1058+
app_state: app_state,
10141059

10151060
# Redux infrastructure (internal, prefixed with _redux_)
10161061
_redux_reducers: redux_reducers,

test/phoenix/session_process/reducer_integration_test.exs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ defmodule Phoenix.SessionProcess.ReducerIntegrationTest do
77
defmodule UserReducer do
88
use Phoenix.SessionProcess, :reducer
99

10+
def init_state do
11+
%{users: [], fetch_count: 0, search_query: nil}
12+
end
13+
1014
@throttle {"fetch-users", "100ms"}
1115
def handle_action(%{type: "fetch-users"}, state) do
1216
Map.update(state, :fetch_count, 1, &(&1 + 1))
@@ -28,6 +32,10 @@ defmodule Phoenix.SessionProcess.ReducerIntegrationTest do
2832
defmodule CartReducer do
2933
use Phoenix.SessionProcess, :reducer
3034

35+
def init_state do
36+
%{items: []}
37+
end
38+
3139
def handle_action(%{type: "add-item", payload: item}, state) do
3240
Map.update(state, :items, [item], &[item | &1])
3341
end
@@ -44,11 +52,8 @@ defmodule Phoenix.SessionProcess.ReducerIntegrationTest do
4452
use Phoenix.SessionProcess, :process
4553

4654
def init_state(_arg) do
47-
%{
48-
global_count: 0,
49-
users: %{users: [], fetch_count: 0, search_query: nil},
50-
cart: %{items: []}
51-
}
55+
# Only define state not managed by reducers
56+
%{global_count: 0}
5257
end
5358

5459
def combined_reducers do
@@ -73,6 +78,19 @@ defmodule Phoenix.SessionProcess.ReducerIntegrationTest do
7378
end
7479

7580
describe "reducer modules" do
81+
test "verifies reducer init_state is called for each slice", %{session_id: session_id} do
82+
state = SessionProcess.get_state(session_id)
83+
84+
# Verify users slice was initialized from UserReducer.init_state/0
85+
assert state.users == %{users: [], fetch_count: 0, search_query: nil}
86+
87+
# Verify cart slice was initialized from CartReducer.init_state/0
88+
assert state.cart == %{items: []}
89+
90+
# Verify global state from SessionProcess init_state/1
91+
assert state.global_count == 0
92+
end
93+
7694
test "verifies reducer module metadata functions exist", %{session_id: _session_id} do
7795
# Check throttle metadata
7896
assert [{_, "100ms"}] = UserReducer.__reducer_throttles__()

0 commit comments

Comments
 (0)