While NgRx Auto-Entity can do a lot for you, there may be times when you need to perform more complex operations than are supported with the ready-made generic actions of Auto-Entity. In such situations, you can always fall back onto "classic" NgRx and leverage all the raw power it provides.
There are a few scenarios where you may need to fall back onto regular old NgRx. Purely custom functionality that doesn't fall into the "entity" mold may simply require a "full on NgRx." The top use case that comes to mind would be your average user login scenario, which often requires actions and server interactions that fall into more of a challenge/response sort of paradigm than an entity CRUD paradigm.
In the auth case, aside from just Load(User, id)
the actual act of Authentication will usually be a custom action, custom effect, probably a custom service method (although it is fine to add additional functionality to entity services if you so require! Entity services may be provided directly as well as via the model if necessary.)
There are a couple other scenarios where you may need to implement some custom functionality, however you may also still be able to leverage Auto-Entity to one degree or another. Such cases may include say a bundling complex behaviors such as a multi-entity save into a single action, and leveraging a custom effect to ultimately dispatch the necessary generic actions to work on each individual entity behind the scenes.
Another case may be when you need a workflow, where one action begets another action, and so on. In this case custom effects may be leveraged to watch for particular generic actions dispatched for particular entities, and in turn dispatch other generic or custom actions for other entities or behaviors. This is where the power of reactive programming really comes into play.
Doing more with less...
When developing complex applications, sometimes it may help to "bundle" multiple actions together or compose many individual actions into sort of a higher order action, an action of actions. Such actions may be handled by custom effects to enforce order-of-execution in a manner that may be much more challenging otherwise.
A simple use case might be the creation of an Order
that contains LineItem
s. Depending on the nature of the backend system you must work with, it may be that you must first create an empty order which returns its key, then populate that order, for which you now know the key, with items that relate back to the order. You may have certain timing issues here that are not necessarily easy to solve with the asynchronous nature of the web and NgRx.
Start by creating a custom action, createFullOrder
. This action will require both an Order entity as well as an array of LineItem
entities. We only need this one action, as we will ultimately leverage existing Auto-Entity actions down the line. Lets just put this new action in the state file for our order state:
With this action now in hand, we want to update our Order facade to support creation of a full order with its line items. We can simple add a new method to our facade that will facilitate this new behavior:
In order to leverage NgRx Auto-Entity here, we will need to create some effects that decompose our createFullOrder action into a generic Create action for the Order, then upon the success of said order creation dispatch additional generic Create actions for each LineItem.
This order of execution is important as creation of a LineItem entity requires knowledge of the Order they belong to, and the key for this order is only known once it is created. Achieving it can be a little tricky, but it tends to be easier in an effect than elsewhere:
Don't forget register your new OrderEffects
class with the NgRx EffectsModule
!
This new effect will now handle dispatching of Create
generic actions for our new Order
, then wait for a CreateSuccess
generic action from Auto-Entity to be dispatched for the Order
entity type, and upon said dispatch occurring will then dispatch additional Create
generic actions for each of the LineItem
s for that order.
This careful timing of dispatches may be achieved elsewhere, such as a component or a facade, however the interactions will generally be more complex and disjoint than when handled in a cohesive effect. Leverage the full power of NgRx when you need to!
This leads to that, which leads to...
An alternative use case to the bundling of actions or action composition, where related data must be handled with explicit order of execution, is workflows. A workflow is the use case where the result of one action may result in the dispatching of another, perhaps with derived data, perhaps with otherwise unrelated data.
Let's face it. Sometimes we have to work with odd data structures in order to make things work. One of the key tenants of NgRx is denormalization of data, so we may have multiple collections that contain entities related to each other.
A potential example use case here, however contrived it may be (although not necessarily unheard of, especially for those who work with existing, well-established and particularly older backends), might be tracking the things a user favorites with joiner records in a many-to-many table. Lets say we have a single-user app that needs to easily display this information. We may want to track an isFavorite
flag on our things, and update said flag whenever our joiner entities are created or deleted.
Unlike our full order creation example, where we needed to track a group of entities together and manage the order of execution for their creation, this use case requires that we chain one action off of the success of another.
We do not need any new custom actions for this use case, however we do need some effects. Let's say we have Post
s that a user may Like
. The actual likes are tracked as a join between the postId
and the userId
and are created and deleted as independent entities in the database. For simplicity of processing this data in our UI and UI performance, we track an wasLiked
flag on our Post
entities in state.
Don't forget register your new LikeEffects
class with the NgRx EffectsModule
!
These two effects are fairly strait forward. They look for the success of prior Create
or Delete
actions dispatched for Like
entities. They then grab all the posts currently loaded into state, find the post related to the like, and dispatch a LoadSuccess
. This may seem odd, however we do not actually wish to load a Post
from the database, we only wish to update the related Post
in state...the goal is efficiency and performance.
The Auto-Entity meta reducer handles LoadSuccess
in a specific way...it merges the specified entity into state. If it already exists, it is updated, if not it is added. Since creating a Like marks a post as liked for the specified user, we know we can set wasLiked
to true when a Like
is successfully created. Similarly, we know we can set wasLiked
to false when a Like
is successfully deleted.
In the event that you may need to update many entities at once, rather than just a single entity, a similar result may be achieved by dispatching LoadManySuccess
by including the array of updated entities.
Do not dispatch LoadAllSuccess
, as the semantics are different. The nature of LoadMany
means that when its success result is dispatched, the entities will be MERGED into the existing state. In contrast, the nature of LoadAll
means that when its success result is dispatched, the entities will REPLACE whatever is currently in state.
Further, if either the Create
or Delete
of a Like
fails, due to network connectivity issues, a server outage, etc. then we do not update the Post
in state, which might incorrectly reflect to the user their status for the given post.
Ok, now that we have the necessary effects to handle these state updates, let's update our facades to support the new behavior here. In fact...let's create a new facade of a special type, what we like to call a Manager
, to handle the composition of information from multiple entities into another.
This is just a matter of code organization, and for simpler applications adding the necessary method to an existing facade, say the PostFacade
. For larger applications, segregating out behavior that relies on multiple entities can help keep facades small and manageable.
We could try to simplify things here. We could, say, bring in the PostFacade
here as well, and right after creating our like we update the Post
and directly set the wasLiked
flag to true. We could...
Remember that NgRx is asynchronous. Dispatching an initiating action such as Create
or Delete
does not imply success, or even completion. It simply means you have requested a particular activity be performed. Said action's status is unknown until the result action, such as CreateSuccess
or DeleteFailure
, has subsequently been dispatched.
When we create something, such as our Like
joiner entity here, that dispatches the request to create the entity. This request may be fulfilled at a later time, but it is not guaranteed to be fulfilled by the time we get to the next line of code. It may in fact never be fulfilled depending on network or server conditions.
By creating a simple workflow with effects and subsequent action dispatches, we are able to enforce and maintain the integrity of our data in our application. We only update the wasLiked
flag once the CreateSuccess
or DeleteSuccess
actions for the liked Post
have been dispatched, something much harder to do in a facade or component.