Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/Odonno/ReduxSimple

Simple Stupid Redux Store using Reactive Extensions
https://github.com/Odonno/ReduxSimple

csharp dispatch dotnet effects history memoized-selectors reactive-programming reducers redux redux-store selectors state state-management

Last synced: about 2 months ago
JSON representation

Simple Stupid Redux Store using Reactive Extensions

Awesome Lists containing this project

README

        

![./images/logo.png](./images/logo.png)

# Redux Simple

[![CodeFactor](https://www.codefactor.io/repository/github/odonno/reduxsimple/badge)](https://www.codefactor.io/repository/github/odonno/reduxsimple)

| Package | Versions |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| ReduxSimple | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.svg)](https://www.nuget.org/packages/ReduxSimple/) |
| ReduxSimple.Entity | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.Entity.svg)](https://www.nuget.org/packages/ReduxSimple.Entity/) |
| ReduxSimple.Uwp | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.Uwp.svg)](https://www.nuget.org/packages/ReduxSimple.Uwp/) |
| ReduxSimple.Uwp.RouterStore | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.Uwp.RouterStore.svg)](https://www.nuget.org/packages/ReduxSimple.Uwp.RouterStore/) |
| ReduxSimple.Uwp.DevTools | [![NuGet](https://img.shields.io/nuget/v/ReduxSimple.Uwp.DevTools.svg)](https://www.nuget.org/packages/ReduxSimple.Uwp.DevTools/) |

> Simple Stupid Redux Store using Reactive Extensions

Redux 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.

## Example app

There 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.

You can follow this link: https://www.microsoft.com/store/apps/9PDBXGFZCVMS

## Getting started

Like 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`.

In your app, you can:

- `Dispatch` new `Action` to change the `State`
- and listen to events/changes using the `Subscribe` method

You will need to follow the following steps to create your own Redux Store:

1. Create `State` definition

```csharp
public record RootState
{
public string CurrentPage { get; set; } = string.Empty;
public ImmutableArray Pages { get; set; } = ImmutableArray.Empty;
}
```

Each State should be immutable. That's why we prefer to use immutable types for each property of the State.

2. Create `Action` definitions

```csharp
public class NavigateAction
{
public string PageName { get; set; }
}

public class GoBackAction { }

public class ResetAction { }
```

3. Create `Reducer` functions

```csharp
public static class Reducers
{
public static IEnumerable> CreateReducers()
{
return new List>
{
On(
(state, action) => state with { Pages = state.Pages.Add(action.PageName) }
),
On(
state =>
{
var newPages = state.Pages.RemoveAt(state.Pages.Length - 1);

return state with {
CurrentPage = newPages.LastOrDefault(),
Pages = newPages
};
}
),
On(
state => state with {
CurrentPage = string.Empty,
Pages = ImmutableArray.Empty
}
)
};
}
}
```

4. Create a new instance of your Store

```csharp
sealed partial class App
{
public static readonly ReduxStore Store;

static App()
{
Store = new ReduxStore(CreateReducers());
}
}
```

5. And be ready to use your store inside your entire application...

## Features

Dispatch & Subscribe

You can now dispatch new actions using your globally accessible `Store`.

```csharp
using static MyApp.App; // static reference on top of your file

Store.Dispatch(new NavigateAction { PageName = "Page1" });
Store.Dispatch(new NavigateAction { PageName = "Page2" });
Store.Dispatch(new GoBackAction());
```

And subscribe to either state changes or actions raised.

```csharp
using static MyApp.App; // static reference on top of your file

Store.ObserveAction().Subscribe(_ =>
{
// TODO : Handle navigation
});

Store.Select(state => state.CurrentPage)
.Where(currentPage => currentPage == nameof(Page1))
.UntilDestroyed(this)
.Subscribe(_ =>
{
// TODO : Handle event when the current page is now "Page1"
});
```

Reducers

Reducers are pure functions used to create a new `state` once an `action` is triggered.

### Reducers on action

You can define a list of `On` functions where at least one action can be triggered.

```csharp
return new List>
{
On(
(state, action) => state with { Pages = state.Pages.Add(action.PageName) }
),
On(
state =>
{
var newPages = state.Pages.RemoveAt(state.Pages.Length - 1);

return state with {
CurrentPage = newPages.LastOrDefault(),
Pages = newPages
};
}
),
On(
state => state with {
CurrentPage = string.Empty,
Pages = ImmutableArray.Empty
}
)
};
```

### Sub-reducers aka feature reducers

Sub-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.

The `CreateSubReducers` function helps you to create sub-reducers. This function has a few requirements:

- a `Selector` - to be able to access the value of the current nested state
- a `Reducer` - to explicitly detail how to update the parent state given a new value for the nested state
- and the list of reducers using `On` pattern

First you need to create a new state lens for feature/nested states:

```csharp
public static IEnumerable> GetReducers()
{
return CreateSubReducers(SelectCounterState)
.On(state => state with { Count = state.Count + 1 })
.On(state => state with { Count = state.Count - 1 })
.ToList();
}
```

Then you can combine nested reducers into your root state:

```csharp
public static IEnumerable> CreateReducers()
{
return CombineReducers(
Counter.Reducers.GetReducers(),
TicTacToe.Reducers.GetReducers(),
TodoList.Reducers.GetReducers(),
Pokedex.Reducers.GetReducers()
);
}
```

And so inject your reducers into the Store:

```csharp
public static readonly ReduxStore Store =
new ReduxStore(CreateReducers(), RootState.InitialState);
```

Remember that following this pattern, you can have an infinite number of layers for your state.

Selectors

Based on what you need, you can observe the entire state or just a part of it.

Note 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.

### Full state

```csharp
Store.Select()
.Subscribe(state =>
{
// Listening to the full state (when any property changes)
});
```

### Inline function

You can use functions to select a part of the state, like this:

```csharp
Store.Select(state => state.CurrentPage)
.Subscribe(currentPage =>
{
// Listening to the "CurrentPage" property of the state (when only this property changes)
});
```

### Simple selectors

Simple 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.

```csharp
public static ISelectorWithoutProps SelectCurrentPage = CreateSelector(
(RootState state) => state.CurrentPage
);
public static ISelectorWithoutProps> SelectPages = CreateSelector(
(RootState state) => state.Pages
);

Store.Select(SelectCurrentPage)
.Subscribe(currentPage =>
{
// Listening to the "CurrentPage" property of the state (when only this property changes)
});
```

### Reuse selectors - without props

Note that you can combine multiple selectors to create a new one.

```csharp
public static ISelectorWithoutProps SelectHasPreviousPage = CreateSelector(
SelectPages,
(ImmutableArray pages) => pages.Count() > 1
);
```

### Reuse selectors - with props

You can also use variables out of the store to create a new selector.

```csharp
public static ISelectorWithProps SelectIsPageSelected = CreateSelector(
SelectCurrentPage,
(string currentPage, string selectedPage) => currentPage == selectedPage
);
```

And then use it this way:

```csharp
Store.Select(SelectIsPageSelected, "mainPage")
.Subscribe(isMainPageSelected =>
{
// TODO
});
```

### Combine selectors

Sometimes, 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:

```csharp
Store.Select(
CombineSelectors(SelectGameEnded, SelectWinner)
)
.Subscribe(x =>
{
var (gameEnded, winner) = x;

// TODO
});
```

Effects - Asynchronous Actions

Side effects are functions that runs outside of the predictable State -> UI cycle. Effects does not interfere with the UI directly and can dispatch a new action in the `ReduxStore` when necessary.

### The 3-actions pattern

When you work with asynchronous tasks (side effects), you can follow the following rule:

- Create 3 actions - a start action, a `fulfilled` action and a `failed` action
- Reduce/Handle response on `fulfilled` action
- Reduce/Handle error on `failed` action

Here is a concrete example.

```csharp
public class GetTodosAction { }
public class GetTodosFulfilledAction
{
public ImmutableList Todos { get; set; }
}
public class GetTodosFailedAction
{
public int StatusCode { get; set; }
public string Reason { get; set; }
}
```

```csharp
Store.Dispatch(new GetTodosAction());
```

### Create and register effect

You now need to observe this action and execute an HTTP call that will then dispatch the result to the store.

```csharp
public static Effect GetTodos = CreateEffect(
() => Store.ObserveAction()
.Select(_ =>
_todoApi.GetTodos()
.Select(todos =>
{
return new GetTodosFulfilledAction
{
Todos = todos.ToImmutableList()
};
})
.Catch(e =>
{
return Observable.Return(
new GetTodosFailedAction
{
StatusCode = e.StatusCode,
Reason = e.Reason
}
);
})
)
.Switch(),
true // indicates if the ouput of the effect should be dispatched to the store
);
```

And remember to always register your effect to the store.

```csharp
Store.RegisterEffects(
GetTodos
);
```

Time travel

By default, `ReduxStore` only support the default behavior which is a forward-only state.
You can however set `enableTimeTravel` to `true` in order to debug your application with some interesting features: handling `Undo` and `Redo` actions.

### Enable time travel

```csharp
sealed partial class App
{
public static readonly ReduxStore Store;

static App()
{
Store = new ReduxStore(CreateReducers(), true);
}
}
```

### Go back in time...

When the Store contains stored actions (ie. actions of the past), you can go back in time.

```csharp
if (Store.CanUndo)
{
Store.Undo();
}
```

It will then fires an `UndoneAction` event you can subscribe to.

```csharp
Store.Select()
.Subscribe(_ =>
{
// TODO : Handle event when the State changed
// You can observe the previous state generated or...
});

Store.ObserveUndoneAction()
.Subscribe(_ =>
{
// TODO : Handle event when an Undo event is triggered
// ...or you can observe actions undone
});
```

### ...And then rewrite history

Once you got back in time, you have two choices:

1. Start a new timeline
2. Stay on the same timeline of events

#### Start a new timeline

Once you dispatched a new action, the new `State` is updated and the previous timeline is erased from history: all previous actions are gone.

```csharp
// Dispatch the next actions
Store.Dispatch(new NavigateAction { PageName = "Page1" });
Store.Dispatch(new NavigateAction { PageName = "Page2" });

if (Store.CanUndo)
{
// Go back in time (Page 2 -> Page 1)
Store.Undo();
}

// Dispatch a new action (Page 1 -> Page 3)
Store.Dispatch(new NavigateAction { PageName = "Page3" });
```

#### Stay on the same timeline of events

You can stay o nthe same timeline by dispatching the same set of actions you did previously.

```csharp
// Dispatch the next actions
Store.Dispatch(new NavigateAction { PageName = "Page1" });
Store.Dispatch(new NavigateAction { PageName = "Page2" });

if (Store.CanUndo)
{
// Go back in time (Page 2 -> Page 1)
Store.Undo();
}

if (Store.CanRedo)
{
// Go forward (Page 1 -> Page 2)
Store.Redo();
}
```

Reset state

You can also reset the entire `Store` (reset current state and list of actions) by using the following method.

```csharp
Store.Reset();
```

You can then handle the reset event on your application.

```csharp
Store.ObserveReset()
.Subscribe(_ =>
{
// TODO : Handle event when the Store is reset
// (example: flush navigation history and restart from login page)
});
```

Entity management (in preview)

When 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:

1. Start creating an `EntityState` and an `EntityAdapter`

```csharp
public record TodoItemEntityState : EntityState
{
}

public static class Entities
{
public static EntityAdapter TodoItemAdapter = EntityAdapter.Create(item => item.Id);
}
```

2. Use the `EntityState` in your state

```csharp
public record TodoListState
{
public TodoItemEntityState Items { get; set; }
public TodoFilter Filter { get; set; }
}
```

3. Then use the `EntityAdapter` in reducers

```csharp
On(
(state, action) =>
{
return state with
{
Items = TodoItemAdapter.UpsertOne(new { action.Id, Completed = true }, state.Items)
};
}
)
```

4. And use the `EntityAdapter` in selectors

```csharp
private static readonly ISelectorWithoutProps SelectItemsEntityState = CreateSelector(
SelectTodoListState,
state => state.Items
);
private static readonly EntitySelectors TodoItemSelectors = TodoItemAdapter.GetSelectors(SelectItemsEntityState);
```

```csharp
public static ISelectorWithoutProps> SelectItems = TodoItemSelectors.SelectEntities;
```

Router (in preview)

You can observe router changes in your own state. You first need to create a State which inherits from `IBaseRouterState`.

```csharp
public class RootState : IBaseRouterState
{
public RouterState Router { get; set; }

public static RootState InitialState =>
new RootState
{
Router = RouterState.InitialState
};
}
```

#### For UWP

In order to get router information, you need to enable the feature like this (in `App.xaml.cs`):

```csharp
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
// TODO : Initialize rootFrame

// Enable router store feature
Store.EnableRouterFeature(rootFrame);
}
```

Redux DevTools (in preview)

![./images/devtools.PNG](./images/devtools.PNG)

Sometimes, it can be hard to debug your application. So there is a perfect tool called Redux DevTools which help you with that:

- list all dispatched actions
- payload of the action and details of the new state after dispatch
- differences between previous and next state
- replay mechanism (time travel)

#### For UWP

In order to make the Redux DevTools work, you need to enable time travel.

```csharp
public static readonly ReduxStore Store =
new ReduxStore(CreateReducers(), RootState.InitialState, true);
```

And then display the Redux DevTools view using a separate window.

```csharp
await Store.OpenDevToolsAsync();
```