Skip to content

Commit 655a2ec

Browse files
committed
feat(v1.0.0): implement action prefix stripping in reducers
Breaking Change: - Reducers with @action_prefix now receive action types with the prefix stripped - Example: With @action_prefix "counter", handle_action receives "increment" instead of "counter.increment" - Dispatch calls remain unchanged (still use full prefixed names) - Catch-all reducers (prefix nil/"") receive unchanged action types Benefits: - Cleaner reducer code without prefix repetition - Reducers focus on local action names - Better separation of concerns - Consistent with Redux best practices Implementation: - Added strip_action_prefix/2 helper function - Strips prefix before passing to handle_action/handle_async - Only affects reducers with non-nil/non-empty @action_prefix Changes: - lib/phoenix/session_process.ex: Core prefix stripping logic - examples/02_redux_reducers.exs: Updated 5 action type patterns - examples/03_async_actions.exs: Updated 5 action type patterns - test/phoenix/session_process/reducer_integration_test.exs: Updated 3 patterns - README.md: Updated 2 documentation patterns - CLAUDE.md: Updated 5 documentation patterns - CHANGELOG.md: Added breaking change documentation with migration guide Testing: - All 148 tests passing - Examples 01 and 02 verified working - Example 03 has unrelated Task reply issue (not from this change) Migration: 1. Remove prefix from action type patterns in handle_action/handle_async 2. Keep dispatch calls unchanged 3. Only affects reducers with explicit @action_prefix
1 parent 98dafc7 commit 655a2ec

File tree

7 files changed

+89
-34
lines changed

7 files changed

+89
-34
lines changed

CHANGELOG.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5151
- Use the Redux Store API (built into SessionProcess) instead
5252
- See migration guide: `REDUX_TO_SESSIONPROCESS_MIGRATION.md`
5353

54+
- **Action prefix stripping in reducers**
55+
- Reducers with `@action_prefix` now receive action types with the prefix stripped
56+
- When a reducer has `@action_prefix "counter"`, `handle_action` receives `"increment"` instead of `"counter.increment"`
57+
- Dispatch calls still use full prefixed names (e.g., `dispatch(id, "counter.increment")`)
58+
- Catch-all reducers (prefix `nil` or `""`) receive unchanged action types
59+
- Migration:
60+
```elixir
61+
# Before (v0.x)
62+
defmodule CounterReducer do
63+
use Phoenix.SessionProcess, :reducer
64+
@name :counter
65+
@action_prefix "counter"
66+
67+
def handle_action(%Action{type: "counter.increment"}, state) do
68+
%{state | count: state.count + 1}
69+
end
70+
end
71+
72+
# After (v1.0.0)
73+
defmodule CounterReducer do
74+
use Phoenix.SessionProcess, :reducer
75+
@name :counter
76+
@action_prefix "counter"
77+
78+
def handle_action(%Action{type: "increment"}, state) do
79+
%{state | count: state.count + 1}
80+
end
81+
end
82+
83+
# Dispatch calls remain unchanged
84+
dispatch(session_id, "counter.increment")
85+
```
86+
5487
### Added
5588

5689
- **`dispatch_async/3` function for explicit async dispatch**
@@ -72,12 +105,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
72105
- Replace with `@action_prefix`
73106
- No logic changes required
74107

75-
2. **Update dispatch call sites to handle `:ok` return value**
108+
2. **Update action type patterns in handle_action/handle_async**
109+
- Remove the prefix from action type patterns
110+
- Example: `"counter.increment"` becomes `"increment"`
111+
- Dispatch calls remain unchanged (still use full prefixed names)
112+
- Only affects reducers with non-nil/non-empty `@action_prefix`
113+
114+
3. **Update dispatch call sites to handle `:ok` return value**
76115
- Replace `{:ok, state} = dispatch(...)` with `:ok = dispatch(...)`
77116
- Add `get_state(session_id)` calls where you need the updated state
78117
- Consider: Do you actually need the state? Many dispatches are fire-and-forget
79118

80-
3. **Remove uses of deprecated Redux module**
119+
4. **Remove uses of deprecated Redux module**
81120
- If using `Phoenix.SessionProcess.Redux` struct-based API
82121
- Migrate to Redux Store API (SessionProcess IS the store)
83122
- See `REDUX_TO_SESSIONPROCESS_MIGRATION.md` for detailed migration

CLAUDE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,10 @@ defmodule MyApp.CounterReducer do
323323
alias Phoenix.SessionProcess.Action
324324

325325
case action do
326-
%Action{type: "counter.increment"} ->
326+
%Action{type: "increment"} ->
327327
%{state | count: state.count + 1}
328328

329-
%Action{type: "counter.set", payload: value} ->
329+
%Action{type: "set", payload: value} ->
330330
%{state | count: value}
331331

332332
_ ->
@@ -349,10 +349,10 @@ defmodule MyApp.UserReducer do
349349
alias Phoenix.SessionProcess.Action
350350

351351
case action do
352-
%Action{type: "user.set", payload: user} ->
352+
%Action{type: "set", payload: user} ->
353353
%{state | current_user: user}
354354

355-
%Action{type: "user.update_preferences", payload: prefs} ->
355+
%Action{type: "update_preferences", payload: prefs} ->
356356
%{state | preferences: Map.merge(state.preferences, prefs)}
357357

358358
_ ->
@@ -365,7 +365,7 @@ defmodule MyApp.UserReducer do
365365
alias Phoenix.SessionProcess.Action
366366

367367
case action do
368-
%Action{type: "user.fetch", payload: user_id} ->
368+
%Action{type: "fetch", payload: user_id} ->
369369
task = Task.async(fn ->
370370
user = MyApp.Users.get(user_id)
371371
# dispatch signature: dispatch(type, payload \\ nil, meta \\ [])

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,10 @@ defmodule MyApp.CounterReducer do
163163
alias Phoenix.SessionProcess.Action
164164

165165
case action do
166-
%Action{type: "counter.increment"} ->
166+
%Action{type: "increment"} ->
167167
%{state | count: state.count + 1}
168168

169-
%Action{type: "counter.set", payload: value} ->
169+
%Action{type: "set", payload: value} ->
170170
%{state | count: value}
171171

172172
_ ->

examples/02_redux_reducers.exs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ defmodule CounterReducer do
2424
alias Phoenix.SessionProcess.Action
2525

2626
case action do
27-
%Action{type: "counter.increment"} ->
27+
%Action{type: "increment"} ->
2828
%{state | count: state.count + 1}
2929

30-
%Action{type: "counter.decrement"} ->
30+
%Action{type: "decrement"} ->
3131
%{state | count: state.count - 1}
3232

33-
%Action{type: "counter.set", payload: value} ->
33+
%Action{type: "set", payload: value} ->
3434
%{state | count: value}
3535

3636
_ ->
@@ -54,10 +54,10 @@ defmodule UserReducer do
5454
alias Phoenix.SessionProcess.Action
5555

5656
case action do
57-
%Action{type: "user.login", payload: user} ->
57+
%Action{type: "login", payload: user} ->
5858
%{state | current_user: user, logged_in: true}
5959

60-
%Action{type: "user.logout"} ->
60+
%Action{type: "logout"} ->
6161
%{state | current_user: nil, logged_in: false}
6262

6363
_ ->

examples/03_async_actions.exs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@ defmodule AsyncReducer do
2323
alias Phoenix.SessionProcess.Action
2424

2525
case action do
26-
%Action{type: "data.loading"} ->
26+
%Action{type: "loading"} ->
2727
%{state | loading: true, error: nil}
2828

29-
%Action{type: "data.loaded", payload: items} ->
29+
%Action{type: "loaded", payload: items} ->
3030
%{state | items: items, loading: false}
3131

32-
%Action{type: "data.error", payload: error} ->
32+
%Action{type: "error", payload: error} ->
3333
%{state | error: error, loading: false}
3434

35-
%Action{type: "data.clear"} ->
35+
%Action{type: "clear"} ->
3636
%{state | items: [], loading: false, error: nil}
3737

3838
_ ->
@@ -45,7 +45,7 @@ defmodule AsyncReducer do
4545
alias Phoenix.SessionProcess.Action
4646

4747
case action do
48-
%Action{type: "data.fetch", payload: delay_ms} ->
48+
%Action{type: "fetch", payload: delay_ms} ->
4949
# Start async task
5050
task =
5151
Task.async(fn ->

lib/phoenix/session_process.ex

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,13 +1520,34 @@ defmodule Phoenix.SessionProcess do
15201520
Map.put(state, slice_key, slice_initial_state)
15211521
end
15221522

1523+
# Strip action prefix before passing to reducer if reducer has a prefix
1524+
defp strip_action_prefix(action, reducer_prefix) do
1525+
if reducer_prefix && reducer_prefix != "" do
1526+
case String.split(action.type, ".", parts: 2) do
1527+
[^reducer_prefix, local_type] ->
1528+
%{action | type: local_type}
1529+
1530+
_ ->
1531+
# Prefix doesn't match, keep as-is
1532+
action
1533+
end
1534+
else
1535+
# No prefix or catch-all reducer, pass unchanged
1536+
action
1537+
end
1538+
end
1539+
15231540
# credo:disable-for-lines:60 Credo.Check.Refactor.Nesting
15241541
defp apply_combined_reducer(module, action, slice_key, app_state, internal_state) do
15251542
alias Phoenix.SessionProcess.ActionRateLimiter
15261543

15271544
# Get the state slice for this reducer
15281545
slice_state = Map.get(app_state, slice_key, %{})
15291546

1547+
# Get reducer's action prefix and strip it from action type
1548+
reducer_prefix = module.__reducer_action_prefix__()
1549+
local_action = strip_action_prefix(action, reducer_prefix)
1550+
15301551
# Check throttle first
15311552
if ActionRateLimiter.should_throttle?(module, action, internal_state) do
15321553
# Skip action due to throttle
@@ -1551,7 +1572,8 @@ defmodule Phoenix.SessionProcess do
15511572
dispatch_fn = &__async_dispatch__(session_pid, &1, &2, &3)
15521573

15531574
# handle_async returns cancel function, not state
1554-
cancel_fn = module.handle_async(action, dispatch_fn, slice_state)
1575+
# Pass local_action with stripped prefix
1576+
cancel_fn = module.handle_async(local_action, dispatch_fn, slice_state)
15551577

15561578
# Store cancel function in internal state if action has cancel_ref
15571579
new_internal_with_cancel =
@@ -1569,7 +1591,8 @@ defmodule Phoenix.SessionProcess do
15691591
{slice_state, new_internal_with_cancel}
15701592
else
15711593
# Synchronous action updates state
1572-
new_state = module.handle_action(action, slice_state)
1594+
# Pass local_action with stripped prefix
1595+
new_state = module.handle_action(local_action, slice_state)
15731596
{new_state, internal_state}
15741597
end
15751598

test/phoenix/session_process/reducer_integration_test.exs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ defmodule Phoenix.SessionProcess.ReducerIntegrationTest do
2020
Map.update(state, :fetch_count, 1, &(&1 + 1))
2121
end
2222

23-
def handle_action(%Action{type: "user.fetch-users"}, state) do
24-
Map.update(state, :fetch_count, 1, &(&1 + 1))
25-
end
26-
2723
@debounce {"search-users", "50ms"}
2824
def handle_action(%Action{type: "search-users", payload: query}, state) do
2925
Map.put(state, :search_query, query)
@@ -56,11 +52,6 @@ defmodule Phoenix.SessionProcess.ReducerIntegrationTest do
5652
Map.put(state, :items, [])
5753
end
5854

59-
# Also match with prefix for routing tests
60-
def handle_action(%Action{type: "cart.clear-cart"}, state) do
61-
Map.put(state, :items, [])
62-
end
63-
6455
def handle_action(_action, state), do: state
6556
end
6657

@@ -1172,12 +1163,14 @@ defmodule Phoenix.SessionProcess.ReducerIntegrationTest do
11721163
session_id = "routing_custom_prefix_#{:rand.uniform(1_000_000)}"
11731164
{:ok, _pid} = SessionProcess.start(session_id, CustomPrefixSession)
11741165

1175-
# Action with "ship.calculate" should route to ShippingReducer
1176-
SessionProcess.dispatch(session_id, "ship.calculate-shipping", payload: "123 Main St")
1166+
# Action with "ship.calculate-shipping" should route to ShippingReducer
1167+
# After prefix stripping, becomes "calculate-shipping" which matches the handler
1168+
SessionProcess.dispatch(session_id, "ship.calculate-shipping", "123 Main St")
11771169

11781170
state = SessionProcess.get_state(session_id)
1179-
# ShippingReducer doesn't handle this action, but it was routed correctly
1180-
assert state.shipping.address == nil
1171+
# ShippingReducer now handles this action with stripped prefix
1172+
assert state.shipping.address == "123 Main St"
1173+
assert state.shipping.cost == 10
11811174

11821175
SessionProcess.terminate(session_id)
11831176
end

0 commit comments

Comments
 (0)