Use Fluxor for state management

This commit is contained in:
Neil Brommer 2021-12-03 16:44:02 -08:00
parent 437e90039f
commit 560c25b4e8
27 changed files with 823 additions and 210 deletions

View file

@ -0,0 +1,42 @@
using System.Collections.Generic;
using Start.Shared;
namespace Start.Client.Store.Features.ContainersList {
/// <summary>Dispatch before sending an API request</summary>
public class FetchContainerListAction { }
/// <summary>Dispatch after recieving the container list from an API request</summary>
public class RecievedContainerListAction {
public IList<BookmarkContainerDto> Containers { get; set; }
public RecievedContainerListAction(IList<BookmarkContainerDto> containers) {
this.Containers = containers;
}
}
public class ErrorFetchingContainerListAction {
public string ErrorMessage { get; set; }
public ErrorFetchingContainerListAction(string errorMessage) {
this.ErrorMessage = errorMessage;
}
}
public class LoadContainerListAction { }
public class RemoveContainerFromListAction {
public int ContainerIdToRemove { get; set; }
public RemoveContainerFromListAction(int containerIdToRemove) {
this.ContainerIdToRemove = containerIdToRemove;
}
}
public class AddContainerToListAction {
public BookmarkContainerDto NewContainer { get; set; }
public AddContainerToListAction(BookmarkContainerDto newContainer) {
this.NewContainer = newContainer;
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Fluxor;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Refit;
using Start.Shared;
using Start.Shared.Api;
namespace Start.Client.Store.Features.ContainersList {
public class ContainerListEffects {
public IBookmarkContainersApi BookmarkContainersApi { get; init; }
public ContainerListEffects(IBookmarkContainersApi bookmarkContainersApi) {
this.BookmarkContainersApi = bookmarkContainersApi;
}
[EffectMethod(typeof(LoadContainerListAction))]
public async Task LoadContainerList(IDispatcher dispatch) {
dispatch.Dispatch(new FetchContainerListAction());
try {
ApiResponse<IEnumerable<BookmarkContainerDto>> response = await this
.BookmarkContainersApi
.GetAllBookmarkContainers();
List<BookmarkContainerDto>? bookmarkContainers = response.Content?.ToList();
if (bookmarkContainers == null) {
dispatch.Dispatch(new ErrorFetchingContainerListAction(
"Failed to fetch containers list"));
return;
}
if (!bookmarkContainers.Any())
throw new NotImplementedException("Create bookmark effect has not been created");
dispatch.Dispatch(new RecievedContainerListAction(bookmarkContainers));
}
catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
}
}

View file

@ -0,0 +1,63 @@
using System.Collections.Immutable;
using System.Linq;
using Fluxor;
using Start.Client.Store.State;
using Start.Shared;
namespace Start.Client.Store.Features.ContainersList {
public static class ContainerListReducers {
[ReducerMethod(typeof(FetchContainerListAction))]
public static RootState OnFetchContainerList(RootState state) {
return state with {
ContainerListState = state.ContainerListState with {
Containers = ImmutableList<BookmarkContainerDto>.Empty,
IsLoadingContainersList = true
}
};
}
[ReducerMethod]
public static RootState OnRecievedContainerList(RootState state,
RecievedContainerListAction action) {
return state with {
ContainerListState = state.ContainerListState with {
Containers = action.Containers.ToImmutableList(),
IsLoadingContainersList = false,
ErrorMessage = null
}
};
}
[ReducerMethod]
public static RootState OnErrorFetchingContainerList(RootState state,
ErrorFetchingContainerListAction action) {
return state with {
ContainerListState = state.ContainerListState with {
ErrorMessage = action.ErrorMessage
}
};
}
[ReducerMethod]
public static RootState AddContainerToList(RootState state,
AddContainerToListAction action) {
return state with {
ContainerListState = state.ContainerListState with {
Containers = state.ContainerListState.Containers.Add(action.NewContainer)
}
};
}
[ReducerMethod]
public static RootState RemoveContainerFromList(RootState state,
RemoveContainerFromListAction action) {
return state with {
ContainerListState = state.ContainerListState with {
Containers = state.ContainerListState.Containers
.Where(c => c.BookmarkContainerId != action.ContainerIdToRemove)
.ToImmutableList()
}
};
}
}
}

View file

@ -0,0 +1,27 @@
using Start.Shared;
namespace Start.Client.Store.Features.CreateContainer {
public class ShowCreateContainerFormAction { }
public class HideCreateContainerFormAction { }
public class FetchCreateContainerAction { }
public class ReceivedCreateContainerAction { }
public class ErrorFetchingCreateContainerAction {
public string ErrorMessage { get; set; }
public ErrorFetchingCreateContainerAction(string errorMessage) {
this.ErrorMessage = errorMessage;
}
}
public class SubmitCreateContainerAction {
public BookmarkContainerDto NewContainer { get; set; }
public SubmitCreateContainerAction(BookmarkContainerDto container) {
this.NewContainer = container;
}
}
}

View file

@ -0,0 +1,43 @@
using System.Threading.Tasks;
using Fluxor;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Refit;
using Start.Shared;
using Start.Shared.Api;
using Start.Client.Store.Features.CurrentContainer;
using Start.Client.Store.Features.ContainersList;
namespace Start.Client.Store.Features.CreateContainer {
public class CreateContainerEffects {
public IBookmarkContainersApi BookmarkContainersApi { get; set; }
public CreateContainerEffects(IBookmarkContainersApi bookmarkContainersApi) {
this.BookmarkContainersApi = bookmarkContainersApi;
}
[EffectMethod]
public async Task SubmitCreateContainer(SubmitCreateContainerAction action,
IDispatcher dispatch) {
dispatch.Dispatch(new FetchCreateContainerAction());
try {
ApiResponse<BookmarkContainerDto?> apiResponse = await this.BookmarkContainersApi
.CreateBookmarkContainer(action.NewContainer.Title);
BookmarkContainerDto? container = apiResponse.Content;
if (container == null)
dispatch.Dispatch(new ErrorFetchingCreateContainerAction(
"Failed to create container"));
else {
dispatch.Dispatch(new AddContainerToListAction(container));
dispatch.Dispatch(new ReceivedCreateContainerAction());
dispatch.Dispatch(new LoadCurrentContainerAction(
container.BookmarkContainerId));
}
} catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
}
}

View file

@ -0,0 +1,12 @@
using Fluxor;
using Start.Client.Store.State;
namespace Start.Client.Store.Features.CreateContainer {
public class CreateContainerFeature : Feature<CreateContainerState> {
public override string GetName() => "Create Container";
protected override CreateContainerState GetInitialState() {
return new CreateContainerState();
}
}
}

View file

@ -0,0 +1,44 @@
using Fluxor;
namespace Start.Client.Store.Features.CreateContainer {
public static class CreateContainerReducers {
[ReducerMethod(typeof(ShowCreateContainerFormAction))]
public static CreateContainerState ShowCreateContainerForm(CreateContainerState state) {
return state with {
ShowCreateContainerForm = true
};
}
[ReducerMethod(typeof(HideCreateContainerFormAction))]
public static CreateContainerState HideCreateContainerForm(CreateContainerState state) {
return state with {
ShowCreateContainerForm = false
};
}
[ReducerMethod(typeof(FetchCreateContainerAction))]
public static CreateContainerState FetchCreateContainer(CreateContainerState state) {
return state with {
IsLoadingCreateContainer = true
};
}
[ReducerMethod(typeof(ReceivedCreateContainerAction))]
public static CreateContainerState ReceivedCreateContainer(CreateContainerState state) {
return state with {
IsLoadingCreateContainer = false,
CreateContainerErrorMessage = null,
ShowCreateContainerForm = false
};
}
[ReducerMethod]
public static CreateContainerState ErrorFetchingCreateContainer(CreateContainerState state,
ErrorFetchingCreateContainerAction action) {
return state with {
IsLoadingCreateContainer = false,
CreateContainerErrorMessage = action.ErrorMessage
};
}
}
}

View file

@ -0,0 +1,20 @@
using Start.Client.Store.State;
namespace Start.Client.Store.Features.CreateContainer {
public record CreateContainerState : RootState {
public bool ShowCreateContainerForm { get; init; }
public bool IsLoadingCreateContainer { get; init; }
public string? CreateContainerErrorMessage { get; init; }
public CreateContainerState() { }
public CreateContainerState(ContainerListState containerList,
CurrentContainerState currentContainer, bool showCreateContainer, bool isLoading,
string? errorMessage)
: base(containerList, currentContainer) {
this.ShowCreateContainerForm = showCreateContainer;
this.IsLoadingCreateContainer = isLoading;
this.CreateContainerErrorMessage = errorMessage;
}
}
}

View file

@ -0,0 +1,31 @@
using Start.Shared;
namespace Start.Client.Store.Features.CurrentContainer {
public class FetchCurrentContainerAction { }
public class ReceivedCurrentContainerAction {
public BookmarkContainerDto BookmarkContainer { get; init; }
public ReceivedCurrentContainerAction(BookmarkContainerDto bookmarkContainer) {
this.BookmarkContainer = bookmarkContainer;
}
}
public class ErrorFetchingCurrentContainerAction {
public string ErrorMessage { get; init; }
public ErrorFetchingCurrentContainerAction(string errorMessage) {
this.ErrorMessage = errorMessage;
}
}
public class LoadCurrentContainerAction {
public int BookmarkContainerId { get; init; }
public LoadCurrentContainerAction(int bookmarkContainerId) {
this.BookmarkContainerId = bookmarkContainerId;
}
}
public class FixCurrentContainerAction { }
}

View file

@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Blazored.LocalStorage;
using Fluxor;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Refit;
using Start.Client.Store.State;
using Start.Client.Store.Features.CreateContainer;
using Start.Shared;
using Start.Shared.Api;
namespace Start.Client.Store.Features.CurrentContainer {
public class CurrentContainerEffects {
public IBookmarkContainersApi BookmarkContainersApi { get; set; }
public ILocalStorageService LocalStorage { get; set; }
public IState<RootState> RootState { get; set; }
public CurrentContainerEffects(IBookmarkContainersApi bookmarkContainersApi,
ILocalStorageService localStorage, IState<RootState> rootState) {
this.BookmarkContainersApi = bookmarkContainersApi;
this.LocalStorage = localStorage;
this.RootState = rootState;
}
[EffectMethod]
public async Task LoadCurrentContainer(LoadCurrentContainerAction action,
IDispatcher dispatch) {
dispatch.Dispatch(new FetchCurrentContainerAction());
try {
ApiResponse<BookmarkContainerDto?> response = await this.BookmarkContainersApi
.GetBookmarkContainer(action.BookmarkContainerId);
BookmarkContainerDto? container = response.Content;
if (container == null) {
dispatch.Dispatch(new ErrorFetchingCurrentContainerAction(
"Failed to get current bookmark container"));
return;
}
dispatch.Dispatch(new ReceivedCurrentContainerAction(container));
await this.LocalStorage
.SetItemAsync("SelectedContainer", action.BookmarkContainerId);
} catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
[EffectMethod(typeof(FixCurrentContainerAction))]
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public async Task FixCurrentContainer(IDispatcher dispatch) {
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
if (!this.RootState.Value.ContainerListState.Containers.Any()) {
dispatch.Dispatch(new SubmitCreateContainerAction(
new BookmarkContainerDto("Default")));
return;
}
IEnumerable<int?> containerIds = this.RootState.Value.ContainerListState.Containers
.Select(c => (int?)c.BookmarkContainerId);
int? currentContainerId = this.RootState.Value.CurrentContainerState.Container
?.BookmarkContainerId;
if (containerIds.Contains(currentContainerId))
return;
int firstContainerId = this.RootState.Value.ContainerListState.Containers
.First().BookmarkContainerId;
dispatch.Dispatch(new LoadCurrentContainerAction(firstContainerId));
}
}
}

View file

@ -0,0 +1,41 @@
using Fluxor;
using Start.Client.Store.State;
namespace Start.Client.Store.Features.CurrentContainer {
public static class CurrentContainerReducers {
[ReducerMethod(typeof(FetchCurrentContainerAction))]
public static RootState FetchCurrentContainer(RootState state) {
return state with {
CurrentContainerState = state.CurrentContainerState with {
Container = null,
IsLoadingCurrentContainer = true,
ErrorMessage = null
}
};
}
[ReducerMethod]
public static RootState ReceivedCurrentContainer(RootState state,
ReceivedCurrentContainerAction action) {
return state with {
CurrentContainerState = state.CurrentContainerState with {
Container = action.BookmarkContainer,
IsLoadingCurrentContainer = false,
ErrorMessage = null
}
};
}
[ReducerMethod]
public static RootState ErrorFetchingCurrentContainer(RootState state,
ErrorFetchingCurrentContainerAction action) {
return state with {
CurrentContainerState = state.CurrentContainerState with {
Container = null,
IsLoadingCurrentContainer = false,
ErrorMessage = action.ErrorMessage
}
};
}
}
}

View file

@ -0,0 +1,34 @@
namespace Start.Client.Store.Features.DeleteContainer {
public class ShowDeleteContainerFormAction {
public int ContainerIdToDelete { get; init; }
public string ContainerTitleToDelete { get; init; }
public ShowDeleteContainerFormAction(int containerIdToDelete,
string containerTitleToDelete) {
this.ContainerIdToDelete = containerIdToDelete;
this.ContainerTitleToDelete = containerTitleToDelete;
}
}
public class HideDeleteContainerFormAction { }
public class FetchDeleteContainerFormAction { }
public class SubmitDeleteContainerAction {
public int ContainerIdToDelete { get; init; }
public SubmitDeleteContainerAction(int containerIdToDelete) {
this.ContainerIdToDelete = containerIdToDelete;
}
}
public class RecievedDeleteContainerAction { }
public class ErrorFetchingDeleteContainerAction {
public string ErrorMessage { get; init; }
public ErrorFetchingDeleteContainerAction(string errorMessage) {
this.ErrorMessage = errorMessage;
}
}
}

View file

@ -0,0 +1,56 @@
using System.Threading.Tasks;
using Fluxor;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Start.Client.Store.Features.CurrentContainer;
using Start.Shared.Api;
using System.Net;
using Start.Client.Store.State;
using Start.Client.Store.Features.ContainersList;
namespace Start.Client.Store.Features.DeleteContainer {
public class DeleteContainerEffects {
public IBookmarkContainersApi BookmarkContainersApi { get; init; }
public IState<RootState> RootState { get; set; }
public DeleteContainerEffects(IBookmarkContainersApi bookmarkContainersApi,
IState<RootState> rootState) {
this.BookmarkContainersApi = bookmarkContainersApi;
this.RootState = rootState;
}
[EffectMethod]
public async Task SubmitDeleteContainer(SubmitDeleteContainerAction action,
IDispatcher dispatch) {
dispatch.Dispatch(new FetchDeleteContainerFormAction());
try {
System.Net.Http.HttpResponseMessage? apiResponse = await this.BookmarkContainersApi
.DeleteBookmarkContainer(action.ContainerIdToDelete);
if (apiResponse == null) {
dispatch.Dispatch(
new ErrorFetchingDeleteContainerAction("Failed to submit request"));
return;
}
if (apiResponse.StatusCode == HttpStatusCode.NotFound) {
dispatch.Dispatch(new ErrorFetchingDeleteContainerAction(
"The bookmark container to delete doesn't exist"));
return;
}
if (!apiResponse.IsSuccessStatusCode) {
dispatch.Dispatch(new ErrorFetchingDeleteContainerAction(
"There was an error deleting the bookmark container"));
return;
}
dispatch.Dispatch(new RemoveContainerFromListAction(action.ContainerIdToDelete));
dispatch.Dispatch(new FixCurrentContainerAction());
dispatch.Dispatch(new RecievedDeleteContainerAction());
} catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
}
}

View file

@ -0,0 +1,11 @@
using Fluxor;
namespace Start.Client.Store.Features.DeleteContainer {
public class DeleteContainerFeature : Feature<DeleteContainerState> {
public override string GetName() => "Delete Container";
protected override DeleteContainerState GetInitialState() {
return new DeleteContainerState();
}
}
}

View file

@ -0,0 +1,50 @@
using Fluxor;
namespace Start.Client.Store.Features.DeleteContainer {
public static class DeleteContainerReducers {
[ReducerMethod]
public static DeleteContainerState ShowDeleteContainerForm(DeleteContainerState state,
ShowDeleteContainerFormAction action) {
return state with {
ShowDeleteContainerForm = true,
BookmarkContainerIdToDelete = action.ContainerIdToDelete,
BookmarkContainerTitleToDelete = action.ContainerTitleToDelete,
DeleteContainerErrorMessage = null
};
}
[ReducerMethod(typeof(HideDeleteContainerFormAction))]
public static DeleteContainerState HideDeleteContainerForm(DeleteContainerState state) {
return state with {
ShowDeleteContainerForm = false,
DeleteContainerErrorMessage = null
};
}
[ReducerMethod(typeof(FetchDeleteContainerFormAction))]
public static DeleteContainerState FetchDeleteContainerForm(DeleteContainerState state) {
return state with {
IsLoadingDeleteContainer = true,
DeleteContainerErrorMessage = null
};
}
[ReducerMethod(typeof(RecievedDeleteContainerAction))]
public static DeleteContainerState ReceivedDeleteContainer(DeleteContainerState state) {
return state with {
IsLoadingDeleteContainer = false,
DeleteContainerErrorMessage = null,
ShowDeleteContainerForm = false
};
}
[ReducerMethod]
public static DeleteContainerState ErrorFetchingDeleteContainer(DeleteContainerState state,
ErrorFetchingDeleteContainerAction action) {
return state with {
DeleteContainerErrorMessage = action.ErrorMessage,
IsLoadingDeleteContainer = false
};
}
}
}

View file

@ -0,0 +1,28 @@
using Start.Client.Store.State;
namespace Start.Client.Store.Features.DeleteContainer {
public record DeleteContainerState : RootState {
public bool ShowDeleteContainerForm { get; set; }
public int BookmarkContainerIdToDelete { get; set; }
public string BookmarkContainerTitleToDelete { get; set; }
public bool IsLoadingDeleteContainer { get; set; }
public string? DeleteContainerErrorMessage { get; set; }
public DeleteContainerState() {
this.BookmarkContainerIdToDelete = 0;
this.BookmarkContainerTitleToDelete = "";
}
public DeleteContainerState(ContainerListState containerList,
CurrentContainerState currentContainer, bool showDeleteContainerForm,
int containerIdToDelete, string containerTitleToDelete, bool isLoadingDeleteContainer,
string? errorMessage)
: base(containerList, currentContainer) {
this.ShowDeleteContainerForm = showDeleteContainerForm;
this.BookmarkContainerIdToDelete = containerIdToDelete;
this.BookmarkContainerTitleToDelete = containerTitleToDelete;
this.IsLoadingDeleteContainer = isLoadingDeleteContainer;
this.DeleteContainerErrorMessage = errorMessage;
}
}
}

View file

@ -0,0 +1,59 @@
using System.Collections.Immutable;
using Fluxor;
using Start.Shared;
namespace Start.Client.Store.State {
[FeatureState]
public record RootState {
public ContainerListState ContainerListState { get; init; }
public CurrentContainerState CurrentContainerState { get; init; }
public RootState() {
this.ContainerListState = new ContainerListState();
this.CurrentContainerState = new CurrentContainerState();
}
public RootState(ContainerListState containerList, CurrentContainerState currentContainer) {
this.ContainerListState = containerList;
this.CurrentContainerState = currentContainer;
}
}
public record ContainerListState {
public ImmutableList<BookmarkContainerDto> Containers { get; init; }
public bool IsLoadingContainersList { get; init; }
public string? ErrorMessage { get; init; }
public ContainerListState() {
this.Containers = ImmutableList<BookmarkContainerDto>.Empty;
this.IsLoadingContainersList = false;
this.ErrorMessage = null;
}
public ContainerListState(ImmutableList<BookmarkContainerDto> containers,
bool isLoadingContainersList, string? errorMessage) {
this.Containers = containers;
this.IsLoadingContainersList = isLoadingContainersList;
this.ErrorMessage = errorMessage;
}
}
public record CurrentContainerState {
public BookmarkContainerDto? Container { get; init; }
public bool IsLoadingCurrentContainer { get; init; }
public string? ErrorMessage { get; init; }
public CurrentContainerState() {
this.Container = null;
this.IsLoadingCurrentContainer = false;
this.ErrorMessage = null;
}
public CurrentContainerState(BookmarkContainerDto? currentContainer,
bool isLoadingCurrentContainer, string? errorMessage) {
this.Container = currentContainer;
this.IsLoadingCurrentContainer = isLoadingCurrentContainer;
this.ErrorMessage = errorMessage;
}
}
}