|
280 | 280 |
|
281 | 281 | --- |
282 | 282 |
|
283 | | -> **Deprecation Notice**: The following section shows the old Redux API which is deprecated as of v0.6.0. |
284 | | -> Please migrate to the new Redux Store API shown above. |
285 | | -
|
286 | | -### With LiveView Integration (Redux-Based) - **DEPRECATED** |
287 | | - |
288 | | -Phoenix.SessionProcess provides Redux-based LiveView integration for real-time state synchronization. |
289 | | - |
290 | | -**This approach is deprecated** - use the Redux Store API (see above) instead. |
291 | | - |
292 | | -#### Session Process with Redux - **DEPRECATED** |
293 | | - |
294 | | -```elixir |
295 | | -defmodule MyApp.SessionProcess do |
296 | | - use Phoenix.SessionProcess, :process |
297 | | - alias Phoenix.SessionProcess.Redux |
298 | | - |
299 | | - @impl true |
300 | | - def init(_init_arg) do |
301 | | - redux = Redux.init_state(%{user: nil, count: 0}) |
302 | | - {:ok, %{redux: redux}} |
303 | | - end |
304 | | - |
305 | | - @impl true |
306 | | - def handle_call(:get_redux_state, _from, state) do |
307 | | - {:reply, {:ok, state.redux}, state} |
308 | | - end |
309 | | - |
310 | | - @impl true |
311 | | - def handle_cast({:set_user, user}, state) do |
312 | | - # Dispatch action - Redux handles all notifications automatically |
313 | | - new_redux = Redux.dispatch(state.redux, {:set_user, user}, &reducer/2) |
314 | | - {:noreply, %{state | redux: new_redux}} |
315 | | - end |
316 | | - |
317 | | - @impl true |
318 | | - def handle_cast(:increment, state) do |
319 | | - new_redux = Redux.dispatch(state.redux, :increment, &reducer/2) |
320 | | - {:noreply, %{state | redux: new_redux}} |
321 | | - end |
322 | | - |
323 | | - defp reducer(state, action) do |
324 | | - case action do |
325 | | - {:set_user, user} -> %{state | user: user} |
326 | | - :increment -> %{state | count: state.count + 1} |
327 | | - _ -> state |
328 | | - end |
329 | | - end |
330 | | -end |
331 | | -``` |
332 | | - |
333 | | -#### LiveView with Redux Integration |
334 | | - |
335 | | -```elixir |
336 | | -defmodule MyAppWeb.DashboardLive do |
337 | | - use Phoenix.LiveView |
338 | | - alias Phoenix.SessionProcess.LiveView, as: SessionLV |
339 | | - |
340 | | - def mount(_params, %{"session_id" => session_id}, socket) do |
341 | | - # Subscribe to Redux state and get initial state |
342 | | - case SessionLV.mount_session(socket, session_id) do |
343 | | - {:ok, socket, state} -> |
344 | | - {:ok, assign(socket, state: state, session_id: session_id)} |
345 | | - |
346 | | - {:error, _reason} -> |
347 | | - {:ok, redirect(socket, to: "/login")} |
348 | | - end |
349 | | - end |
350 | | - |
351 | | - # Automatically receive Redux state updates |
352 | | - def handle_info({:redux_state_change, %{state: new_state}}, socket) do |
353 | | - {:noreply, assign(socket, state: new_state)} |
354 | | - end |
355 | | - |
356 | | - # Send messages to session |
357 | | - def handle_event("increment", _params, socket) do |
358 | | - SessionLV.dispatch_async(socket.assigns.session_id, "increment") |
359 | | - {:noreply, socket} |
360 | | - end |
361 | | - |
362 | | - # Clean up subscription on terminate |
363 | | - def terminate(_reason, socket) do |
364 | | - SessionLV.unmount_session(socket) |
365 | | - :ok |
366 | | - end |
367 | | -end |
368 | | -``` |
369 | | - |
370 | | -#### Configuration for LiveView |
371 | | - |
372 | | -**Important**: LiveView integration requires Redux for state management. |
373 | | - |
374 | 283 | ## API Reference |
375 | 284 |
|
376 | 285 | ### Starting Sessions |
@@ -559,225 +468,8 @@ end |
559 | 468 |
|
560 | 469 | This is idiomatic Elixir and gives you full control over your state transitions. |
561 | 470 |
|
562 | | -### Advanced: Redux-Style State (Optional) |
563 | | - |
564 | | -For complex applications requiring audit trails or time-travel debugging, you can optionally use `Phoenix.SessionProcess.Redux`: |
565 | | - |
566 | | -```elixir |
567 | | -defmodule MyApp.ReduxSessionProcess do |
568 | | - use Phoenix.SessionProcess, :process |
569 | | - alias Phoenix.SessionProcess.Redux |
570 | | - |
571 | | - @impl true |
572 | | - def init(_init_arg) do |
573 | | - initial_state = %{ |
574 | | - user: nil, |
575 | | - preferences: %{}, |
576 | | - cart: [], |
577 | | - activity_log: [] |
578 | | - } |
579 | | - |
580 | | - redux = Redux.init_state(initial_state, reducer: &reducer/2) |
581 | | - {:ok, %{redux: redux}} |
582 | | - end |
583 | | - |
584 | | - # Define reducer to handle all state changes |
585 | | - def reducer(state, action) do |
586 | | - case action do |
587 | | - {:set_user, user} -> |
588 | | - %{state | user: user} |
589 | | - |
590 | | - {:update_preferences, prefs} -> |
591 | | - %{state | preferences: Map.merge(state.preferences, prefs)} |
592 | | - |
593 | | - {:add_to_cart, item} -> |
594 | | - %{state | cart: [item | state.cart]} |
595 | | - |
596 | | - {:remove_from_cart, item_id} -> |
597 | | - cart = Enum.reject(state.cart, &(&1.id == item_id)) |
598 | | - %{state | cart: cart} |
599 | | - |
600 | | - :clear_cart -> |
601 | | - %{state | cart: []} |
602 | | - |
603 | | - {:log_activity, activity} -> |
604 | | - log = [activity | state.activity_log] |
605 | | - %{state | activity_log: log} |
606 | | - |
607 | | - :reset -> |
608 | | - %{user: nil, preferences: %{}, cart: [], activity_log: []} |
609 | | - |
610 | | - _ -> |
611 | | - state |
612 | | - end |
613 | | - end |
614 | | - |
615 | | - @impl true |
616 | | - def handle_call(:get_state, _from, %{redux: redux} = state) do |
617 | | - current_state = Redux.current_state(redux) |
618 | | - {:reply, current_state, state} |
619 | | - end |
620 | | - |
621 | | - @impl true |
622 | | - def handle_call(:get_history, _from, %{redux: redux} = state) do |
623 | | - history = Redux.history(redux) |
624 | | - {:reply, history, state} |
625 | | - end |
626 | | - |
627 | | - @impl true |
628 | | - def handle_cast({:dispatch, action}, %{redux: redux} = state) do |
629 | | - new_redux = Redux.dispatch(redux, action) |
630 | | - {:noreply, %{state | redux: new_redux}} |
631 | | - end |
632 | | -end |
633 | | - |
634 | | -# Usage |
635 | | -Phoenix.SessionProcess.start("session_123", MyApp.ReduxSessionProcess) |
636 | | -Phoenix.SessionProcess.cast("session_123", {:dispatch, {:set_user, %{id: 1, name: "Alice"}}}) |
637 | | -Phoenix.SessionProcess.cast("session_123", {:dispatch, {:add_to_cart, %{id: 101, name: "Widget", price: 29.99}}}) |
638 | | - |
639 | | -{:ok, state} = Phoenix.SessionProcess.call("session_123", :get_state) |
640 | | -{:ok, history} = Phoenix.SessionProcess.call("session_123", :get_history) |
641 | | -``` |
642 | | - |
643 | | -#### Redux with Subscriptions and Selectors |
644 | | - |
645 | | -React to specific state changes with subscriptions and selectors: |
646 | | - |
647 | | -```elixir |
648 | | -defmodule MyApp.ReactiveSession do |
649 | | - use Phoenix.SessionProcess, :process |
650 | | - alias Phoenix.SessionProcess.Redux |
651 | | - alias Phoenix.SessionProcess.Redux.Selector |
652 | | - |
653 | | - @impl true |
654 | | - def init(_init_arg) do |
655 | | - redux = Redux.init_state(%{user: nil, cart: [], total: 0}) |
656 | | - |
657 | | - # Subscribe to user changes |
658 | | - redux = |
659 | | - Redux.subscribe(redux, fn state -> state.user end, fn user -> |
660 | | - IO.inspect(user, label: "User changed") |
661 | | - end) |
662 | | - |
663 | | - # Subscribe with memoized selector for cart total |
664 | | - cart_total_selector = |
665 | | - Selector.create_selector( |
666 | | - [fn state -> state.cart end], |
667 | | - fn cart -> |
668 | | - Enum.reduce(cart, 0, fn item, acc -> acc + item.price end) |
669 | | - end |
670 | | - ) |
671 | | - |
672 | | - redux = |
673 | | - Redux.subscribe(redux, cart_total_selector, fn total -> |
674 | | - IO.inspect(total, label: "Cart total") |
675 | | - end) |
676 | | - |
677 | | - {:ok, %{redux: redux}} |
678 | | - end |
679 | | - |
680 | | - @impl true |
681 | | - def handle_call({:dispatch, action}, _from, state) do |
682 | | - new_redux = Redux.dispatch(state.redux, action, &reducer/2) |
683 | | - {:reply, {:ok, Redux.get_state(new_redux)}, %{state | redux: new_redux}} |
684 | | - end |
685 | | - |
686 | | - defp reducer(state, action) do |
687 | | - case action do |
688 | | - {:set_user, user} -> %{state | user: user} |
689 | | - {:add_to_cart, item} -> %{state | cart: [item | state.cart]} |
690 | | - {:clear_cart} -> %{state | cart: []} |
691 | | - _ -> state |
692 | | - end |
693 | | - end |
694 | | -end |
695 | | -``` |
696 | | - |
697 | | -#### Redux with LiveView |
698 | | - |
699 | | -Automatically update LiveView assigns from Redux state: |
700 | | - |
701 | | -```elixir |
702 | | -defmodule MyAppWeb.ShoppingCartLive do |
703 | | - use Phoenix.LiveView |
704 | | - alias Phoenix.SessionProcess.Redux.LiveView, as: ReduxLV |
705 | | - alias Phoenix.SessionProcess.Redux.Selector |
706 | | - |
707 | | - def mount(_params, %{"session_id" => session_id}, socket) do |
708 | | - if connected?(socket) do |
709 | | - # Define selectors |
710 | | - cart_count_selector = Selector.create_selector( |
711 | | - [fn state -> state.cart end], |
712 | | - fn cart -> length(cart) end |
713 | | - ) |
714 | | - |
715 | | - cart_total_selector = Selector.create_selector( |
716 | | - [fn state -> state.cart end], |
717 | | - fn cart -> Enum.reduce(cart, 0, &(&1.price + &2)) end |
718 | | - ) |
719 | | - |
720 | | - # Auto-subscribe to Redux changes |
721 | | - socket = |
722 | | - ReduxLV.assign_from_session(socket, session_id, %{ |
723 | | - user: fn state -> state.user end, |
724 | | - cart_count: cart_count_selector, |
725 | | - cart_total: cart_total_selector |
726 | | - }) |
727 | | - |
728 | | - {:ok, assign(socket, session_id: session_id)} |
729 | | - else |
730 | | - {:ok, assign(socket, session_id: session_id, user: nil, cart_count: 0, cart_total: 0)} |
731 | | - end |
732 | | - end |
733 | | - |
734 | | - # Handle automatic Redux assign updates |
735 | | - def handle_info({:redux_assign_update, key, value}, socket) do |
736 | | - {:noreply, ReduxLV.handle_assign_update(socket, key, value)} |
737 | | - end |
738 | | - |
739 | | - def handle_event("add_item", %{"item" => item}, socket) do |
740 | | - ReduxLV.dispatch_to_session(socket.assigns.session_id, {:add_to_cart, item}) |
741 | | - {:noreply, socket} |
742 | | - end |
743 | | - |
744 | | - def render(assigns) do |
745 | | - ~H\"\"\" |
746 | | - <div> |
747 | | - <h2>Welcome, <%= @user.name %></h2> |
748 | | - <p>Cart: <%= @cart_count %> items</p> |
749 | | - <p>Total: $<%= @cart_total %></p> |
750 | | - </div> |
751 | | - \"\"\" |
752 | | - end |
753 | | -end |
754 | | -``` |
755 | | -
|
756 | | -**Redux Features:** |
757 | | -- **Time-travel debugging** - Access complete action history |
758 | | -- **Middleware support** - Add logging, validation, side effects |
759 | | -- **Subscriptions** - React to specific state changes with callbacks |
760 | | -- **Selectors with memoization** - Efficient derived state computation |
761 | | -- **LiveView integration** - Automatic assign updates |
762 | | -- **State persistence** - Serialize and restore state |
763 | | -- **Predictable updates** - All changes through explicit actions |
764 | | -- **Comprehensive telemetry** - Monitor Redux operations |
765 | | -
|
766 | | -**Best for:** Complex applications, team collaboration, debugging requirements, state persistence needs, real-time reactive UIs. |
767 | | -
|
768 | | -### Comparison |
769 | 471 |
|
770 | | -| Feature | Basic GenServer | Agent State | Redux | |
771 | | -|---------|----------------|-------------|-------| |
772 | | -| Complexity | Low | Very Low | Medium | |
773 | | -| Performance | Excellent | Excellent | Good | |
774 | | -| Debugging | Manual | Manual | Built-in | |
775 | | -| Time-travel | No | No | Yes | |
776 | | -| Middleware | Manual | No | Yes | |
777 | | -| State History | No | No | Yes | |
778 | | -| Learning Curve | Low | Very Low | Medium | |
779 | 472 |
|
780 | | -See [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md) for detailed Redux migration guide and examples. |
781 | 473 |
|
782 | 474 | ## Configuration Options |
783 | 475 |
|
@@ -806,13 +498,6 @@ The library emits comprehensive telemetry events for monitoring and debugging: |
806 | 498 | - `[:phoenix, :session_process, :cleanup]` - When a session is cleaned up |
807 | 499 | - `[:phoenix, :session_process, :cleanup_error]` - When cleanup fails |
808 | 500 |
|
809 | | -### Redux State Management Events |
810 | | -- `[:phoenix, :session_process, :redux, :dispatch]` - When a Redux action is dispatched |
811 | | -- `[:phoenix, :session_process, :redux, :subscribe]` - When a subscription is created |
812 | | -- `[:phoenix, :session_process, :redux, :unsubscribe]` - When a subscription is removed |
813 | | -- `[:phoenix, :session_process, :redux, :notification]` - When subscriptions are notified |
814 | | -- `[:phoenix, :session_process, :redux, :selector_cache_hit]` - When selector cache is hit |
815 | | -- `[:phoenix, :session_process, :redux, :selector_cache_miss]` - When selector cache misses |
816 | 501 |
|
817 | 502 | ### Example Telemetry Setup |
818 | 503 |
|
|
0 commit comments