Working with React.js is really enjoyable.
I built a simple comment app based on the official tutorial.
You can add, edit, and delete comments. They are pulled via GET every 10 seconds.
At one point, the tutorial mentions optimistic updates: updating the UI before the server has responded in the case of a create, update or delete operation.
Since comments are part of a list, React suggests to assign a unique key to each comment.
I therefore use the database id of each comment as a key. That works fine for update and delete operations.
However, in the case of a create operation, I do not know the database id of a comment until it has been actually created server-side, and therefore I don't know what value to assign to the key.
At that point in time, the comment is added to the comment list but has no key/ID and therefore cannot be edited or deleted, until the list gets updated during the next API poll.
Can I work around that?
If you need the key to remain the same across updates, one option is to assign a temporary id to to an unused property. Then, use a function to retrieve the correct key for your list item models. As long as you account for the tempId property when updating the item, you can keep the key the same as long as the list remains in memory.
While you may not always care if the optimistic item gets removed and re-added, it can simplify your CSS when using on-enter or on-leave animations in your list. This also helps when you have stateful list item components.
Example:
let tempIds = 1; // 1 and up are truthy
// where ever you add the new item to your list
const newList = [...list, {...newItem, tempId: tempIds++}];
// get the right id
function getKey(instance) {
if (instance.tempId) {
return instance.tempId;
} else {
return instance.id;
}
}
// in your list render function
<List>
{list.map(model => (
<Item
key={getKey(model)}
//other props go here
/>
))}
</List
You need keys that are unique, consistent, and available. Your database IDs can't provide the third requirement, but you can--using local "client IDs". Obviously, you're responsible for guaranteeing their uniqueness and consistency.
You can add, edit, and delete comments. They are pulled via GET every 10 seconds.
We always POST to a resource which in returns yields the JSON response containing data we need, in your case ID. The delay is up to ~100ms which is fine.
If you set a temporary ID which is not equal to the one database is going to provide then React will re-render again once it receives the new data, just you will see two identical items as key is not the same.
Related
I'm trying to preselect rows in my table but the table won't refresh unless there are changes to the actual data itself. Is there a method to reinit the table that doesn't involve changing the data?
It's also completely possible that my method for approaching this requirement is wrong and there may be a better way? I've created an example sandbox here:
https://codesandbox.io/s/mock-preselected-rows-data-t36nl?file=/src/App.js
In this you can see I have a mock response from my server for determining what rows should be selected. I'm then grabbing the data to compare to see if any of the items from the mock response exist in the data and if so push them to a new obj which is then fed into the intialState for selectedRowIds
Any guidance appreciated.
Seems your work is all working. The short answer to your question.
As long as you want the user see something, in a React way, it needs to be contained in a state, or state derivative. In your case, it's a cell data wrapped in row and in a table.
So you can't avoid selecting it without touching the data. Unless you don't want user see the change.
Although the checkbox doesn't seem to be part of the original data stream, when you develop on it, you have to make it part of the data. To be honest, it's easy you make it part of the data, because by the time you want to refresh the table, ex. selecting or de-selecting, or deleting a row, you want everything refreshed. Unfortunately it's very difficult to do local refresh with a table in React. It's possible, but very difficult, because most of the design is based on either prop or context.
You can also refactor your handleSelectedRows function.
// Find row ids and compare them with our 'preSelectedTheseItems' array.
const handleSelectedRows = () => {
const preIds = preSelectTheseItems.map(item => item.collectibleId)
return data?.filter((collectibleRow, index) => preIds.includes(collectibleRow.collectibleId));
};
Example : codesandbox
How it is
I have an array of objects called vm.queued_messages (vm is set to this in my controller), and vm.queued_messages is used in ng-repeat to display a list of div's.
When I make an API call which changes the underlying model in the database, I have the API call return a fresh list of queued messages, and in my controller I set the variable vm.queued_messages to that new value, that fresh list of queued messages.
vm.queued_messages = data; // data is the full list of new message objects
The problem
This "full replacement" of vm.queued_messages worked exactly as I wanted, at first. But what I didn't think about was the fact that even objects in that list which had no changes to any properties were leaving and new objects were taking their place. This made no different to the display because the new objects had identical keys and values, they were technically different objects, and thus the div's were secretly leaving and entering every time. THIS MEANS THERE ARE MANY UNWANTED .ng-enter's AND .ng-leave's OCCURRING, which came to my attention when I tried to apply an animation to these div's when they entered or left. I would expect a single div to do the .ng-leave animation on some click, but suddenly a bunch of them did!
My solution attempt
I made a function softRefreshObjectList which updates the keys and values (as well as any entirely new objects, or now absent objects) of an existing list to match those of a new list, WITHOUT REPLACING THE OBJECTS, AS TO MAINTAIN THEIR IDENTITY. I matched objects between the new list and old list by their _id field.
softRefreshObjectList: function(oldObjs, newObjs) {
var resultingObjList = [];
var oldObjsIdMap = {};
_.each(oldObjs, function(obj) {
oldObjsIdMap[obj._id] = obj;
});
_.each(newObjs, function(newObj) {
var correspondingOldObj = oldObjsIdMap[newObj._id];
if (correspondingOldObj) {
// clear out the old obj and put in the keys/values from the new obj
for (var key in correspondingOldObj) delete correspondingOldObj[key];
for (var key in newObj) correspondingOldObj[key] = newObj[key];
resultingObjList.push(correspondingOldObj);
} else {
resultingObjList.push(newObj);
};
});
return resultingObjList;
}
which works for certain things, but with other ng-repeat lists I get odd behavior, I believe because of the delete's and values of the objects being references to other controller variables. Before continuing down this rabbit hole, I want to make this post in case I'm thinking about this wrong, or there's something I'm missing.
My question
Is there a more appropriate way to handle this case, which would either make it easier to handle, or bypass my issue altogether?
Perhaps a way to signal to Angular that these objects are identified by their _id instead of their reference, so that it doesn't make them leave and enter as long as the _id doesn't change.
Or perhaps a better softRefreshObjectList function which iterates through the objects differently, if there's something fishy about how I'm doing it.
Thanks to Petr's comment, I now know about track by for ng-repeat. It's where you can specify a field in your elements that "identifies" that element, so that angular can know when that element really is leaving or entering. In my case, that field was _id, and adding track by message._id to my ng-repeat (ng-repeat="message in ctrl.queued_messages track by message._id") solved my issue perfectly.
Docs here. Search for track by.
I have a recursive data structure to be fetched and displayed. I have a graph ql type as follow:
human {
name,
children: [human]
}
Now I wanted to incrementally fetch data and hence used to react classes HumanList and HumanItem, where I've used relay to fetch children only when a item is clicked. In my actual code relay gives children a null on very click i.e. on rendering very first set of children. I tried test code on relay playground and found similar issue. Here is the link to gist. Playground.js contains the code part and Playground.gql.js contains schema part. Clicking on each number open children under it. After 3 or 4 level it starts showing Found children as null. For me it happens on 1.1.2.2. If it doesn't happens so for you then try adding more levels in SCHEMA code and the bug would pop in.
I've already checked relay issues #246 and #536 but none of them helped.
Any help is very much welcome.
This was a bug. Given a plural field, when the time came to make a query for new data, we would diff what we have in the store with what the application wants. The bug was that we would assume that all records of a plural field have the same shape in the store, and only use the first store record in any plural field against which to diff. This was of course not true in your case, where some records in a plural field might be expanded and some might be collapsed.
This has been fixed as part of https://github.com/facebook/relay/issues/1243 and will be released in the version after Relay 0.9.1.
I'm trying to modify the stocklist Dynagrid demo which is designed to work with HTML5 and Javascript, it's originally designed to subscribe an item per subscription.
In my case, I have connected this demo to my lightstreamer server, the adapter in my server deals with itemgroub and fieldschema instead of itemList and fieldList which was used in the example.
I modified the code to subscribe using this item group and equivalent field schema, now the dynagrid listener (onVisualUpdate function) is capable to detect how many items in the item group and based on that it creates the equivalent number of rows, however, when I call getChangedFieldValue for and one of the fields in the dynagrid, I get null always, and based of that no data is updated on the screen.
What is the solution for this problem, and how can I get the updated values?
(Note: Currently, I get the data directly from the info paramter which is passed to onVisualUpdate function).
When using a Field Schema, fields in the subscription are identified by their 1-based index within the schema and not by their name. So, wen you call getChangedFieldValue try to use the 1-based index to identify a field.
I've been trying to figure this out for quite some time now. I couldn't find anything that addresses this problem, but please correct me if I'm wrong.
The problem:
I have data from a JSON API comming in, with an nested array/object structure. I use mapping to initially fill the model with my data. To update this, I want to extend the model if new data arrives, or update the existing data.
As far as I found out, the mapping option key, should do this trick for me, but I might have misunderstood the functionality of the mapping options.
I've boiled down the problem to be represented by this example:
var userMapping = {
key: function(item) {
return ko.utils.unwrapObservable(item.id);
}
};
// JSON call replaced with values
var viewModel = {
users: ko.mapping.fromJS([], userMapping)
};
// Should insert new - new ID?
ko.mapping.fromJS([{"id":1,"name":"Foo"}, {"id":2,"name":"Bar"}], userMapping, viewModel.users);
// Should only update ID#1 - same ID?
ko.mapping.fromJS([{"id":1,"name":"Bat"}], userMapping, viewModel.users);
// Should insert new - New ID?
ko.mapping.fromJS([{"id":3,"name":"New"}, {"id":4,"name":"New"}], userMapping, viewModel.users);
ko.applyBindings(viewModel);
Fiddle: http://jsfiddle.net/mikaelbr/gDjA7/
As you can see, the first line inserts the data. All good. But when I try to update, it replaces the content. The same for the third mapping; it replaces the content, instead of extening it.
Am I using it wrong? Should I try to extend the content "manually" before using mapping?
Edit Solution:
I solved this case by having a second helper array storing all current models. On new data i extended this array, and updated the view model to contain the accumulated items.
On update (In my case a WebSocket message), I looped through the models, changed the contents of the item in question, and used method valueHasMutated() to give notice of changed value to the Knockout lib.
From looking at your example code the mapping plugin is behaving exactly as I would expect it to. When you call fromJS on a collection you are effectively telling the mapping plugin this is the new contents of that collection. For example:
On the second line, How could it know whether you were updating or whether you had simply removed id:2?
I can't find any mention of a suitable method that treats the data as simply an update, although you could add one. Mapped arrays come with some helpful methods such as mappedIndexOf to help you find particular items. If you receive an update data set simply loop through it, find the item and update it with a mapping.fromJS call to that particular item. This can easily be generalized into reusable method.
You can use ko.mapping.updateFromJS() to update existing values. However, it does not add new values so that would be a problem in your instance. Take a look at the link below for more details.
Using updateFromJS is replacing values when it should be adding them
Yes, you should first collect all data into a list or array and then apply the mapping to that list. Otherwise you are going to overwrite the values in your viewModel.