Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs for Sample 02 - Effects #3

Merged
merged 15 commits into from
Mar 18, 2020
Binary file added Images/flux-pattern.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,14 @@ else
</tr>
</thead>
<tbody>
@if (WeatherState.Value.Forecasts != null)
@foreach (var forecast in WeatherState.Value.Forecasts)
{
foreach (var forecast in WeatherState.Value.Forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FluxorBlazorWeb.EffectsSample.Shared;
using System;
using System.Collections.Generic;

namespace FluxorBlazorWeb.EffectsSample.Client.Store.WeatherUseCase
Expand All @@ -11,7 +12,7 @@ public class WeatherState
public WeatherState(bool isLoading, IEnumerable<WeatherForecast> forecasts)
{
IsLoading = isLoading;
Forecasts = forecasts;
Forecasts = forecasts ?? Array.Empty<WeatherForecast>();
}
}
}
227 changes: 227 additions & 0 deletions Samples/Blazor/02EffectsSample/README.md
Original file line number Diff line number Diff line change
@@ -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<WeatherForecast> Forecasts { get; }

public WeatherState(bool isLoading, IEnumerable<WeatherForecast> forecasts)
{
IsLoading = isLoading;
Forecasts = forecasts ?? Array.Empty<WeatherForecast>();
}
}
```

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<WeatherState>
{
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> 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> 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<TAction>` class. The name of the class is unimportant.

```c#
public class FetchDataActionEffect : Effect<FetchDataAction>
{
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[]>("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[]>("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<WeatherForecast> Forecasts { get; }

public FetchDataResultAction(IEnumerable<WeatherForecast> 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.


3 changes: 3 additions & 0 deletions Source/Fluxor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down