A simple and modest library to implement Flux/Redux pattern in .NET Blazor.
If you are not familiar with the Flux/Redux model, have a look on the following nice resources:
You can find diagrams here to help you understand.
Blazor does not contain a native Flux/Redux or MVU api internally.
The objective of this library is to provide a very simple and minimalist api to implement a Flux/Redux architecture.
This repository is largely inspired by the following existing repositories:
If you are not satisfied by this library, don't hesitate to check them out, they are great.
Compared to the listed existing libraries, Blaztore has the following advantages:
- ✅ Focused on immutability for every concepts (State, Action, ...)
- ✅ Never force you to inherit from a base class or a base record. Every concepts are based on interfaces. It allows you to structure your code as you like (multiple handling, file structure, ...)
- ✅ Use the underlying MediatR library to dispatch actions. It is highly extendable and allows you to easily implement pipeline or preprocessing to add loggers, retry pattern, ...
- ✅ Use the Flux/Redux terminology and not a custom one.
- ✅ Enable to store multiple instances of the same state type, identified by a unique id.
You can download the latest release NuGet packages from the official Blaztor nuget pages.
You can find below examples to illustrate how to implement concepts with Blazstore.
A state represents the data of a particular component. In .NET record is largely recommended for state immutability.
public record TaskCreationState(bool IsAddingTask, string? NewTaskDescription) : IGlobalState
{
// Mandatory static factory method to create the initial state.
public static TaskCreationState Initialize() => new(false, null);
}
Actions are messages that can represent a command to mutate the system or an event that happened in the system.
You must implement IAction<TState>
to explicitly define for which state is this action.
public record StartAddingNewTask : IAction<TaskCreationState>;
public record DefineNewDescription(string NewDescription) : IAction<TaskCreationState>;
public record TaskListLoaded(IReadOnlyCollection<TaskListItem> Payload) : IAction<TaskListState>;
A base component BlaztoreComponentBase<TState>
is provided to easily access the Dispatch(IAction<TState> action)
and GetState<TState>()
method.
@inherits BlaztoreComponentBase<TaskCreationState>
@code {
private TaskCreationState State => GetState<TaskCreationState>();
protected override Task OnAfterInitialRenderAsync() =>
Dispatch(new TaskCreationState.Load());
}
Use the State
to render you html elements :
<input value="@State.NewTaskDescription" ... />
Use Dispatch()
to execute command.
<button onclick="@(() => Dispatch(new StartAddingNewTask()))">
New task
</button>
A pure Reducer is a function that execute an action on a state, returning a new state. Theoretically, it should not have any dependencies and generates no side effects.
public record StartAddingNewTaskReducer(IStore Store)
: IPureReducer<TaskCreationState, StartAddingNewTask>
{
public TaskCreationState Reduce(TaskCreationState state, StartAddingNewTask action) =>
state with
{
IsAddingTask = true
};
}
You can organize you reducers as you prefer: a reducer for each action or a single reducer for all your actions.
public record TaskCreationStateReducer(IStore Store)
: IPureReducer<TaskCreationState, StartAddingNewTask>,
IPureReducer<TaskCreationState, EndAddingNewTask>
{
public TaskCreationState Reduce(TaskCreationState state, StartAddingNewTask action) =>
state with
{
IsAddingTask = true
};
public TaskCreationState Reduce(TaskCreationState state, EndAddingNewTask action) =>
state with
{
NewTaskDescription = null,
IsAddingTask = false
};
}
An Effect allows you to execute side effects on external system and dispatching new actions.
public record ExecuteTaskCreationEffect(
IStore Store,
ITodoListApi Api,
IActionDispatcher ActionDispatcher
) : IEffect<TaskCreationState, ExecuteTaskCreation>
{
public async Task Effect(TaskCreationState state, ExecuteTaskCreation action)
{
if (string.IsNullOrWhiteSpace(state.NewTaskDescription))
{
return;
}
await Api.Create(Guid.NewGuid(), state.NewTaskDescription);
await ActionDispatcher.Dispatch(new EndAddingNewTask());
await ActionDispatcher.Dispatch(new TodoListState.Load());
}
}
To see full examples go here or here.
You can find an example app in Blazor Wasm with Blaztor implementation here.
You have 3 interfaces to determine the scope of your states:
IGlobalState
: a state globally instanciated (singleton). The same instance is used for all components requiring it.IScopedState<TScope>
: a state locally instanciated, depending on an explicit scope. Components using the same scope shares the same state instance, different scopes, different state instance.IComponentState
: a state that is transient to a component, that means unique per component. All components have a different instance of the state.
By default, a state is destroyed once all components are destroyed. If you want a state to stay alive you can implement IPersistentLifecycleState
.
By default, state are instanciated once the first component requires it. If an action is executed but no state have been created yet, the action is ignored.
If you want to allow your actions to instanciate a default state, you can make your state implement IStateInstanciableFromActionExecution
.
- refactor: rename
StateComponent
toBlaztoreComponentBase
(remove the StateComponent concept) - feat: introducing a redux gateway for each state access scopes (
IGlobalStateReduxGateway<TGlobalState>
,IScopedStateReduxGateway<TScopedState>
,IComponentSTateReduxGateway<TComponentState>
). It allows to have their own method signature to ensure to not forget scope parameter. - feat: add
OnParametersChangedAfterComponentRendered()
virtual method onExtendedComponentBase
- fix: issues on
OnParametersChangedAsync()
- feat: better lifecycle management for states
- fix: missing dependency injection configuration for IActionEventDispatcher
- fix: IEffect was retrieving wrong state (not scoped when action was scoped)
- doc: add readme and assets