diff --git a/Images/flux-pattern.jpg b/Images/flux-pattern.jpg new file mode 100644 index 00000000..ae8936d3 Binary files /dev/null and b/Images/flux-pattern.jpg differ diff --git a/README.md b/README.md index 5b849382..1158704e 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,20 @@ You can download the latest release / pre-release NuGet packages from the offici * [Fluxor.Blazor.Web](https://www.nuget.org/packages/Fluxor.Blazor.Web) [![NuGet version (Fluxor)](https://img.shields.io/nuget/v/Fluxor.Blazor.Web.svg?style=flat-square)](https://www.nuget.org/packages/Fluxor.Blazor.Web/) * [Fluxor.Blazor.Web.ReduxDevTools](https://www.nuget.org/packages/Fluxor.Blazor.Web.ReduxDevTools) [![NuGet version (Fluxor.Blazor.Web.ReduxDevTools)](https://img.shields.io/nuget/v/Fluxor.Blazor.Web.ReduxDevTools.svg?style=flat-square)](https://www.nuget.org/packages/Fluxor.Blazor.Web.ReduxDevTools/) +## Flux pattern + +Often confused with Redux. Redux is the name of a library, Flux is the name of the pattern that Redux and +Fluxor implement. + +![](./Images/flux-pattern.jpg) + +* State should always be read-only. +* To alter state our app should dispatch an action. +* The store runs the action through every registered reducer. +* Every reducer that processes the dispatched action type will create new state +from the existing state, along with any changes intended by the dispatched action. +* The UI then uses the new state to render its display. + # Licence [MIT](https://opensource.org/licenses/MIT) diff --git a/Samples/Blazor/02EffectsSample/EffectsSample/Client/Pages/FetchData.razor b/Samples/Blazor/02EffectsSample/EffectsSample/Client/Pages/FetchData.razor index fc9fdc93..f82c1032 100644 --- a/Samples/Blazor/02EffectsSample/EffectsSample/Client/Pages/FetchData.razor +++ b/Samples/Blazor/02EffectsSample/EffectsSample/Client/Pages/FetchData.razor @@ -21,17 +21,14 @@ else - @if (WeatherState.Value.Forecasts != null) + @foreach (var forecast in WeatherState.Value.Forecasts) { - foreach (var forecast in WeatherState.Value.Forecasts) - { - - @forecast.Date.ToShortDateString() - @forecast.TemperatureC - @forecast.TemperatureF - @forecast.Summary - - } + + @forecast.Date.ToShortDateString() + @forecast.TemperatureC + @forecast.TemperatureF + @forecast.Summary + } diff --git a/Samples/Blazor/02EffectsSample/EffectsSample/Client/Store/WeatherUseCase/WeatherState.cs b/Samples/Blazor/02EffectsSample/EffectsSample/Client/Store/WeatherUseCase/WeatherState.cs index 8154cfef..4be892f5 100644 --- a/Samples/Blazor/02EffectsSample/EffectsSample/Client/Store/WeatherUseCase/WeatherState.cs +++ b/Samples/Blazor/02EffectsSample/EffectsSample/Client/Store/WeatherUseCase/WeatherState.cs @@ -1,4 +1,5 @@ using FluxorBlazorWeb.EffectsSample.Shared; +using System; using System.Collections.Generic; namespace FluxorBlazorWeb.EffectsSample.Client.Store.WeatherUseCase @@ -11,7 +12,7 @@ public class WeatherState public WeatherState(bool isLoading, IEnumerable forecasts) { IsLoading = isLoading; - Forecasts = forecasts; + Forecasts = forecasts ?? Array.Empty(); } } } diff --git a/Samples/Blazor/02EffectsSample/README.md b/Samples/Blazor/02EffectsSample/README.md new file mode 100644 index 00000000..b57530fa --- /dev/null +++ b/Samples/Blazor/02EffectsSample/README.md @@ -0,0 +1,227 @@ +# Fluxor - Blazor Web Samples + +## Effects + +Flux state is supposed to be immutable, and that state replaced only by +[pure functions](https://en.wikipedia.org/wiki/Pure_function), which should only take input from their +parameters. + +With this in mind, we need something that will enable us to access other sources of data such as +web services, and then reduce the results into our state. + +### Goal +This tutorial will recreate the `Fetch data` page in a standard Blazor app. + +### Steps + +- Under the `Store` folder, create a new folder named `WeatherUseCase`. +- Create a new state class to hold the state for this use case. + +```c# +public class WeatherState +{ + public bool IsLoading { get; } + public IEnumerable Forecasts { get; } + + public WeatherState(bool isLoading, IEnumerable forecasts) + { + IsLoading = isLoading; + Forecasts = forecasts ?? Array.Empty(); + } +} +``` + +This state holds a property indicating whether or not the data is currently being retrieved from +the server, and an enumerable holding zero to many `WeatherForecast` objects. + +*Note: Again, the state is immutable* + +- Create a new class named `Feature`. This class describes the state to the store. + +```c# +public class Feature : Feature +{ + public override string GetName() => "Weather"; + protected override WeatherState GetInitialState() => + new WeatherState( + isLoading: false, + forecasts: null); +} +``` + +#### Displaying state in the component + +- Find the `Pages` folder and add a new file named `FetchData.razor.cs` +- Mark the class `partial`. +- Add the following `using` declarations + +```c# +using Fluxor; +using Microsoft.AspNetCore.Components; +using YourAppName.Store.WeatherUseCase; +``` + +- Next we need to inject the `WeatherState` into our component + +```c# +public partial class FetchData +{ + [Inject] + private IState WeatherState { get; set; } +} +``` + +- Edit `FetchData.razor` and make the page descend from `FluxorComponent`. + +``` +@inherits Fluxor.Blazor.Web.Components.FluxorComponent +``` + +- Change the mark-up so it uses our `IsLoading` state to determine if data is being +retrieved from the server or not. + +Change + +`@if (forecasts == null)` + +to + +`@if (WeatherState.Value.IsLoading)` + +- Change the mark-up so it uses our `Forecasts` state. + +Change + +`@foreach (var forecast in forecasts)` + +to + +`@foreach (var forecast in WeatherState.Value.Forecasts)` + +- Remove `@inject WeatherForecastService ForecastService` + +#### Using an Action and a Reducer to alter state + +- Create an empty class `FetchDataAction`. +- Create a static `Reducers` class, which will set `IsLoading` to true when our +`FetchDataAction` action is dispatched. + +```c# +public static class Reducers +{ + [ReducerMethod] + public static WeatherState ReduceFetchDataAction(WeatherState state, FetchDataAction action) => + new WeatherState( + isLoading: true, + forecasts: null); +} +``` + +- In `Fetchdata.razor.cs` inject `IDispatcher` and dispatch our action from the `OnInitialized` +lifecycle method. The code-behind class should now look like this + +```c# +public partial class FetchData +{ + [Inject] + private IState WeatherState { get; set; } + + [Inject] + private IDispatcher Dispatcher { get; set; } + + protected override void OnInitialized() + { + base.OnInitialized(); + Dispatcher.Dispatch(new FetchDataAction()); + } +} +``` + +#### Requesting data from the server via an `Effect` + +Effect handlers do not (and cannot) affect state directly. They are triggered when the action they are +waiting for is dispatched through the store, and as a response they can dispatch new actions. + +Effect handlers can be written in one of two ways. + +1. By descending from the `Effect` class. The name of the class is unimportant. + +```c# +public class FetchDataActionEffect : Effect +{ + private readonly HttpClient Http; + + public FetchDataActionEffect(HttpClient http) + { + Http = http; + } + + protected override async Task HandleAsync(FetchDataAction action, IDispatcher dispatcher) + { + var forecasts = await Http.GetJsonAsync("WeatherForecast"); + dispatcher.Dispatch(new FetchDataResultAction(forecasts)); + } +} +``` + +2. By decorating instance or static methods with `[EffectMethod]`. The name of the class and the +method are unimportant. + +```c# +public class Effects +{ + private readonly HttpClient Http; + + public Effects(HttpClient http) + { + Http = http; + } + + [EffectMethod] + public async Task HandleFetchDataAction(FetchDataAction action, IDispatcher dispatcher) + { + var forecasts = await Http.GetJsonAsync("WeatherForecast"); + dispatcher.Dispatch(new FetchDataResultAction(forecasts)); + } +} +``` + +Both techniques work equally well, which you choose is an organisational choice. However, if your effect +requires lots of (or unique) dependencies then you should consider having the handling method in its +own class for simplicity (still either approach #1 or #2 may be used). + +#### Reducing the `Effect` result into state + +- Create a new class `FetchDataResultAction`, which will hold the results of the call to the server +so they can be "reduced" into our application state. + +```c# +public class FetchDataResultAction +{ + public IEnumerable Forecasts { get; } + + public FetchDataResultAction(IEnumerable forecasts) + { + Forecasts = forecasts; + } +} +``` + +This is the action that is dispatched by our `Effect` earlier, after it has retrieved the data from +the server via an HTTP request. + +- Edit the `Reducers.cs` class and add a new `[ReducerMethod]` to reduce the contents of this result +action into state. + +```c# +[ReducerMethod] +public static WeatherState ReduceFetchDataResultAction(WeatherState state, FetchDataResultAction action) => + new WeatherState( + isLoading: false, + forecasts: action.Forecasts); +``` + +This reducer simply sets the `IsLoading` state back to false, and sets the `Forecasts` state to the +values in the action that was dispatched by our effect. + + diff --git a/Source/Fluxor.sln b/Source/Fluxor.sln index ba5387ed..4600235c 100644 --- a/Source/Fluxor.sln +++ b/Source/Fluxor.sln @@ -35,6 +35,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluxorBlazorWeb.CounterSample", "..\Samples\Blazor\01CounterSample\CounterSample\FluxorBlazorWeb.CounterSample.csproj", "{E4E667AC-8CFE-4632-99D0-56DD69117DD2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02 EffectsSample", "02 EffectsSample", "{730B26CD-CADC-4CF0-8A1D-3BB174692A40}" + ProjectSection(SolutionItems) = preProject + ..\Samples\Blazor\02EffectsSample\README.md = ..\Samples\Blazor\02EffectsSample\README.md + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluxorBlazorWeb.EffectsSample.Server", "..\Samples\Blazor\02EffectsSample\EffectsSample\Server\FluxorBlazorWeb.EffectsSample.Server.csproj", "{01E13581-FD37-4595-A9DC-37BFAA77864F}" EndProject