{"id":13451207,"url":"https://github.com/Odonno/ReduxSimple","last_synced_at":"2025-03-23T18:32:02.602Z","repository":{"id":45332278,"uuid":"118379987","full_name":"Odonno/ReduxSimple","owner":"Odonno","description":"Simple Stupid Redux Store using Reactive Extensions","archived":false,"fork":false,"pushed_at":"2021-12-20T16:25:49.000Z","size":1875,"stargazers_count":143,"open_issues_count":16,"forks_count":20,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-03-02T06:04:45.161Z","etag":null,"topics":["csharp","dispatch","dotnet","effects","history","memoized-selectors","reactive-programming","reducers","redux","redux-store","selectors","state","state-management"],"latest_commit_sha":null,"homepage":"http://www.nuget.org/packages/ReduxSimple/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Odonno.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-01-21T22:36:16.000Z","updated_at":"2024-12-26T19:27:39.000Z","dependencies_parsed_at":"2022-08-28T01:51:58.249Z","dependency_job_id":null,"html_url":"https://github.com/Odonno/ReduxSimple","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Odonno%2FReduxSimple","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Odonno%2FReduxSimple/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Odonno%2FReduxSimple/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Odonno%2FReduxSimple/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Odonno","download_url":"https://codeload.github.com/Odonno/ReduxSimple/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245149439,"owners_count":20568906,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","dispatch","dotnet","effects","history","memoized-selectors","reactive-programming","reducers","redux","redux-store","selectors","state","state-management"],"created_at":"2024-07-31T07:00:49.776Z","updated_at":"2025-03-23T18:31:57.587Z","avatar_url":"https://github.com/Odonno.png","language":"C#","funding_links":[],"categories":["C# #"],"sub_categories":[],"readme":"![./images/logo.png](./images/logo.png)\n\n# Redux Simple\n\n[![CodeFactor](https://www.codefactor.io/repository/github/odonno/reduxsimple/badge)](https://www.codefactor.io/repository/github/odonno/reduxsimple)\n\n| Package                     | Versions                                                                                                                                |\n| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |\n| ReduxSimple                 | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.svg)](https://www.nuget.org/packages/ReduxSimple/)                                 |\n| ReduxSimple.Entity          | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.Entity.svg)](https://www.nuget.org/packages/ReduxSimple.Entity/)                   |\n| ReduxSimple.Uwp             | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.Uwp.svg)](https://www.nuget.org/packages/ReduxSimple.Uwp/)                         |\n| ReduxSimple.Uwp.RouterStore | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.Uwp.RouterStore.svg)](https://www.nuget.org/packages/ReduxSimple.Uwp.RouterStore/) |\n| ReduxSimple.Uwp.DevTools    | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.Uwp.DevTools.svg)](https://www.nuget.org/packages/ReduxSimple.Uwp.DevTools/)       |\n\n\u003e Simple Stupid Redux Store using Reactive Extensions\n\nRedux Simple is a .NET library based on [Redux](https://redux.js.org/) principle. Redux Simple is written with Rx.NET and built with the minimum of code you need to scale your whatever .NET application you want to design.\n\n## Example app\n\nThere is a sample UWP application to show how ReduxSimple library can be used and the steps required to make a C#/XAML application using the Redux pattern.\n\nYou can follow this link: https://www.microsoft.com/store/apps/9PDBXGFZCVMS\n\n## Getting started\n\nLike the original Redux library, you will have to initialize a new `State` when creating a `Store` + you will create `Reducer` functions each linked to an `Action` which will possibly update this `State`.\n\nIn your app, you can:\n\n- `Dispatch` new `Action` to change the `State`\n- and listen to events/changes using the `Subscribe` method\n\nYou will need to follow the following steps to create your own Redux Store:\n\n1. Create `State` definition\n\n```csharp\npublic record RootState\n{\n    public string CurrentPage { get; set; } = string.Empty;\n    public ImmutableArray\u003cstring\u003e Pages { get; set; } = ImmutableArray\u003cstring\u003e.Empty;\n}\n```\n\nEach State should be immutable. That's why we prefer to use immutable types for each property of the State.\n\n2. Create `Action` definitions\n\n```csharp\npublic class NavigateAction\n{\n    public string PageName { get; set; }\n}\n\npublic class GoBackAction { }\n\npublic class ResetAction { }\n```\n\n3. Create `Reducer` functions\n\n```csharp\npublic static class Reducers\n{\n    public static IEnumerable\u003cOn\u003cRootState\u003e\u003e CreateReducers()\n    {\n        return new List\u003cOn\u003cRootState\u003e\u003e\n        {\n            On\u003cNavigateAction, RootState\u003e(\n                (state, action) =\u003e state with { Pages = state.Pages.Add(action.PageName) }\n            ),\n            On\u003cGoBackAction, RootState\u003e(\n                state =\u003e\n                {\n                    var newPages = state.Pages.RemoveAt(state.Pages.Length - 1);\n\n                    return state with {\n                        CurrentPage = newPages.LastOrDefault(),\n                        Pages = newPages\n                    };\n                }\n            ),\n            On\u003cResetAction, RootState\u003e(\n                state =\u003e state with {\n                    CurrentPage = string.Empty,\n                    Pages = ImmutableArray\u003cstring\u003e.Empty\n                }\n            )\n        };\n    }\n}\n```\n\n4. Create a new instance of your Store\n\n```csharp\nsealed partial class App\n{\n    public static readonly ReduxStore\u003cRootState\u003e Store;\n\n    static App()\n    {\n        Store = new ReduxStore\u003cRootState\u003e(CreateReducers());\n    }\n}\n```\n\n5. And be ready to use your store inside your entire application...\n\n## Features\n\n\u003cdetails\u003e\n\u003csummary\u003eDispatch \u0026 Subscribe\u003c/summary\u003e\n\u003cbr\u003e\n\nYou can now dispatch new actions using your globally accessible `Store`.\n\n```csharp\nusing static MyApp.App; // static reference on top of your file\n\nStore.Dispatch(new NavigateAction { PageName = \"Page1\" });\nStore.Dispatch(new NavigateAction { PageName = \"Page2\" });\nStore.Dispatch(new GoBackAction());\n```\n\nAnd subscribe to either state changes or actions raised.\n\n```csharp\nusing static MyApp.App; // static reference on top of your file\n\nStore.ObserveAction\u003cNavigateAction\u003e().Subscribe(_ =\u003e\n{\n    // TODO : Handle navigation\n});\n\nStore.Select(state =\u003e state.CurrentPage)\n    .Where(currentPage =\u003e currentPage == nameof(Page1))\n    .UntilDestroyed(this)\n    .Subscribe(_ =\u003e\n    {\n        // TODO : Handle event when the current page is now \"Page1\"\n    });\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eReducers\u003c/summary\u003e\n\u003cbr\u003e\n\nReducers are pure functions used to create a new `state` once an `action` is triggered.\n\n### Reducers on action\n\nYou can define a list of `On` functions where at least one action can be triggered.\n\n```csharp\nreturn new List\u003cOn\u003cRootState\u003e\u003e\n{\n    On\u003cNavigateAction, RootState\u003e(\n        (state, action) =\u003e state with { Pages = state.Pages.Add(action.PageName) }\n    ),\n    On\u003cGoBackAction, RootState\u003e(\n        state =\u003e\n        {\n            var newPages = state.Pages.RemoveAt(state.Pages.Length - 1);\n\n            return state with {\n                CurrentPage = newPages.LastOrDefault(),\n                Pages = newPages\n            };\n        }\n    ),\n    On\u003cResetAction, RootState\u003e(\n        state =\u003e state with {\n            CurrentPage = string.Empty,\n            Pages = ImmutableArray\u003cstring\u003e.Empty\n        }\n    )\n};\n```\n\n### Sub-reducers aka feature reducers\n\nSub-reducers also known as feature reducers are nested reducers that are used to update a part of the state. They are mainly used in larger applications to split state and reducer logic in multiple parts.\n\nThe `CreateSubReducers` function helps you to create sub-reducers. This function has a few requirements:\n\n- a `Selector` - to be able to access the value of the current nested state\n- a `Reducer` - to explicitly detail how to update the parent state given a new value for the nested state\n- and the list of reducers using `On` pattern\n\nFirst you need to create a new state lens for feature/nested states:\n\n```csharp\npublic static IEnumerable\u003cOn\u003cRootState\u003e\u003e GetReducers()\n{\n    return CreateSubReducers(SelectCounterState)\n        .On\u003cIncrementAction\u003e(state =\u003e state with { Count = state.Count + 1 })\n        .On\u003cDecrementAction\u003e(state =\u003e state with { Count = state.Count - 1 })\n        .ToList();\n}\n```\n\nThen you can combine nested reducers into your root state:\n\n```csharp\npublic static IEnumerable\u003cOn\u003cRootState\u003e\u003e CreateReducers()\n{\n    return CombineReducers(\n        Counter.Reducers.GetReducers(),\n        TicTacToe.Reducers.GetReducers(),\n        TodoList.Reducers.GetReducers(),\n        Pokedex.Reducers.GetReducers()\n    );\n}\n```\n\nAnd so inject your reducers into the Store:\n\n```csharp\npublic static readonly ReduxStore\u003cRootState\u003e Store =\n    new ReduxStore\u003cRootState\u003e(CreateReducers(), RootState.InitialState);\n```\n\nRemember that following this pattern, you can have an infinite number of layers for your state.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSelectors\u003c/summary\u003e\n\u003cbr\u003e\n\nBased on what you need, you can observe the entire state or just a part of it.\n\nNote that every selector is a _memoized selector_ by design, which means that a next value will only be subscribed if there is a difference with the previous value.\n\n### Full state\n\n```csharp\nStore.Select()\n    .Subscribe(state =\u003e\n    {\n        // Listening to the full state (when any property changes)\n    });\n```\n\n### Inline function\n\nYou can use functions to select a part of the state, like this:\n\n```csharp\nStore.Select(state =\u003e state.CurrentPage)\n    .Subscribe(currentPage =\u003e\n    {\n        // Listening to the \"CurrentPage\" property of the state (when only this property changes)\n    });\n```\n\n### Simple selectors\n\nSimple selectors are like functions but the main benefits are that they can be reused in multiple components and they can be reused to create other selectors.\n\n```csharp\npublic static ISelectorWithoutProps\u003cRootState, string\u003e SelectCurrentPage = CreateSelector(\n    (RootState state) =\u003e state.CurrentPage\n);\npublic static ISelectorWithoutProps\u003cRootState, ImmutableArray\u003cstring\u003e\u003e SelectPages = CreateSelector(\n    (RootState state) =\u003e state.Pages\n);\n\nStore.Select(SelectCurrentPage)\n    .Subscribe(currentPage =\u003e\n    {\n        // Listening to the \"CurrentPage\" property of the state (when only this property changes)\n    });\n```\n\n### Reuse selectors - without props\n\nNote that you can combine multiple selectors to create a new one.\n\n```csharp\npublic static ISelectorWithoutProps\u003cRootState, bool\u003e SelectHasPreviousPage = CreateSelector(\n    SelectPages,\n    (ImmutableArray\u003cstring\u003e pages) =\u003e pages.Count() \u003e 1\n);\n```\n\n### Reuse selectors - with props\n\nYou can also use variables out of the store to create a new selector.\n\n```csharp\npublic static ISelectorWithProps\u003cRootState, string, bool\u003e SelectIsPageSelected = CreateSelector(\n    SelectCurrentPage,\n    (string currentPage, string selectedPage) =\u003e currentPage == selectedPage\n);\n```\n\nAnd then use it this way:\n\n```csharp\nStore.Select(SelectIsPageSelected, \"mainPage\")\n    .Subscribe(isMainPageSelected =\u003e\n    {\n        // TODO\n    });\n```\n\n### Combine selectors\n\nSometimes, you need to consume multiple selectors. In some cases, you just want to combine them. This is what you can do with `CombineSelectors` function. Here is an example:\n\n```csharp\nStore.Select(\n    CombineSelectors(SelectGameEnded, SelectWinner)\n)\n    .Subscribe(x =\u003e\n    {\n        var (gameEnded, winner) = x;\n\n        // TODO\n    });\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eEffects - Asynchronous Actions\u003c/summary\u003e\n\u003cbr\u003e\n\nSide effects are functions that runs outside of the predictable State -\u003e UI cycle. Effects does not interfere with the UI directly and can dispatch a new action in the `ReduxStore` when necessary.\n\n### The 3-actions pattern\n\nWhen you work with asynchronous tasks (side effects), you can follow the following rule:\n\n- Create 3 actions - a start action, a `fulfilled` action and a `failed` action\n- Reduce/Handle response on `fulfilled` action\n- Reduce/Handle error on `failed` action\n\nHere is a concrete example.\n\n```csharp\npublic class GetTodosAction { }\npublic class GetTodosFulfilledAction\n{\n    public ImmutableList\u003cTodo\u003e Todos { get; set; }\n}\npublic class GetTodosFailedAction\n{\n    public int StatusCode { get; set; }\n    public string Reason { get; set; }\n}\n```\n\n```csharp\nStore.Dispatch(new GetTodosAction());\n```\n\n### Create and register effect\n\nYou now need to observe this action and execute an HTTP call that will then dispatch the result to the store.\n\n```csharp\npublic static Effect\u003cRootState\u003e GetTodos = CreateEffect\u003cRootState\u003e(\n    () =\u003e Store.ObserveAction\u003cGetTodosAction\u003e()\n        .Select(_ =\u003e\n            _todoApi.GetTodos()\n                .Select(todos =\u003e\n                {\n                    return new GetTodosFulfilledAction\n                    {\n                        Todos = todos.ToImmutableList()\n                    };\n                })\n                .Catch(e =\u003e\n                {\n                    return Observable.Return(\n                        new GetTodosFailedAction\n                        {\n                            StatusCode = e.StatusCode,\n                            Reason = e.Reason\n                        }\n                    );\n                })\n        )\n        .Switch(),\n    true // indicates if the ouput of the effect should be dispatched to the store\n);\n```\n\nAnd remember to always register your effect to the store.\n\n```csharp\nStore.RegisterEffects(\n    GetTodos\n);\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eTime travel\u003c/summary\u003e\n\u003cbr\u003e\n\nBy default, `ReduxStore` only support the default behavior which is a forward-only state.\nYou can however set `enableTimeTravel` to `true` in order to debug your application with some interesting features: handling `Undo` and `Redo` actions.\n\n### Enable time travel\n\n```csharp\nsealed partial class App\n{\n    public static readonly ReduxStore\u003cRootState\u003e Store;\n\n    static App()\n    {\n        Store = new ReduxStore\u003cRootState\u003e(CreateReducers(), true);\n    }\n}\n```\n\n### Go back in time...\n\nWhen the Store contains stored actions (ie. actions of the past), you can go back in time.\n\n```csharp\nif (Store.CanUndo)\n{\n    Store.Undo();\n}\n```\n\nIt will then fires an `UndoneAction` event you can subscribe to.\n\n```csharp\nStore.Select()\n    .Subscribe(_ =\u003e\n    {\n        // TODO : Handle event when the State changed\n        // You can observe the previous state generated or...\n    });\n\nStore.ObserveUndoneAction()\n    .Subscribe(_ =\u003e\n    {\n        // TODO : Handle event when an Undo event is triggered\n        // ...or you can observe actions undone\n    });\n```\n\n### ...And then rewrite history\n\nOnce you got back in time, you have two choices:\n\n1. Start a new timeline\n2. Stay on the same timeline of events\n\n#### Start a new timeline\n\nOnce you dispatched a new action, the new `State` is updated and the previous timeline is erased from history: all previous actions are gone.\n\n```csharp\n// Dispatch the next actions\nStore.Dispatch(new NavigateAction { PageName = \"Page1\" });\nStore.Dispatch(new NavigateAction { PageName = \"Page2\" });\n\nif (Store.CanUndo)\n{\n    // Go back in time (Page 2 -\u003e Page 1)\n    Store.Undo();\n}\n\n// Dispatch a new action (Page 1 -\u003e Page 3)\nStore.Dispatch(new NavigateAction { PageName = \"Page3\" });\n```\n\n#### Stay on the same timeline of events\n\nYou can stay o nthe same timeline by dispatching the same set of actions you did previously.\n\n```csharp\n// Dispatch the next actions\nStore.Dispatch(new NavigateAction { PageName = \"Page1\" });\nStore.Dispatch(new NavigateAction { PageName = \"Page2\" });\n\nif (Store.CanUndo)\n{\n    // Go back in time (Page 2 -\u003e Page 1)\n    Store.Undo();\n}\n\nif (Store.CanRedo)\n{\n    // Go forward (Page 1 -\u003e Page 2)\n    Store.Redo();\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eReset state\u003c/summary\u003e\n\u003cbr\u003e\n\nYou can also reset the entire `Store` (reset current state and list of actions) by using the following method.\n\n```csharp\nStore.Reset();\n```\n\nYou can then handle the reset event on your application.\n\n```csharp\nStore.ObserveReset()\n    .Subscribe(_ =\u003e\n    {\n        // TODO : Handle event when the Store is reset\n        // (example: flush navigation history and restart from login page)\n    });\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eEntity management (in preview)\u003c/summary\u003e\n\u003cbr\u003e\n\nWhen dealing with entities, you often repeat the same process to add, update and remove entity from your collection state. With the `ReduxSimple.Entity` package, you can simplify the management of entities using the following pattern:\n\n1. Start creating an `EntityState` and an `EntityAdapter`\n\n```csharp\npublic record TodoItemEntityState : EntityState\u003cint, TodoItem\u003e\n{\n}\n\npublic static class Entities\n{\n    public static EntityAdapter\u003cint, TodoItem\u003e TodoItemAdapter = EntityAdapter\u003cint, TodoItem\u003e.Create(item =\u003e item.Id);\n}\n```\n\n2. Use the `EntityState` in your state\n\n```csharp\npublic record TodoListState\n{\n    public TodoItemEntityState Items { get; set; }\n    public TodoFilter Filter { get; set; }\n}\n```\n\n3. Then use the `EntityAdapter` in reducers\n\n```csharp\nOn\u003cCompleteTodoItemAction, TodoListState\u003e(\n    (state, action) =\u003e\n    {\n        return state with\n        {\n            Items = TodoItemAdapter.UpsertOne(new { action.Id, Completed = true }, state.Items)\n        };\n    }\n)\n```\n\n4. And use the `EntityAdapter` in selectors\n\n```csharp\nprivate static readonly ISelectorWithoutProps\u003cRootState, TodoItemEntityState\u003e SelectItemsEntityState = CreateSelector(\n    SelectTodoListState,\n    state =\u003e state.Items\n);\nprivate static readonly EntitySelectors\u003cRootState, int, TodoItem\u003e TodoItemSelectors = TodoItemAdapter.GetSelectors(SelectItemsEntityState);\n```\n\n```csharp\npublic static ISelectorWithoutProps\u003cRootState, List\u003cTodoItem\u003e\u003e SelectItems = TodoItemSelectors.SelectEntities;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eRouter (in preview)\u003c/summary\u003e\n\u003cbr\u003e\n\nYou can observe router changes in your own state. You first need to create a State which inherits from `IBaseRouterState`.\n\n```csharp\npublic class RootState : IBaseRouterState\n{\n    public RouterState Router { get; set; }\n\n    public static RootState InitialState =\u003e\n        new RootState\n        {\n            Router = RouterState.InitialState\n        };\n}\n```\n\n#### For UWP\n\nIn order to get router information, you need to enable the feature like this (in `App.xaml.cs`):\n\n```csharp\nprotected override void OnLaunched(LaunchActivatedEventArgs e)\n{\n    // TODO : Initialize rootFrame\n\n    // Enable router store feature\n    Store.EnableRouterFeature(rootFrame);\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eRedux DevTools (in preview)\u003c/summary\u003e\n\u003cbr\u003e\n\n![./images/devtools.PNG](./images/devtools.PNG)\n\nSometimes, it can be hard to debug your application. So there is a perfect tool called Redux DevTools which help you with that:\n\n- list all dispatched actions\n- payload of the action and details of the new state after dispatch\n- differences between previous and next state\n- replay mechanism (time travel)\n\n#### For UWP\n\nIn order to make the Redux DevTools work, you need to enable time travel.\n\n```csharp\npublic static readonly ReduxStore\u003cRootState\u003e Store =\n    new ReduxStore\u003cRootState\u003e(CreateReducers(), RootState.InitialState, true);\n```\n\nAnd then display the Redux DevTools view using a separate window.\n\n```csharp\nawait Store.OpenDevToolsAsync();\n```\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOdonno%2FReduxSimple","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FOdonno%2FReduxSimple","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOdonno%2FReduxSimple/lists"}