Vuejs watcher order - javascript

I have a Vue instance with two watchers:
watch: {
zone:function(zone) {
console.log('Zone watcher');
this.route = {};
},
route:function(route) {
console.log('Route watcher');
if(Object.getOwnPropertyNames(route).length === 0) {
var _this = this;
axios.get(route.url).then(function(response) {
_this.tracks = response.data;
});
} else this.tracks = {};
}
},
When a user selects a zone, route (and tracks) are reset. When user selects a route, tracks are loaded;
I have a component receiving zone and tracks as props, also with two internal watchers that perform some independent actions when any of this props change.
I also have a method that changes both variables:
jump:function(path) {
var parts = path.split(',');
this.zone = this.ZONES[parts[0]];
this.route = this.zone.routes[parts[1]];
},
The problem is watcher for route is fired in first place, then the watcher for zone changes route value triggering its watcher again, reseting tracks value to an empty object.
Is there a way to define the order that watchers must be triggered?

Andrey's comment shows the way. This question comes down to which tools you use for what job. Inevitably there's a bit of opinion... watch is for edge cases. You don't need it often, and if you can do without it, you probably should. watch belongs with computed and v-bind: they're reactive, use them (only) for representing state on screen, you have no (or little) control over when they run, and you shouldn't care.
A server request belongs in a method, or in a function outside of Vue (in your store perhaps) where it can be called explicitly. So create yourself a changeZone() function that clears routes and tracks, then calls the server, then updates your data (or store) with the server response. Your code will be much clearer if these little functional sequences are specified explicitly, in one place. The trigger for your sequence should likely be from an event (user-action) or lifecyle hook, not a watch.

Related

How to deal with race conditions in event listeners and shared state?

I have 2 event listeners that operate on the same shared data/state. For instance:
let sharedState = {
username: 'Bob',
isOnline: false,
};
emitter.on('friendStatus', (status) => {
sharedState.isOnline = status.isOnline;
});
emitter.on('friendData', (friend) => {
if (sharedState.isOnline) {
sharedState.username = friend.username;
}
});
My problem is that these events are emitted at any order. The friendData event might come in before the friendStatus. But friendData does something with the data returned from friendStatus. In other words: I need the event handler for friendData to execute after friendStatus, but I don't have this assurance from the event emitter perspective. I need to somehow implement this in my code.
Now of course I could simply remove the if (sharedState.isOnline) { from the friendData listener and let it run its course. Then I'd have a function run after both handlers have finished and somewhat reconciliate the shared state dependencies:
emitter.on('friendStatus', (status) => {
sharedState.isOnline = status.isOnline;
reconcileStateBetweenUsernameAndIsOnline();
});
emitter.on('friendData', (friend) => {
sharedState.username = friend.username;
reconcileStateBetweenUsernameAndIsOnline();
});
Problem is that this reconciliation function knows about this specific data dependencies use case; hence cannot be very generic. With large interconnected data dependencies this seems a lot harder to achieve. For instance I am already dealing with other subscriptions and other data dependencies and my reconciliation function is becoming quite large and complicated.
My question is: is there a better way to model this? For instance if I had the assurance that the handlers would run in a specific order I wouldn't have this issue.
EDIT: expected behavior is to use the sharedState and render a UI where I want the username to show ONLY if the status isOnline is true.
From #Bergi's answer in the comments the solution I was hinting seems to be the most appropriate for such case. Simply let the event-handlers set their own independent state, then observe on the values changing and write appropriate logic based on what you need to do. For instance I need to show a username; this function shouldn't care about the order or have any knowledge of time: it should simply check whether the isOnline status is true and if there's a username. Then the observable pattern can be used to call this function whenever each dependency of the function changes. In this case the function depends on status.isOnline and friend.username hence it will observe and re-execute whenever those values change.
function showUsername() {
if (status.isOnline && friend.username != '') return true;
}
This function must observe the properties it depends on (status.isOnline and friend.username). You can have a look at RxJS or other libraries for achieving this in a more "standard" way.

Update context when state object mutates

I have a PageContext that is holding the state of User objects as array. Each User object contains a ScheduledPost object that does mutate when user decides to add a new post. I have no idea how to trigger an update on my PageContext when it happens (I want to avoid forceUpdate() call). I need to somehow be notified of that, in order to re-render posts, maintain timer etc.
Please, see the code:
class User {
name: string;
createTime: number;
scheduledPosts: ScheduledPost[] = [];
/*
* Creates a new scheduled post
*/
public createScheduledPost(title : string, content : string, date : number): void {
this.scheduledPosts.push(Object.assign(new ScheduledPost(), {
title,
content,
date
}));
}
}
class ScheduledPost {
title: string;
content: string;
date: number;
public load(): void {
// Create timers etc.
}
public publish(): void {
// Publish post
}
}
// PageContext/index.tsx
export default React.createContext({
users: [],
editingUser: null,
setEditingUser: (user: User) => {}
});
// PageContextProvider.tsx
const PageContextProvider: React.FC = props => {
const [users, setUsers] = useState<User[]>([]);
const [editingUser, setEditingUser] = useState<User>(null);
// Load users
useEffect(() => {
db.getUsers()
.then(result => setUsers(result));
}, []);
return (
<PageContext.Provider value={{
users,
editingUser,
setEditingUser
}}>
{props.children}
</PageContext.Provider>
);
};
What I would like to achieve is, when consuming my provider with useContext hook:
const ctx = useContext(PageContext);
I would like to create a schedule post from any component like so:
// Schedule post (1 hour)
ctx.editingUser.createScheduledPost("My post title", "The post content", (new Date).getTime() + 1 * 60 * 60);
However, this wont work, since React doesn't know that User property has just mutated.
Questions:
How can I make React being notified of the changes within any of the User object instance? What is the way to solve it properly (excluding forceUpdate)?
Am I doing it right? I'm new to React and I feel like the structure I'm using here is cumbersome and just not right.
Where are the users being mutated? If you're storing them in your state as it appears, the changes should be detected. However if you're using the methods built into the User class to let them directly update themselves, then React will not pick up on them. You would need to update the entire users array in your state to make sure React can respond to the changes.
It's tough to give a more specific example without seeing exactly where/how you're updating your users currently, but a generalized mutation might go something like this (you can still use a class method, if desired):
const newUsers = Array.from(users); // Deep copy users array to prevent direct state mutation.
newUsers[someIndex].createScheduledPost(myTitle, myContent, myDate);
setUsers(newUsers); // Calling the setX function tied to your useState call will automatically trigger updates/re-renders for all (unless otherwise specified) components/operations that depend on it
In React re-render is caused by calling setState within component (or by using hooks, but the point is that you need to call specific method) or by changing component props. That means that manual mutation of your state will never cause a re-render - even if you had simple component and called
this.state.something = somethingElse;
re-render would not occur. Same thing works for context.
For your case, this means that you should not mutate editingUser state, but call setEditingUser with changed user state, something like:
const user = { ...ctx.editingUser };
user.createScheduledPost("My post title", "The post content", (new Date).getTime() + 1 * 60 * 60);
ctx.setEditingUser(user);
I'm not sure about your inner structure, but if that same user is also in users array, then you'll need to update that part of state by calling setUsers method where you maintain whole array and only update that single user which changed data - if thats the case then I'd think about restructuring the app because it already gets complicated for such simple state changes. You should also consider using redux, mobx or some other state management library instead of react context (my personal advice).
EDIT
Please take a look at this quite:
In a typical React application, data is passed top-down (parent to
child) via props, but this can be cumbersome for certain types of
props (e.g. locale preference, UI theme) that are required by many
components within an application. Context provides a way to share
values like these between components without having to explicitly pass
a prop through every level of the tree.
As you can see, react team is suggesting using context for some global preferences that are required within many components. The main problem with using context (in my opinion) is that you don't write natural react components - they don't receive dependant data through props but rather from within the context api itself. This means that you won't be able to reuse your components without also integrating context part of application.
While for example redux has similar concept of keeping state at one place, it still propagades that state (and its changes) to components via props, making your components undependent of both redux, context or anything else.
You can stick to react context and make whole app work with it, but I'm just saying it wouldn't be best practice to do so.

How does react detect changes in state/props?

JavaScript basically doesn't provide watchers for variables.
So I wonder how React.js does it.
I'm trying to do the same thing. For objects and arrays, I can use Proxy, like this:
let proxy = new Proxy(arr, {
deleteProperty: function(target, property) {
console.log("Deleted %s", property);
return true;
},
set: function(target, property, value, receiver) {
target[property] = value;
console.log("Set %s to %o", property, value);
return true;
}
});
Maybe it's not a good idea. And I'm still don't know what to do with literals such as Numbers.
The TL/DR was already given in the comments: React does not watch the data, changes are always triggered from "the outside", which means setState. The state does not even have to change, calling setState with the same data again already triggers the lifecycle of the component.
However, if you use redux or some other state container, it can look as if components magically detect changes themself whenever the data used by mapStateToProps function changes. But react-redux's connect function merely wraps your component with a proxy that listens for events:
register a listener for every redux action
compare the current and previous state on every event using strict equal (===)
if the state is different, call mapStateToProps
compare the result of mapStateToProps with previous calls using strict equal
if different, trigger a rerendering of the component
In order to imitate the behaviour, you would need some kind "thingsDidChange" listener which is triggered by events or continuous (setInterval) check.

Listening directly to Reflux Actions in Component

From what I've read the pattern is the Components pass data to the Actions which Pass to the Store whose value changes trigger updates in Components that subscribe to the Stores. My question is how to "react" to these triggered updates in the form of a notification? ( ie a successfully saved notification )
Ie do I add logic to the render of this notification component that only displays itself if there is a some flag attribute in the object that its subscribed to? Then deletes itself after a time. This sounds wrong.
UPDATE
Thanks to Hannes Johansson I think I have a better grasp of a pattern. What I have working is the following:
Component passes data through action to the Store
The Store interacts with the api and adds a flag to the model that the component is now notified of an updated model.
createItem: function (item) {
$.ajax({
url: '/items',
method: 'POST',
data: item,
success: function (item) {
CurrentBrandActions.addCampaign(item);
this.item = item;
item.newlyCreated = true;
this.trigger(item);
}.bind(this)
})
}
The Component sees the flag and renders a "Notification Child Component"
var newlyCreated = this.state.item.newlyCreated === true;
if (newlyCreated) {
newlyCreated = <ItemCreatedNotification item={this.state.item} />
} else {
newlyCreated = '';
}
return (
<form onSubmit={this.createItem} className="form">
{newlyCreated}
Something needs to move the app to a new place based on this event. Should this be a) the Notification Child Component b) Parent Component c) The Store?
According to Colin Megill's talk on flux api patterns the api interaction should occur in the Action, but reflux doesn't really allow for that.
UPDATE 2
Component passes data to an Action called createItemRequest
The Action has a preEmit hook that actually does the api call. The createItemRequest continues to the Store so that the store can change the model to reflect the state of sending which is then displayed in the component( maybe show a spinner ). The Action is also responsible for firing two other events depending on the api result.
ItemActions.createItemRequest.preEmit = function (data) {
$.ajax({
url: '/items',
method: 'POST',
data: data,
success: function (item) {
ItemActions.itemCreatedSuccess(item);
},
error: function (error) {
ItemActions.itemCreatedError(error);
}
});
}
There are different approaches to this. For example, in Reflux it's very easy to listen directly to actions if you choose to, since each action is actually a "dispatcher".
However, the general, purist Flux principle is that only stores register with the dispatcher and that components only listen to store updates. And the store just trigger an event that notifies that something has changed, not providing any payload. Then it's up to the component to read the store's state and determine how to render it.
One approach would be the one you describe, put some flag on the items in the store to signal that an update has happened, but it would violate the Flux principle if the components themselves then update the stored items' flags, since only stores are meant to mutate state, and only in response to actions and not from any other source. So in that case the "Flux thing" to do would probably be to trigger yet another event that signals that the newly added item has been noted so that the store can then reset the flag in response to that action.
Another approach I can think of would be to diff the state in your component when it gets notified of a store update. Then keep the flags only in the component, or even keeping newly added items in a separate list in the state and render them separately.
There are no hard rules here, except that if you want to follow the core Flux principle, components should never directly mutate stores' state, because that should only be mutated by the stores themselves in response to actions. That allows for a uni-directional data flow and a single source of truth about the data, which is the main goal of Flux.

Clean up requests and callbacks when switching routes

I have a backbone app that loads some data from an API into a chart. This app has several tabs that can be navigated through to access these different charts, and every tab executes a route. Every chart is an instance of ChartView with the appropriate data put in.
I have a problem that is caused by some API calls that may take a while. When the requests takes too long, some users start to cycle quickly through the tabs, executing each route quickly after each other. This fires up all the collection fetches which eventually messes up the interface because some callbacks will do a bit of rendering.
So my question is, how can i make sure that every time a new route is loaded (even if it is done in quick succession) that all pending or started requests are stopped so no "request-success" callbacks are fired?
I would suggest, override Backbone.Views remove method. With regular stopListening, abort ajax calls, also set a flag like this.removed=true. In your render function check for removed flag, if present don't render. If click has been done very quickly, you may need to check it before making any calls.
Based on Ravi Hamsa's reply I implemented an object that is injected into each route that keeps the requests and whether or not the route is still relevant.
It looks like this:
var RouteContext = function RouteContext() {
this._xhrs = {};
this.stopped = false;
this.manageRequest = function(xhr) {
this.xhrs.push(xhr);
}
this.stop = function() {
this.stopped = true;
_.invoke(this.xhrs, 'abort');
}
}
I override the Backbone.Router route method like this:
route: function(route, name, callbackFactory) {
var callback;
if (_.isFunction(callbackFactory)) {
var context = new RouteContext();
callback = callbackFactory(context);
// When a new route is opened, this route should be stopped and all
// corresponding jqXHR's should be aborted.
App.mediator.on('tabClicked', function() {
context.stop();
});
} else {
callback = callbackFactory;
}
return Backbone.Router.prototype.route.call(this, route, name, callback);
}
I can now create a new route method with this context like this:
var routeFactory = function(routeContext) {
// Might do some route initialisation here.
return function() {
this.reset(routeContext);
// This function is the actual function that will be called when a route is triggered.
if (routeContext.stopped === false) {
myView.renderChart();
}
}
};
// Register the route on the router.
myRouter.route('route', 'name', routeFactory);
Because a route can be called multiple times I reset the RouteContext back to it's original state when the route is called again.
And in my route I keep checking everywhere I need to do rendering whether the routeContext.stopped is still false. If it is true I don't do the rendering.

Categories

Resources