Is it a good practice to allocate a Redux state dedicated to entity creation with a 'fire-and-forget' API? - reactjs

In several blogs video and so on, there's a CRUD tutorial with Redux.
None of them (AFAIK after surfing) deal with fully async API on servers, like a fire-and-forget behavior.
Main commands in a CQRS environment deal frequently with those kind of fire-and-forget.
Let's take a fictive example of Twitter to easily get the idea:
Basically, in a context of a synchronous CRUD API, you likely have:
Redux Action: POST_TWEET
Server API: returning the entire created tweet in the response data.
State: TweetReducer exploring and storing the created tweet's data from the response.
UI: listening to the new tweet from the Tweet state directly.
The TweetReducer, besides the classical fetching APIs, can fully handle the POST_TWEET action since it can encompass the new tweet directly.
However, in a context of fire-and-forget API on the server:
Redux Action: POST_TWEET
Server API: returning only the tweet's id (e.g Location) in the response.
State: TweetReducer does not handle the creation since the tweet is not available at the time of the success action triggering.
Thus a new Redux state dedicated to handle tweet creation labeled TweetCreation typically owning those properties: (data: {id: string}, inProgress: boolean, errors: []).
It would then grab the newly created tweet's id in the data and allow UI to listen to this state (TweetCreation).
UI: listening to TweetCreation state and hence displays that the tweet is sent or even tries to fetch the server at some time interval to get the full tweet.
Is it a good practice some people experiment to add another state on the Redux store to deal with fire-and-forget APIs?
Is it "conventional" in the community or is there another clever way?

1. Creating a separate state for pending tweets
For a start, you'd need to change your TweetCreation to an array in case the user makes a second tweet before the first is confirmed.
So your shape would look like this: { pendingTweets: [], confirmedTweets: [] }.
In POST_TWEET, you append the new tweet to pendingTweets.
In SET_TWEET_ID, you remove the matching tweet from pendingTweets and push it to confirmedTweets.
In your component, you probably do something like confirmedTweets.concat(pendingTweets).map(...).
2. Use the same state for pending tweets
The shape will just be { tweets: [] }.
In POST_TWEET, you append the new tweet to tweets.
In SET_TWEET_ID, you update the matching tweet in tweets.
In your component, you do tweets.map(...).
Conclusion
Using the same state for pending tweets seems like a simpler (and therefore better) approach.
Additional considerations (for both approaches)
I left out details about avoiding direct state mutations when updating since that's very basic.
You probably need to do something like generating a temporary id for the pending tweet and sending it back from the server so that you can find the matching tweet in SET_TWEET_ID.
The temporary id can use a different object key (or use an additional flag) so that you can distinguish between a pending and a confirmed tweet in the component (eg. to render a loading icon beside pending tweets).
Replacing [] with {} using id as object key might be better (depending on the exact requirements) but that's not the focus of this question.

Related

React-Query/SWR vs Global state (e.g Redux, Zustand) when updating a single post creator's details in an infinite list of posts

In my application, I have a paginated feed containing posts retrieved from the /feed endpoint.
Each feed post has...
postId
postTitle
postBody
postCreator (object)
Each postCreator object has...
userId
userName
userBio
userImageUrl
As suggested by Redux, I'm currently...
Extracting the postCreator objects and storing them in their own global usersStore (using Zustand)
Replacing the postCreator objects with the relevant postCreatorId (which references a user in the global usersStore) in each post
Storing the posts in their own postsStore (using Zustand)
The benefits of this are...
If a user updates their information (e.g. name or bio), we only have to update their record in the usersStore, and their information will be updated across the whole app (their post creator info in the feed posts UI, their profile image in the tab-bar etc)
We don't have to re-request the data from the /feed endpoint to get the posts containing the updated postCreator object. Especially considering that a user may have scrolled way down the feed at that point in time, so we may have to get hundreds of posts from the /feed endpoint, putting strain on our backend.
React-Query/SWR
I have recently discovered React Query and SWR and have heard a lot about how they "remove much of the need for global state", but I'm slightly confused by how they handle the above scenario. After doing some research on React-Query, I'm under the impression that there a few ways to handle this...
Still use the global usersStore and postStore to update the user's information in a single place, as described above.
Scrap the global usersStore, and update the postCreator information directly in React-Query's cache for the /feed infiniteQuery, using queryClient.setQueryData. But we would have to also update the other query caches where the user's information is returned, and what if the cache has expired?
Scrap the global usersStore, and refetch all of the posts from the /feed endpoint when a user updates their info. However, if a user has scrolled way down the feed at that point in time, we may have to get hundreds of posts from the /feed endpoint, putting strain on our backend. React-Query infinite query refetch docs
What is the best practice way to handle this using React-Query/SWR?
react-query doesn't have a normalized cache. That being said, mostly, there is no need to do any normalization - just keep what the api sends you as your state, retrieve it via useQuery, and get rid of all redux / zustand global stores that would otherwise hold your server state.
In your example, I see two queries:
useQuery(['user', id]) for the user data
useInfiniteQuery(['feed']) for the feed of posts
When rendering your feed, you'll likely render a Post component for each entry, which can in turn call the useQuery that retrieves the user data. If you are worried about the n+1 queries that are being made, and you are in fact already delivering the user-data with the feed, you can manually prime the user-cache in the onSuccess callback of the feed query - just take the user data and copy it to the user-cache.
That way, the user-cache becomes the single source of truth for user data, which also updates from the feed query.

React+Redux - managing state for autocomplete inputs

I have a large app with some autocomplete text-inputs that retrieve search suggestions from the backend on every key stroke.
I want to save recent search query results to avoid multiple backend calls for the same query if the user deletes characters. I also want to expire these queries after some period of time to keep search results fresh.
I also want to have a loading indicator for the current text input if the backend call isn't complete yet for that exact value.
The question is - where to manage this state and the actions associated with this state (below I have a sample state shape I need).
Managing it in the main Redux store looks like an overkill - this state is kind of temporary-intermediate state, it's very short-lived (queries should be expired after a while), there may be many instances of the same Component present on the screen, and different instances might use different backend calls.
Managing the recent search-queries and search-results in the local React Component state object - looks like a fine solution.
But now we have the backend calls which I don't want to fire from within the component, but go through the full-blown Flux process, with proper actions and reducers and passing the results (or errors) into props through the Store Connector.
So eventually, things here don't fit properly with each other - I don't want to manage the state in the main Redux store but I do want the backend-calls (results of which are the main part of that state) to go through the main reducers+store lifecycle.
Any advice for a robust, maintainable, and easy-to-figure-out-without-docs architecture is appreciated.
The state I need for every instance of this component looks something like:
(let's say I typed in dog, and the last result didn't come yet):
{
currentSearchInput: 'dog',
recentQueries: [
{
input: 'd',
isLoading: false,
results: [...]
},
{
input: 'do',
isLoading: false,
results: [...]
},
{
input: 'dog',
isLoading: true,
results: null
},
]
}
In my opinion, using the object structure noted above with the components internal state is just fine. Write a function that will accept the results of the HTTP Request that come back through Redux, and update the results field of the appropriate object in the array. Personally, I would store the promise in the results field. Each time componentWillReceiveProps gets called (due to your reducers returning new redux state) use a setState to update your recentQueries.
I'd also set a limit. 5 recent query max or something like that.
That being said, I use the google places API to return address/establishment suggestions and the data is so small it wasn't worth it to do this. Most of the results are under 10kb.
EDIT:
Based on our discussions in the comments, here is what I would suggest doing.
In the onChange handler for the input.
Pass the search criteria to the action as you normally would
In the action creator, return the results of the API call AND the search criteria
In the reducer
Check to see the length of the array that holds your search calls (the redux store value). If it is less than five, simply concat the current values and the new values and return. If its value, overwrite the 0th position of the array with the new content. e.g.
state[0] = action.newApiResult; return state;

what is this difference between this flux action and this function call?

I could a have a flux action like this:
{type: 'KILL', payload: {target: 'ogre'}}
But I am not seeing what the difference is between having a method on a class People (wrapping the store) like this,
People.kill('ogre')
IF People is the only receiver of the action?
I see that the flux dispatcher gives me two advantages (possibly)
The "kill" method can be broadcast to multiple unknown receivers (good!)
The dispatcher gives me a handy place to log all action traffic (also good!)
These might be good things sure, but is there any other reasons that I am missing?
What I don't see is how putting the actions in the form of JSON objects, suddenly enforces or helps with "1-way" communication flow, which is what I read everywhere is the big advantage of having actions, and of flux.
Looks to me like I am still effectively sending a message back to the store, no matter how I perfume the pig. Sure the action is now going through a couple of layers of indirection (action creator, dispatcher) before it gets to the store, but unless I am missing something the component that sends that action for all practical purposes is updating whatever stores are listening for the kill message.
What I am missing here?
Again I know on Stack Overflow we can't ask too general a question, so I want to keep this very specific. The two snippets of code while having different syntax, appear to be semantically (except for the possibility of broadcasting to multiple stores) exactly the same.
And again if the only reason is that it enables broadcasting and enables a single point of flow for debug purposes, I am fine with that, but would like to know if there is some other thing about flux/the dispatcher I am missing?
The major features of the flux-style architecture are roughly the following:
the store is the single source of truth for application state
only actions can trigger mutation of the store's state
store state should not be mutated directly, i.e. via assigning object values, but by creating new objects via cloning/destructuring instead
Like a diet, using this type of architecture really doesn't work if you slip and go back to the old ways intermittently.
Returning to your example. The benefit for using the action here is not broadcasting or logging aspects, but simply the fact that the People class should only be able to either consume data from a store and express its wishes to mutate the state of said store with actions. Imagine for example that Elves want to sing to the the ogre and thus are interested in knowing the said ogre is still alive. At the same time the People want to be polite and do not wish to kill the ogre while it is being serenaded. The benefits of the flux-style architecture are clear:
class People {
kill(creature) {
if (creatureStore.getSerenadedCreature() !== creature)
store.dispatch({ type: 'KILL', payload: { target: creature } })
return `The ${creature} is being serenaded by those damn elves, let's wait until they've finished.`
}
}
class Elves {
singTo(creature) {
if (!creatureStore.getCreatures().includes(creature))
return store.dispatch({ type: 'SING_TO', payload: { target: creature } })
return `Oh no, the ${creature} has been killed... I guess there will be no serenading tonight..`
}
}
If the class People were to wrap the store, you'd need the Elves class to wrap the same store as well, creating two places where the same state would be mutated in one way or the other. Now imagine if there were 10 other classes that need access to that store and want to change it: adding those new features is becoming a pain because all those classes are now at the mercy of the other classes mutating the state from underneath them, forcing you to handle tons of edge cases not possibly even related to the business logic of those classes.
With the flux style architecture, all those classes will only consume data from the creatureStore and dispatch actions based on that state. The store handles reconciling the different actions with the state so that all of its subscribers have the right data at the right times.
The benefits of this pattern may not be evident when you only have a couple of stores that are consumed by one or two entities each. When you have tens (or hundreds) of stores with tens (or hundreds) of components consuming data from several stores each, this architecture saves you time and money by making it easier to develop new features without breaking existing ones.
Hope this wall-o-text helped to clarify!
What I don't see is how putting the actions in the form of JSON objects, suddenly enforces or helps with "1-way" communication flow, which is what I read everywhere is the big advantage of having actions, and of flux.
Looks to me like I am still effectively sending a message back to the store, no matter how I perfume the pig. Sure the action is now going through a couple of layers of indirection (action creator, dispatcher) before it gets to the store, but unless I am missing something the component that sends that action for all practical purposes is updating whatever stores are listening for the kill message.
What I am missing here?
Facebook Flux took the idea from the event driven GUI systems.
In there even if you move your mouse you get messages. This was called message loop then, and now we have actions dispatching.
Also, we have lists of subscribers inside stores.
And it is really the same principle in Redux where you have one store, while in Flux you may have multiple stores.
Now little mathematics. Having 2 components A and B you need to have just a few possible update chains A updates B and B update A, or self-update (non including in here the updates from outside of the app). This is the possible case.
With just three components we have much more possible chains.
And with even more components it gets complicated. So to suppress the exponential complexity of possible components interaction we have this Flux pattern which in nothing more than IDispatch, IObservable if you worked with these interfaces from some other programming languages. One would be for spitting the actions, and the other for entering the listener's chain that exists inside the store.
With this pattern, your React code will be organized in a different way than common React approach. You will not have to use React.Component state anymore. Instead, you will use the Store(s) that will hold the application state.
Your component can only show the desire to mutate the application state by dispatching the action. For instance: onClick may dispatch the action to increment the counter. The actions are objects with the property type: that is usually a string, and usually in upper case, but the action object may have many other props such as ID, value,...
Since the components are responsible for rendering based on the application state we need somehow to deliver them the application state. It may be via the props = store.getState() or we may use the context. But also check this.
Finally, it is even not forbidden that component uses the internal state (this.state) in case this has no impact on the application. You should recognize these cases.

How to manage data in a React/Redux application

I having some issues on how to conceptualize a react application with Redux being my Flux library of choice.
So I am taking a few assumptions from my readings, correct me if I am wrong,
How does one manage data fetching?
Let's say this, I have a application that needs to fetch some data specific for the current logged in user, I assume this user data should be stored in the Redux Store.
But now the problem ensues, if all my state data is stored in a store, do I have for example a array of messages in the store for this user, and make my component fetch the information from the store? Or should I Fetch the data on the componentWillMount or similar method? I get that when I need to fetch data the first time, I will send an action to the store to fetch the data from the server, which will trigger a change event that I can catch on the component and update the state, is that a correct?
I feel like I am missing a point somewhere and can't make the connection on how the app is supposed to be structured and manage the data, since it seems the store will be bloated with tons of smaller "state" objects that will be used across the other routes/components.
In Redux you have a single store with many reducers. Each reducer changes specific part of the state.
For example you have an state like this:
{
messages: [...],
currentUser: {...},
notifications: [...],
...
}
In this case you will have a reducer to change messages part of state. Another reducer to change notifications part.

Should flux stores, or actions (or both) touch external services?

Should the stores maintain their own state and have the ability to call network and data storage services in doing so ...in which case the actions are just dumb message passers,
-OR-
...should the stores be dumb recipients of immutable data from the actions (and the actions be the ones that fetch/send data between external sources? Store in this instance would act as view-models and would be able to aggregate / filter their data prior to setting their own state base on the immutable data they were fed by the action.
It seems to me that it should be one or the other (rather than a mix of both). If so, why is one preferred / recommended over the other?
I've seen the flux pattern implemented both ways, and after having done both myself (initially going with the former approach), I believe that stores should be dumb recipients of data from the actions, and that asynchronous processing of writes should live in the action creators. (Async reads can be handled differently.) In my experience, this has a few benefits, in order of importance:
Your stores become completely synchronous. This makes your store logic much easier to follow and very easy to test—just instantiate a store with some given state, send it an action, and check to see if the state changed as expected. Furthermore, one of the core concepts in flux is to prevent cascading dispatches and to prevent multiple dispatches at once; this is very difficult to do when your stores do asynchronous processing.
All action dispatches happen from the action creators. If you handle asynchronous operations in your stores and you want to keep your stores' action handlers synchronous (and you should in order to get the flux single-dispatch guarantees), your stores will need to fire additional SUCCESS and FAIL actions in response to asynchronous processing. Putting these dispatches in the action creators instead helps separate the jobs of the action creators and the stores; furthermore, you don't have to go digging through your store logic to figure out where actions are being dispatched from. A typical asynchronous action in this case might look something like this (change the syntax of the dispatch calls based on the flavor of flux you're using):
someActionCreator: function(userId) {
// Dispatch an action now so that stores that want
// to optimistically update their state can do so.
dispatch("SOME_ACTION", {userId: userId});
// This example uses promises, but you can use Node-style
// callbacks or whatever you want for error handling.
SomeDataAccessLayer.doSomething(userId)
.then(function(newData) {
// Stores that optimistically updated may not do anything
// with a "SUCCESS" action, but you might e.g. stop showing
// a loading indicator, etc.
dispatch("SOME_ACTION_SUCCESS", {userId: userId, newData: newData});
}, function(error) {
// Stores can roll back by watching for the error case.
dispatch("SOME_ACTION_FAIL", {userId: userId, error: error});
});
}
Logic that may otherwise be duplicated across various actions should be extracted into a separate module; in this example, that module would be SomeDataAccessLayer, which handles doing the actual Ajax request.
You need less action creators. This is less of a big deal, but nice to have. As mentioned in #2, if your stores have synchronous action dispatch handling (and they should), you'll need to fire extra actions to handle the results of asynchronous operations. Doing the dispatches in the action creators means that a single action creator can dispatch all three action types by handling the result of the asynchronous data access itself.
I tweeted this question to the devs at Facebook and the answer I got from Bill Fisher was:
When responding to a user's interaction with the UI, I would make the async call in the action creator methods.
But when you have a ticker or some other non-human driver, a call from the store works better.
The important thing is to create an action in the error/success callback so data always originates with actions
The stores should do everything, including fetching data, and signalling to components that the store's data has been updated. Why? Because actions can then be lightweight, disposable and replaceable without influencing important behavior. All important behavior and functionality happen in the store. This also prevents duplication of behavior that would otherwise be copied in two very similar but different actions. The stores are your single source of (handling the) truth.
In every Flux implementation I've seen Actions are basically event strings turned into objects, like traditionally you'd have an event named "anchor:clicked" but in Flux it would be defined as AnchorActions.Clicked. They're even so "dumb" that most implementations have separate Dispatcher objects to actually dispatch the events to the stores that are listening.
Personally I like Reflux' implementation of Flux where there are no separate Dispatcher objects and Action objects do the dispatching themselves.
edit: Facebook's Flux actually fetches in "action creators" so they do use smart actions. They do also prepare the payload using the stores:
https://github.com/facebook/flux/blob/19a24975462234ddc583ad740354e115c20b881d/examples/flux-chat/js/actions/ChatMessageActionCreators.js#L27 (line 27 and 28)
The callback on completion would then trigger a new action this time with the fetched data as payload:
https://github.com/facebook/flux/blob/19a24975462234ddc583ad740354e115c20b881d/examples/flux-chat/js/utils/ChatWebAPIUtils.js#L51
So I guess that's the better solution.
I'll provide an argument in favor of "dumb" Actions.
By placing the responsibility for collecting view data in your Actions, you couple your Actions to the data requirements of your views.
In contrast, generic Actions, that declaratively describe the intent of the user, or some state transition in your application, allows any Store that responds to that Action to transform the intent, into state tailored specifically for the views subscribed to it.
This lends itself to more numerous, but smaller, more specialized Stores. I argue for this style because
this gives you more flexibility in how views consume Store data
"smart" Stores, specialized for the views that consume them, will be smaller and less coupled for complex apps, than "smart" Actions, on which potentially many views depend
The purpose of a Store is to provide data to views. The name "Action" suggests to me that its purpose is to describe a change in my Application.
Suppose you have to add a widget to an existing Dashboard view, which shows some fancy new aggregate data your backend team just rolled out.
With "smart" Actions, you might need to change your "refresh-dashboard" Action, to consume the new API. However, "Refreshing the dashboard" in an abstract sense has not changed. The data requirements of your views is what has changed.
With "dumb" Actions, you might add a new Store for the new widget to consume, and set it up so that when it receives the "refresh-dashboard" Action type, it sends a request for the new data, and exposes it to the new widget once it's ready. It makes sense to me that when the view layer needs more or different data, the things that I change are the sources of that data: Stores.
gaeron's flux-react-router-demo has a nice utility variation of the 'correct' approach.
An ActionCreator generates a promise from an external API service, and then passes the promise and three action constants to a dispatchAsync function in a proxy/extended Dispatcher. dispatchAsync will always dispatch the first action e.g. 'GET_EXTERNAL_DATA' and once the promise returns it will dispatch either 'GET_EXTERNAL_DATA_SUCCESS' or 'GET_EXTERNAL_DATA_ERROR'.
If you want one day to have a development environment comparable to what you see in Bret Victor's famous video Inventing on Principle, you should rather use dumb stores that are just a projection of actions/events inside a data structure, without any side effect. It would also help if your stores were actually member of the same global immutable data structure, like in Redux.
More explainations here: https://stackoverflow.com/a/31388262/82609

Resources