I've been working with react/flux for a few weeks now and while I feel like I've got a pretty good handle on everything from async loading to updating props/states/etc, one thing that is still bothering me is how to handle save states.
For example, when loading data, I just have an isLoading boolean parameter in my store that gets passed to my components. But when I try and post an updated object to the server, it's trivial to:
fire the update action
display a "save in progress" state
but figuring out the result of the update action seems to be way more difficult.
Probably the most applicable post I've seen on this is in Fluxxor's async data guide, but their solution (adding/modifying a status property on the object) feels error-prone to me.
onAddBuzz: function(payload) {
var word = {id: payload.id, word: payload.word, status: "ADDING"};
this.words[payload.id] = word;
this.emit("change");
},
onAddBuzzSuccess: function(payload) {
this.words[payload.id].status = "OK";
this.emit("change");
},
onAddBuzzFail: function(payload) {
this.words[payload.id].status = "ERROR";
this.words[payload.id].error = payload.error;
this.emit("change");
}
Is there a better way to manage save states or is adding a status property to the object the best way?
I recommend keeping your "model stores" and "ui stores" separate, or at least accessed via different cursor positions in the same store. So, in your case you'd have one store or branch for your "word model" and then another store or branch for "word status."
While this adds some complexity in the form of breaking up logic across stores and reacting twice to the AddBuzz action, it ends up reducing (more) complexity by confining model store updates to true changes in model data and managing ui states separately.
EDIT
This is what Relay will more-or-less be doing, keeping persisted data in a separate self-managed store, leaving custom stores for nothing but ui state. Some other libraries like https://github.com/Yomguithereal/baobab also recommend this approach. The idea is that these are fundamentally different kinds of state. One is domain-specific persisted data and the other is ui-specific ephemeral application state.
It might look something like this:
model_store.js:
onAddBuzz: function(payload) {
var word = {id: payload.id, word: payload.word};
this.words[payload.id] = word;
this.emit("change");
}
ui_store.js:
onAddBuzz: function(payload) {
this.wordStates[payload.id] = 'ADDING';
this.emit("change");
}
my_view_controller.js:
// listen to both stores and pass down both words and wordStates to your views
my_word_view.js:
...
render: function() {
var word = this.props.word,
wordState = this.props.wordState;
...
}
If you don't want to emit two change events, have one waitFor the other and emit the change only from the second one.
Related
We are in the process of integrating RTK query in our app.
Our current "architecture" is as follow:
All the business logic actions are written inside services which are plain JS classes.
Those services are passed using react context in order for the component tree to be able to call services functions.
As of now those services were accessing the redux store directly to perform the appropriate logic.
Now that we are moving to RTK, accessing the RTK cache from a service is less trivial:
As far as I can see, the only way to access it is via the select function of the relevant endpoint.
The point is that this method is a "selector factory" and using it outside of a react component doesn't seems to be the right way to go.
Here is an exemple:
class TodoService {
getTodoTitle( todoId: string ) {
// This doesn't looks the right way to do it
const todoSelector = api.endpoints.getTodo.select( {id: todoId } );
const todo = todoSelector( state )
return todo.data.title
}
}
Is there any way to implement safely the following code
class TodoService {
getTodoTitle( todoId: string ) {
// Is there any way to do that kind of call ?
const todoEntry = api.endpoints.getTodo.getCacheEntry( {id: todoId } );
return todoEntry.data.title
}
}
I guess that the answer is "no" and I have to refactor our whole architecture, but before doing so I'd like to be sure that there is no alternate approach.
Note that I could build the cache entry key by myself, but that also doesn't sound like a robust approach...
The thing is that you don't want to just get the cache entry - the selector does a little more for you than just that.
So let's just stay with "please use the selector" and "we won't add another way of doing that" because, selectors is how your code should interact with Redux all the time - React or not.
If you are not calling this code from React where you would need a stable object reference, it is good as it is. The selector factory will create a new selector and thus you get an un-memoized result. This is not perfect, but if you are not relying on referential equality, it also does not hurt at all.
If you want to have the referential equality, you'll have to store the selector in some way, probably as a class property.
Something like this would be possible:
class TodoService {
getSelectorForTodo(todoId: string) {
if (this._lastId !== todoId)
this._lastSelector = api.endpoints.getTodo.select( {id: todoId } )
return this._lastSelector
}
getTodoTitle( todoId: string ) {
const todo = this.getSelectorForTodo(todoId)(state)
return todo.data.title
}
}
i'm building an Gmail-like email browser client app prototype and i need a little help/advice structuring my React/Flux app. I decided to use pure Flux to get a better idea of how it works.
It's a simple email client with a list of letters, grouped by folders and tags and an ability to add letters to favorites.
So, i have a LettersStore containing an array of letters. The single letter data object looks something like this
{
id: 0,
new: true, //unread
checked: false,
starred: false,
folder: "inbox", //could be 'sent', 'spam', 'drafts'
sender: "Sender Name",
subject: "Re:",
snippet: "Hello there, how are you...",
body: "Hello there, how are you doing, Mike?",
date: "02.19.2016 16:30",
tags:["personal", "urgent"]
}
So what i'm trying to achieve is to let users navigate through folders (inbox, sent, drafts, spam) and filters (starred, tag, etc.)
In both folders and filters there has to be a way to select (check) some/all letters. The view state depends on how many letters are selected (the Select-all checkbox update, just like on Gmail). When the user selects a letter, the Flux action is being triggered and the state of the app updates.
The controller-view on top of the app does all the calls to the LettersStore public methods and passes the data down as props, but i'm not sure, what public methods the LettersStore should have. Currently it has:
emitChange()
addChangeListener()
removeChangeListener()
getAll() //returns array
areSomeLettersInFolderChecked(folderName) //returns bool
areAllLettersInFolderChecked(folderName) //returns bool
countNewLettersInAllFolders() //returns object
This works ok with folders, but when it comes to filters, it doesn't make sense anymore, since a starred letter is in some folder, and i feel like it's not the right thing to add specific methods like areSomeLettersInFilterChecked(filterType) etc.
Also, just like in Gmail, there has to be a way to select letter in the "Starred" filter, which belongs to the "Inbox" folder, then navigate to "Inbox" folder and keep that letter selected.
Maybe i should move the areSomeLettersInFolderChecked-like stuff to the component level?
I'm sure here has to be a proper way of doing it. Thanks in advance!
Rather than trying to encapsulate all the possible states and filters into your letter objects, keep it dumb. Normalize it and use supporting data structures to represent the other characteristics.
I'd strip it down to just the following properties:
{
id: 0,
sender: "Sender Name",
subject: "Re:",
snippet: "Hello there, how are you...",
body: "Hello there, how are you doing, Mike?",
date: "02.19.2016 16:30",
tags:["personal", "urgent"]
}
Your LetterStore can stay the same, or alternatively you could use an object or map to store letters against their id's for quick lookups later.
Now we need to represent the properties we removed from the message.
We can use individual sets to determine whether a message belongs to the new, checked and starred categories.
For instance, to star a message, just add it's id to the starred set.
var starred = new Set();
starred.add(message.id);
You can easily check whether a message is starred later on.
function isStarred(message) {
return starred.has(message.id);
}
The pattern would be the same for checked and unread.
To represent folders you probably want to use a combination of objects and sets.
var folders = {
inbox: new Set(),
sent: new Set(),
spam: new Set(),
drafts: new Set()
}
Simplifying your structures into these sets makes designing queries quite easy. Here are some examples of the methods you talked about implemented with sets.
function checkAll() {
messages.forEach(function(message) {
checked.add(message.id);
});
return checked;
}
function isChecked(message) {
return checked.has(message.id);
}
function inFolder(name, message) {
return folders[name].has(message.id);
}
// is message checked and in inbox
if(isChecked(message) && inFolder('inbox', message)) {
// do something
}
It becomes easy to construct complex queries, simply by checking whether messages belong to multiple sets.
I have a simple data structure of users and events. I am wanting to find all users who are attending the same events that the logged in user is. Users can attend multiple events.
My data is setup as follows
{
events:{
123:{
name: 'event1',
users:{
9876: true,
7564: true
}
}
},
users:{
9876:{
name: 'John',
events:{
123: true
}
},
7564:{
name: 'Peter',
events:{
123: true
}
}
}
}
I have the following code to achieve this, I was just wondering if I am on the right path and if my data structure is correct for this type of query (Firebaseref is an Angular factory)
FirebaseRef.child("users/" + authData.uid + "/events").orderByChild('displayName').once("value", function (snap) {
snap.forEach(function (event) {
FirebaseRef.child("events/" + event.key() + "/users").once("value", function (userSnap) {
userSnap.forEach(function (user) {
FirebaseRef.child("users/" + user.key()).once("value", function (realUserSnap) {
if (realUserSnap.key() != authData.uid) {
//This is a user who attends the same event
}
});
});
});
});
});
I would probably change that outermost query from a once('value' to an on('child_added' (and its other child_* siblings). The main advantage is that you're monitoring/synchronizing the data, instead of retrieving is just once. An added advantage is that it will remove the need for your first forEach.
Aside from that, this looks pretty common. The inner calls need to be once's, because you only want them to execute once. Most people get nervous because of the number of the number of on calls that will happen. But Firebase's data retrieval has little overhead after the initial websocket connection has been set up, so this typically performs pretty well.
There are lots of "should", "typically" and "may" in this answer, since the only way to be certain is for you to actually:
verify that the code functionally does what your application requires
measure the performance of the code in the conditions that you expect your users to encounter
If you do run into higher-than-expected latency, you could consider denormalizing the data a bit further. For example: you could keep the user's name in each event, where you now store true. With that, you would need to look up each user's name.
I've got a many-to-many relationship in Breeze:
Product *---1 ProductWidgets 1----* Widgets
Product needs to know when any of it's Widgets changes. Widgets can be added or removed from Products at any time.
Ideally, I'd want to do something like:
product.widgets.on('change', function () {});
...but I'm imagining I need something like:
var handleWidgetChange = function (changes) {
console.log("here are the changes", changes);
};
for(var i = 0; i < product.productWidgets.length; i++) {
// make sure we're getting events for the current set of widgets
product.productWidgets[i].widget.entityAspect.propertyChanged.subscribe(handleWidgetChange);
// if any of the current set of product widgets gets pointed elsewhere, catch that
product.productWidgets[i].entityAspect.propertyChanged.subscribe(function (change) {
if (change.propertyName === "widget") {
change.oldValue.entityAspect.propertyChanged.unsubscribe();
change.oldValue.entityAspect.propertyChanged.subscribe(handleWidgetChange);
}
})
}
// handle new product widgets and removal of product widgets
product.productWidgets.arrayChanged.subscribe(function (change) {
if (change.added) {
change.added[0].widget.entityAspect.propertyChanged.subscribe(handleWidgetChange);
} else if (change.removed) {
change.removed[0].widget.entityAspect.propertyChanged.unsubscribe();
}
});
Is there a recommended way to achieve this?
(Note: I'm using angular, and would love to just $watch('product.productWidgets', function () {}, true) but that gives a circular reference error.)
Memory leaks are a huge risk in JavaScript, in part because there are no weak references. You must be careful with events. You really don't want to iterate over entities adding and removing subscriptions.
You also do not want to use Angular watches for monitoring model changes because you'll drive UI performance into the ground. There are too many entities with too many properties and you'll surely make a mistake by leaving watches in place long after you should have stopped watching.
Fortunately, Breeze provides a central entity change monitoring facility. A Breeze EntityManager listens for changes to any of the entities it holds in cache.
var widgetType = manager.metadataStore.getEntityType('Widget');
var productWidgetType = manager.metadataStore.getEntityType('ProductWidget');
entityManager.entityChanged.subscribe(entityChanged);
function entityChanged(changeArgs) {
var entity = changeArgs.entity;
if (entity.entityType === productWidgetType ||
entity.entityType === widgetType) {
// do what you do when someone does something to an entity of this type
// perhaps call back into a method on that instance that knows what to do
entity.somethingChanged(changeArgs.entityAction);
}
}
This one event notifies you of any change to any entity in the manager's cache. It will be called frequently so be crisp in your evaluation. For example, consider deactivating your event handler during queries.
The changeArgs.entityAction tells you what just happened to the entity. There are many actions that trigger this event: a property could change, its EntityState could change (add/modify/delete/detach), etc.
You don't have to worry about the product.productWidgets array. When a ProductWidget is added or removed from that array, the ProductWidget.productId foreign key will change ... and you're picking that up in this entityChanged handler.
There is no need to worry about a memory leak because the EntityManager already holds a reference to the entity and will continue to do so until you detach the entity or dispose of the EntityManager instance (and all of your own or the UI's references to the entity). That, to my mind, is appropriate lifetime management.
Say person can have multiple cars, and car can have multiple accidents. So we could have:
# Person with no cars
person:
name: "Misha"
cars: []
# Person with free-accident car
person:
name "Arlen"
cars:
0:
name: "Toyota"
accidents: []
Firebase stores these people as:
person:
name: "Misha"
and
person:
name "Arlen"
cars:
0:
name: "Toyota"
So in JavaScript I have to do following to restore the empty arrays: (CoffeeScript)
if person.cars?
for car in person.cars
car.accidents = [] unless car.accidents?
else
person.cars = []
Is there a better way to handle empty arrays in Firebase without writing this needless JavaScript code?
I think that, if I understand the core question, the short answer is that there is no way to force an empty array into Firebase. However, there are some paradigms that might work better than what you have above.
Keep in mind that Firebase is a real-time environment. The number of cars and accidents can (and will) change at any time. It's best to treat everything as new data arriving in real time and avoid even thinking about exists or doesn't exist.
// fetch all the people in real-time
rootRef.child('people').on('child_added', function(personSnapshot) {
// monitor their cars
personSnapshot.ref().child('cars', 'child_added', function(carSnapshot) {
// monitor accidents
carSnapshot.ref().child('accidents', 'child_added', function(accidentSnapshot) {
// here is where you invoke your code related to accidents
});
});
});
Note how there is no need for if exists/unless type logic. Note that you would probably also want to monitor child_removed on cars and people and call ref.off() to stop listening to specific children.
If for some reason you want to stick with the static model, then forEach will become your friend:
// fetch all the people as one object, asynchronously
// this won't work well with many thousands of records
rootRef.child('people').once('value', function(everyoneSnap) {
// get each user (this is synchronous!)
everyoneSnap.forEach(function(personSnap) {
// get all cars (this is asynchronous)
personSnap.ref().child('cars').once('value', function(allCars) {
// iterate cars (this is synchronous)
allCars.forEach(function(carSnap) { /* and so on */ });
});
});
});
Note how, even with forEach, there is no need for "exists or unless" sort of logic.
I usually use the DataSnapshot function numChildren() to see if it's empty of not, like this
var fire = new Firebase("https://example.firebaseio.com/");
fire.once('value', function(data){if (data.numChildren() > 0){ /*Do something*/ });