I have a React application that uses Redux.
The application displays the results of some calculations at the top and some of the constituents that go into those calculations at the bottom. The user can click a button which pops up a modal in which the user can add one of these constituents. This will fire off an action during which it will send that new constituent to the backend, which will make those calculations have different results.
As you might expect, what I want is to refresh those results from the server based on the new data. The calculations are quite complex so replicating the calculation in the UI is not an option.
There is a loadConstituents() method which is called by componentWillMount() when the user lands on the page. I basically need to call this method again once the server confirms to the user that the data was received (via a SAVE_SUCCESS action).
How can I intercept an action in my component?
Alternatively, is there a better pattern for achieving this?
The best thing I could think of is passing the loadConstituents() function to the modal, which would pass it to the call, which will execute it once the call is done but passing a function through so many classes as a parameter seems very hacky.
Related
I am facing a challenge where I was requested to send page_view events (GA4) containing some information that is fetched asynchronously.
The challenge is because since the information is not there yet once the page renders, I can't fire the event until it's there. That's fine.
The problem
Since a page can have multiple components and these components can be fetching some data as well that I need to send, the only way I see to fire this event once the data is available is by relying on the redux store to check for when the data is there. So once it's there, I trigger the page view event.
Something like this:
const subscriptionsData = useAppSelector(
(state) => state?.[REDUX_API.KEY]?.[REDUX_API.SUBSCRIPTIONS]?.successPayload?.data
);
useEffect(() => {
sendPageViewEvent({ subscriptionsData });
}, [subscriptionsData]);
Now if a page has multiple components and other components are also fetching some data that I need to send in this same page_view event, this solution doesn't work anymore.
I did a lot of research but was unable to find a proper way to handle this scenario and I am starting to think the problem is that I am trying to send data that I don't have.
The only solution I can think of to solve this problem is too hacky:
Have a sort of configuration file where we check based on the route,
what kind of fields are expected to be sent to GA, but I am trying
to prevent this because maintainability is bad and likely to
developers to forget to do it.
In general, my main point is: Is it a bad practice to fire asynchronous data inside page_view events?
For click events it should be fine, but for page_view, even if you are doing all data-fetching in one single component in the page and could wait for it to fire the event there, you can't control that you might need in the future to fetch data in another component inside the same page, and then get this same data in the original function... you'd have to fire another event, which would mess up with data in analytics...
Imagine I have such setup. Imagine I have some map on my page and next to it there is a dialog where there is a toggle button.
Imagine when user clicks the toggle button a different request (depending on toggle state) is sent to server (which returns different type of map data depending on the request). If toggle is clicked once it receives say data about restaurants, next time it is clicked it receives data about hospitals and so on.
Also imagine when user drags a map, a refresh signal is sent to server, which sends same type of data but with updated information. For example, if user drags map when hospitals are shown on the map, again hospitals are received but with updated location.
Problem
Imagine two situations:
Assume user quickly clicks the toggle several times - then following thing will happen. First, request is sent to receive the restaurant data. But restaurant data has not arrived yet - and now before the restaurant data is received, the next toggle click happens, and request is sent to get hospital data (normally this second click on the toggle would delete restaurant data but since it is not on the map yet, it can't). So in the end we will end up with both restaurant and hospital data on the map which is what we don't want.
Imagine user clicks toggle and request is sent to receive restaurant data. But before restaurant data is received imagine user drags map (which causes refresh). What happens now due to drag is that since currently there is hospital data on the map, due to refresh, request to get updated hospital data will be sent. In the end we will again end up with hospital and restaurant data on the map.
I hope you can see the pattern of the kind of problems I am encountering here.
What is the best practice to deal with such situations?
I have solutions in my mind. One has the impact on User Experience and UI, the other one involves sending the last request. I will explain how:
1. Disabling the map and the button until the request is complete.
So what you can do is a loading or overlay div that stays until the request info is returned. Until this, the user is not allowed to use the map or toggle. This is UI impact but I have seen sites behaving this manner.
2. Map position.
The other option is to store the map position in like local storage and match the map position again on request success. If the map is on a different position, serve a message like Search in this area etc. Google maps behave something like this manner.
3. Serve the latest ajax request and abort others:
This can be done, by pushing the requests in a queue/order and send the last one to get the response. like if you are catering it via ajax, so some code like below code, if I call getFoo for 10 times, the last one will be fired
var ajax = null;
var getFoo = function() {
if(ajax) ajax.abort();
ajax= $.ajax({});
};
What you're describing is a simple case of race conditions.
There is no general solution for this kind of problem, it needs to be handled case-by-case. But in most cases, you probably want to display the results of the latest action user has done. If you don't have another way of knowing which request is which, you can attach some sort of identifier to the request, for example a GUID, and only display the data when the corresponding request is done.
Sometimes, the solution can be simply to disable all actions which could cause a race condition for the duration of the request, but this can deteriorate the user experience. Take for example Google Docs online editor, imagine that the whole editor would get disabled every time the auto-save function is triggered. In cases like this, it would be beneficial to store every update and compose the state from these actions. One of JavaScript that does state management like this is Redux. If you want to store data like this, you could use a database, such as EventStore.
Context
Single page / ajax web app
Basic code structure
LocationManager (responsible for updating the browser hash and switching the application location to a different tile)
Page/Tile Flow
Basic Info > Household Info > Vehicle Info > Purchase Options > Review Order > Enter Payment and Submit
Problem
When the user navigates from Purchase Options to Review Order, a long (5-8 second) service call is made to calculate order details. Upon the call's resolution, the callback is designed to navigate the user to Review Order page. The issue is, if the user clicks back during that time and goes back to Household Info, as soon as the call resolves, they will be "automatically" brought to Review Order. Very awkward user experience.
Limitations
Canceling the call is not an option. Need a solution to handle the navigation.
Current Proposed Implementation
Save "currentLocation" prior to making the calculateOrder call.
Pass the "currentLocation" in the callback to the setLocation method as intendedStartingPoint.
Inside setLocation method if(intendedStartingPoint === Locationmanager.currentLocation) {//Navigate}
To sum it up, if the user changes the location while the call is in progress, upon the call's resolution, we won't navigate since the user doesn't expect to be navigated to Review Order at that point.
This works, right?
The Catch
We have many places in the app where setLocation is called within a callback for a long-running call. This means that I will have to update all the setLocation calls with a new parameter - intendedStartingPoint. While it makes sense to me, it does seem like it has potential to get a bit cluttered.
Any ideas on how to clean it up and centralize it?
So, right now a user can click the Calculate button on a Purchase Options page. You then display some kind of a loading indicator (hopefully)
and send an asynchronous request to a server with setLocation('ReviewOrder') attached in a continuation. There is quite a number of places in the application where you use this pattern.
The problem of unexpected (from a user point of view) redirects is there because with this approach server data retrieval and UI navigation are coupled. A solution that comes to mind is to decouple them and remove setLocation calls
from all long-running request continuations. It can work the following way.
When the user clicks the Calculate button, you start an asynchronous request and at the same time immediately navigate to the Review Order page (this is important from a UX perspective since users now clearly understand that the Calculate button navigates to Review Order). On the Review Order page, display a loading indicator saying something like 'please wait, about 10 seconds remaining...' When a request completes, hide the loading indicator and show the data.
This way your users will have a consistent UX knowing that whenever they click a button in your application the same thing happens (they navigate to a view), and there are no surprising automagical redirects.
Given that you can't prevent the user from navigating among the tiles, notifying her about the calculation delay won't solve the whole problem. You can tell the user the estimated time to completion, you can display a progress bar, and you can take her immediately to the Review Order tile to wait for the results, but if she navigates away from the tile, you're left with your original problem.
If the user chose to navigate away after all of that information, she must have made a conscious decision to interrupt the proceedings. It would be bad UX to transport her back to Review Order. What now?
You propose, quite reasonably, that the callback function sent with calculateOrder should pass an intendedStartingPoint parameter to setLocation. You worry that this would require you to modify every call to setLocation to accommodate the new parameter. Never fear, JavaScript offers a neat way to solve this dilemma.
You can add a new parameter to setLocation without modifying the existing calls. This merely requires that intendedStartingPoint be the last argument in setLocation's argument list. Then your new version of setLocation can check the value of intendedStartingPoint to see if it's undefined.
If intendedStartingPoint is undefined, you know that setLocation is receiving one of the old calls, the ones that don't pass intendedStartingPoint. In these cases you ignore intendedStartingPoint and proceed as before. Otherwise, you compare intendedStartingPoint to the current location and proceed according to the result of the comparison.
An even better approach would be to make the new parameter not intendedStartingPoint, but an object called options that contains intendedStartingPoint as one of its attributes. This allows you to pass further optional values to setLocation if the need arises in the future.
The new behavior of setLocation is quite simple. Before setting a new location, you check whether intendedStartingPoint is equal to the current location. If it is, you don't have to do anything because the user is already where she's intended to be. But if the intendedStartingPoint is different from the current location, the user has navigated away, so you do something like this:
if (LocationManager.currentLocation !== options.intendedStartingPoint) {
// Tell the user that the calculation has finished.
// Ask her if she wants to return to Review Order now.
}
First thing, calculate order details via asynchronous call and show/simulate a progress bar to the end-user via javascript.
The second thing: do not enforce ReviewOrder tile opening in your service callback function. As the service completes it's calculation, your callback function checks the current tile, and if it is not ReviewOrder tile, then it stores the calculated information in the Session or Local Storage.
As user navigates ReviewOrder tile, compare order details which came from the user with the stored order details (via hashing function, for example).
If hashcodes of user order details and stored order details are the same, then show saved order information, otherwise call the service again.
Important note: to prevent order forging, consider the following way:
Upon calculating order details on the server, generate unique order id, that will be returned to the user. Then store the calculated order details along with this id in the server database. If user did not change order details, your script will post only this order id to the server as a sign, that order has been accepted. Then read your database and process the order by this id.
If order was not completed then employ a scheduled task, that cleans up your database from non-completed orders (for example - orders, calculated 24 hours ago, but still not completed).
First of all, if the user is able to go back and change any entered information on previous pages, it is a must to invalidate any pending service calls. If a service call based upon outdated information returns, it must be discarded.
This means, if(intendedStartingPoint === Locationmanager.currentLocation) {//Navigate} is not sufficient. You have to do something like if(intendedStartingPoint === Locationmanager.currentLocation && /* no information altered in the meantime*/) {//Navigate}.
Now for your design question: It's a little hard to construct this without any concrete code, but you could do the following:
Provide means to register and manage long-running calls in LocationManager
Long-running calls should then always be registered with the LocationManager
LocationManager should assure that at most one long-running call is active at a moment
If any location change occurs, all (or the one active) long-running call must be invalidated
Call-back of a long-running call should check if it not has been invalidated, and only navigate if this is the case. LocationManager could do this in a unified manner for all call-backs.
New long-running calls could replace/invalidate an already running call or be rejected, as you like.
I hope this makes sense in your concrete situation.
I'm playing around with Facebook Flux (I'm using Fluxxor, but I don't think that's really important) with ReactJS, and so far I think they its a great way of working with the data flow in an application. However, there's one thing that I'm really struggling to get my head around. It might be that this is simply something that shouldn't be done with Flex, or that I'm missing something obvious, but hence why I'm asking...
For example, I'm building a system to log in with. Very simple - there's a Login dialog that pops up, and you enter your username and password and press the button. This calls the LoginAction.login(username, password) Action Creator, which sends the LOGIN Event to the Dispatcher and then triggers the API call to authenticate the user and make sure the credentials are correct. If we get a success back from the API then we trigger the LOGIN_SUCCESS Event to the Dispatcher, the SessionStore handles this and stores the fact that we have successfully logged in, and the details of who we are. This then triggers bits of the UI to update - for example the "Log In" button changes to a "Hello Graham" bit of text, and a "Log Out" button instead. That's all really easy and just works and makes sense.
What I'm getting stuck with is when the login fails. If I enter an invalid username/password then I want the Login Dialog to tell the user this, so that they can correct what they entered and try again. The only way that I can think of to achieve this is to send a LOGIN_FAILED Event to the Dispatcher, which is then handled by some Store that stores the last Login errors for the Dialog to display. This just feels weird though, because these errors are not application state but are instead a transitive piece of information about this one request that failed, that the user will then correct and retry.
It feels to me that this transitive state is going to be very common around API calls that might fail because of user input, and so don't belong as part of the application state but instead belong somewhere else. However, I can't work out how Flux allows this transitive state to get back to the UI to be displayed to the user...
SessionStore handles this and stores the fact that we have successfully logged in
In your case you keep information about successful auths. Why not to keep info about unsuccessful once also at store (the same store or independent once which keeps auth state or permissions state for user)?
It's another side of your state. In this case you've just have to dispatch special event on failure (like in you case about successful login) to store(s), save new state and send changes to the UI.
Here is the example which is similar to what i mean. You send a request to the server ant can dispatch an action if login fails.
OK, the tite seems a little confusing, so I'll try to explain more thoroughly...
The process the page does currently follows the following sequence:
- User clicks a button
- server-side code goes retrieve data from the DB and exposes said data to the client using, populating, let's say, hidden fields.
- client-side code uses this data to fire up a an ActiveX component which performs a few tasks with the data provided.
And this works fine, however, we need to optimize the process because the ActiveX component is not fit to handle high volumes of data. We need to send data into "blocks" to the component, rather them send all data at once as it is done today.
However, I just hit a roadblock here, on how can I make the page go back and forth from server to client code multiple times? Like... "user clicks a button, server retrieves first block of data, sends to client, client executes ActiveX for the first block, client requests next block, server retrieves second block, sends to client, client executes ActiveX for the second block, client requests third block... and so on"? I can't get past the first request, since I can't register a client script block 2 times and expect AJAX to handle those multiple sequential callbacks...
Or is there a way?
This sounds more like an architectural issue than anything else.
What you should be doing here is:
1) User clicks a button. This is NOT a regular submit button. Just a plain old button that executes some local javascript.
2) Local javascript makes an AJAX request to determine how many records are available.
3) That javascript then does a loop based on the number of available records divided by the amount you want to pull per chunk.
3.a) Execute AJAX request for a chunk
3.b) Throw the data into your ActiveX control - which, btw, I really would suggest you guys think about getting rid of. There are so many issues with ActiveX that it's not even funny.
4) Repeat 3.a and 3.b until completion.
You'll notice that at no point was a full post back performed. You'll also notice that you shouldn't have to register any client script blocks.
Now the draw back here is purely in the ActiveX control. Can it be instantiated from javascript multiple times in a page or are you forced to only use a single instance?
If it's limited to a single instance, then you'll need a different approach entirely.