Flat Array vs nested Array in React are treated differently - javascript

I have noticed that I can pass to React a set of nested arrays and it will render the items properly, but when I go this way, it will not complain of missing keys on my elements.
const stuff = 'a,b,c';
// Nested Array
// React is fine with it and automatically assigns keys
// Sample data: [[a, <br />], [b, <br />], [c, <br />]]
const Foo = () => <div>{stuff.split(',').map(itm => [itm, <br />])}</div>;
// Flat Array
// React warns me that I should assign a key to each element in array
// Sample data: [a, <br />, b, <br />, c, <br />]
const Bar = () => <div>{stuff.split(',').map(itm => [itm, <br />]).reduce((a, b) => a.concat(b), [])}</div>;
Sample pen:
https://codepen.io/FezVrasta/pen/NppLPR
Why does it happens? I can't find any reference to "nested arrays" support in React, neither on "auto assigned keys".

You'll notice that even though a warning is printed to the console, React still shows both Foo and Bar in your HTML being generated. React uses unique keys for reconciliations whilst trying to boost rendering performance. You can read more about this on the React reconciliation recursing on children page. Not providing keys means React cannot be as performant as it has been designed to be.
With regards to your question as to why a warning is not output to the console for nested arrays, we have to dive into the source code:
The function which generates the warning is called validateExplicitKey, and lives in the ReactElementValidator.js module.
This function is used in the validateChildKeys in the same module - looking into the source code gives the following, as of React 15.4.2:
function validateChildKeys(node, parentType) {
if (typeof node !== 'object') {
return;
}
if (Array.isArray(node)) { // 1.
for (var i = 0; i < node.length; i++) {
var child = node[i]; // 2.
if (ReactElement.isValidElement(child)) { // 3.
validateExplicitKey(child, parentType);
}
}
} else if (ReactElement.isValidElement(node)) {
// This element was passed in a valid location.
if (node._store) {
node._store.validated = true;
}
} else if (node) {
var iteratorFn = getIteratorFn(node);
// Entry iterators provide implicit keys.
if (iteratorFn) {
if (iteratorFn !== node.entries) {
var iterator = iteratorFn.call(node);
var step;
while (!(step = iterator.next()).done) {
if (ReactElement.isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
}
}
}
}
}
}
An array of arrays will enter the first code block
the child will be set to child = ["b", Object] (where 'Object' is react's virtual dom representation for the br node we created via JSX)
the array will be run through the function ReactElement.isValidElement:
ReactElement.isValidElement = function (object) {
return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
};
with REACT_ELEMENT_TYPE being set as:
var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol['for'] && Symbol['for']('react.element') || 0xeac7;
The array is an object, and is not null, but it's $$typeof property hasn't been set here, so the check fails.
$$typeof hasn't been set because React only adds this property to elements it creates to identify whether something is a React Element or not. This includes native HTML elements, and not data types.
Hence the ReactElement.isValidElement check fails, and the warning is never shown.

I've been wondering the same thing recently!
From what I've understood in ReactJS's official docs about how the keys work, I would expect to get the same warning with the nested array of data, just like the one with the flat array of data, since in both cases there are no key attributes set.
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.
I actually filled in a bug report (issue) in the ReactJS official GitHub repo, describing the same case you pointed out, but simplified (without the fancy .map() and .reduce() thingies).
It looks like a bug to me.
PS: I will update my answer as soon as the React team responds to me.

Related

Remove function in React with .filter

While building a Todo app, I want to filter out an object out of my array with a remove function. So far I got this.
deleteTask(task) {
let taskList = this.state.tasks;
var newTask = taskList.filter(function(_task) { return _task != task})
this.setState({
tasks: newTask
});
}
Only problem is, the function returns the whole array while using the function.
So the Task argument that should return just an object out of my array returns the whole array instead while in my newTask var.
How can I bind or make this function work?
The array which I am wanting to remove an object from is not located in the same Component, dont know if that matters. But for extra info.
First off, let's see why _task != task doesn't work as you need. Try this:
const a = { x: 10, y: 'hello' };
const b = { x: 10, y: 'hello' };
console.log(
a==b,
a===b,
Object.is(a,b)
);
Suprising, eh? Read this for more details.
Anyway, you should refactor your code to include an id property in your tasks, so that you can compare two tasks with their ids - no need to worry about weird implementations of object comparisons in JavaScript!
This should then work:
deleteTask(taskId) {
this.setState(prevState => ({
tasks: prevState.tasks.filter(task => task.id !== taskId)
}));
}
Equality operator in javascript compares the references of the object. So even if both object have same value, since they point to different instances, == will always return false. So as I see you have two options:
Refactor your code to include a id part and which that to compare two tasks.
Use some library like lodash or underscore for deep comparison

How filter works in Mongoose?

I have seen a tutorial that execute filter in mongoose schema
I want to know how this really work.
const isConversationExist = user.conversations.filter(conversation => (
conversation.userOneId === request.payload.friendId || conversation.userTwoId === request.payload.friendId)
,).length > 0)
Just break it up into pieces starting with
user.conversations.filter(A)
The filter() method creates a new array with all elements that pass the test implemented by the provided function (A).
In the code, the provided function (A) is written using ES6 arrow notation
conversation => (
conversation.userOneId === request.payload.friendId || conversation.userTwoId === request.payload.friendId
)
which can be rewritten (but not always) as
function (conversation) {
return conversation.userOneId === request.payload.friendId || conversation.userTwoId === request.payload.friendId;
}
It basically says, for each element i.e. conversation, test if either of the conversation's user IDs is equal to request.payload.friendId.
If the test passes (as in function returns true), then the element will be added to the new array; otherwise, the element will be ignored.
user.conversations.filter(A).length
This would be the size of the new array with all conversations that pass the test.
user.conversations.filter(A).length > 0
This would be a boolean whether the new array has conversations that pass the test or not i.e. isConversationExist.

Rendering a single element from an array of data in React

If I was to map through an array of objects of data and only wanted to render a single element from the array based on a certain condition like below:
dataArray.map((element, i) => {
if(i === currentElement){
return (
<div>{element.data}</div>
)
} else {
return null;
}
});
Would that be acceptable? this seems to return what I am after but was curious if this was the most efficient way to go about it because this still returns an array with a length of the data array with all null elements except on the desired single element.
Using map on an array will return another array with your function performed on each element, so the function you are using is literally pushing the value 'null' into the return array for any element that doesn't pass your condition. You could just use
dataArray.map((ele, i) => {
if(i === currentElement){
return (
<div>{element.data}</div>
)
}
and the map will simply do nothing with any element that does not pass the condition.
Mapping over an array in React is usually (often?) used for creating <li> tags, and React might get cranky in your console about not having a key. If you see that, check out this link here: https://reactjs.org/docs/lists-and-keys.html
You could use the find function to get the value you want to render rather than rendering a bunch of null values.
const element = dataArray.find((el, i) => i === currentElement);
if(element) {
return (<div>{element.data}</div>);
}

What will happen when React encounter an array nested in another?

When React render an array, a key should be provided for every item in this array. Otherwise, a warning will be given:
Warning: Each child in an array or iterator should have a unique "key" prop
There is detailed explanation in React Doc about how to identify keys and why we do this.
But in this case, I find that no warning is given when render an array nested in another.
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((numbers) =>
<li>{numbers}</li>
);
// 'listItems' is wrapped in an array
ReactDOM.render(
<ul>{[listItems]}</ul>,
document.getElementById('container')
);
After debugging, in React, I find function validateChildKeys, which is used to validate children's keys:
/**
* file path: react/lib/ReactElementValidator.js
* Ensure that every element either is passed in a static location, in an
* array with an explicit keys property defined, or in an object literal
* with valid key property.
*
* #internal
* #param {ReactNode} node Statically passed child of any type.
* #param {*} parentType node's parent's type.
*/
function validateChildKeys(node, parentType) {
if (typeof node !== 'object') {
return;
}
if (Array.isArray(node)) {
for (var i = 0; i < node.length; i++) {
var child = node[i];
if (ReactElement.isValidElement(child)) {
validateExplicitKey(child, parentType);
}
}
} else {
// do some other validation
// ..................
}
}
function ReactElement.isValidElement(child) is used to check if a child node should be checked.
ReactElement.isValidElement = function (object) {
return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
}
When checking nesting array, e.g. array = [array0,array1,array2], array0, array1, array2 are Array objects, which have no property named $$typeof, so function validateExplicitKey will never be called, child node will never be checked, of course, warning message will never be thrown.
I wonder:
Does React acquiesce this?
What impact that would have when re-render happens?
Does it have impact on performance? If does, how to avoid this case when I want to render this code:
const Page = (tables) => (
tables.map((table, idx) => (
[<Title/>,
...(table.get('forms').map((form, aidx) => (<Form/>)))
]
))
);
I think the case you're bringing up is actually described in the comment above validateChildKeys:
Ensure that every element either is passed in a static location, in an array with an explicit keys property defined, or in an object literal with valid key property.
Since your listItems array is only declared once and not recalculated on renders, React assumes it won't need keys to differentiate elements in subsequent renders.
If you move your array declaration into the render function you will see the error again, even if the array is nested:
const numbers = [1, 2, 3, 4, 5];
// 'listItems' is wrapped in an array
ReactDOM.render(
<ul>
{[numbers.map((numbers) => <li>{numbers}</li>)]}
</ul>,
document.getElementById('container')
);

Is there a way to test circular reference in JavaScript?

I'm making a game, and I've come across a problem... When I try to save, JSON fails and reports that circular reference is being made somewhere. I don't think it actually is, I can't see it, so is there an algorithm or anything which could tell me where it is exactly (between which objects and stuff)? Also, is there a JSON alternative that can save circular reference? I'm running a node.js server, I saw this, but I can't get it to work (it's not made as a module i can require() in my code).
If you want to serialize a circular reference so you can save it, you need to make the reference "virtual" in that it can't be serialized as a circular reference, since that would cause serialization to serialize the same circle of objects forever (or at least until the runtime has run out of memory).
So instead of storing the circular reference itself, you just store a pointer to the object. The pointer will just be something like ref : '#path.to.object' that can be resolved when you deserialize so you point the reference back to the actual object. You just need to break the reference on serialization to be able to serialize it.
Discovering a circular reference in JavaScript can be done by recursively iterating through all objects (with for (x in y)), store x in an array and compare each x with the identity operator (a.k.a. strict comparison operator) === for each z in the temporary array. Whenever x === z equals true, replace the reference to x with a placeholder that will be serialized to the above mentioned ref.
An alternative to keeping an array over "visited" objects is to "taint" the objects you iterate through by setting a property on them, like in this very naïve example:
for (x in y) {
if (x.visited) {
continue;
}
x.visited = true;
}
There is no good way to detect circularity in objects but it is possible though by walking the object tree and checking references. I baked up a node-walking function that tries to detect if a node has been already used as its parent
function isCircularObject(node, parents){
parents = parents || [];
if(!node || typeof node != "object"){
return false;
}
var keys = Object.keys(node), i, value;
parents.push(node); // add self to current path
for(i = keys.length-1; i>=0; i--){
value = node[keys[i]];
if(value && typeof value == "object"){
if(parents.indexOf(value)>=0){
// circularity detected!
return true;
}
// check child nodes
if(arguments.callee(value, parents)){
return true;
}
}
}
parents.pop(node);
return false;
}
And the usage would be isCircularObject(obj_value) where the function returns true if circularity exists and false if not.
// setup test object
var testObj = {
property_a:1,
property_b: {
porperty_c: 2
},
property_d: {
property_e: {
property_f: 3
}
}
}
console.log(isCircularObject(testObj)); // false
// add reference to another node in the same object
testObj.property_d.property_e.property_g = testObj.property_b;
console.log(isCircularObject(testObj)); // false
// add circular node
testObj.property_b.property_c = testObj.property_b;
console.log(isCircularObject(testObj)); // true
The key point being that an object value is equal with an other value only if it is the same object reference and not when it's another object (even if completely similar).
This is a small extension to Andris' answer that tells you where the first circular element is so you can deal with it accordingly.
function findCircularObject(node, parents, tree){
parents = parents || [];
tree = tree || [];
if (!node || typeof node != "object")
return false;
var keys = Object.keys(node), i, value;
parents.push(node); // add self to current path
for (i = keys.length - 1; i >= 0; i--){
value = node[keys[i]];
if (value && typeof value == "object") {
tree.push(keys[i]);
if (parents.indexOf(value) >= 0)
return true;
// check child nodes
if (arguments.callee(value, parents, tree))
return tree.join('.');
tree.pop();
}
}
parents.pop();
return false;
}
If you don't want a string, the tree array is unnecessary. Just change the original function to
return value;
for the circular object itself or
return parents.pop();
for its parent.
I was thinking about what you're trying to accomplish based off the initial code from your other question. Why not do something like this.
Player = function()
{
this.UnitTypeXpower = 2
this.UnitTypeYpower = 7
}
UnitTypeXAdd = function(owner)
{
owner.UnitTypeXpower++;
}
That way you don't have to use a circular reference and it accomplishes the same thing.
Here is the code that I am using to detect circular references, it uses the technique that was suggested in the accepted answer by asbjornu, whereby each value is walked through and its reference is maintained in an array so that the next value can be compared with those previously walked.
function isCircular(obj, arr) {
"use strict";
var type = typeof obj,
propName,
//keys,
thisVal,
//iterKeys,
iterArr,
lastArr;
if (type !== "object" && type !== "function") {
return false;
}
if (Object.prototype.toString.call(arr) !== '[object Array]') {
//if (!Array.isArray(arr)) {
type = typeof arr; // jslint sake
if (!(type === "undefined" || arr === null)) {
throw new TypeError("Expected attribute to be an array");
}
arr = [];
}
arr.push(obj);
lastArr = arr.length - 1;
for (propName in obj) {
//keys = Object.keys(obj);
//propName = keys[iterKeys];
//for (iterKeys = keys.length - 1; iterKeys >= 0; iterKeys -= 1) {
thisVal = obj[propName];
//thisVal = obj[keys[iterKeys]];
type = typeof thisVal;
if (type === "object" || type === "function") {
for (iterArr = lastArr; iterArr >= 0; iterArr -= 1) {
if (thisVal === arr[iterArr]) {
return true;
}
}
// alternative to the above for loop
/*
if (arr.indexOf(obj[propName]) >= 0) {
return true;
}
*/
if (isCircular(thisVal, arr)) {
return true;
}
}
}
arr.pop();
return false;
}
This code is available on jsfiddle, where you can test it for yourself.
I have also run some performance tests on jsperf.
Array.indexOf was only introduced as of Javascript 1.6, see MDN page
Array.isArray was only introduced as of Javascript 1.8.5, see MDN page
Object.keys was only introduced as of Javascript 1.8.5, see MDN page
It is also worth noting that arguments.callee is deprecated and forbidden in strict mode in preference to using named functions
This thread already contains some good answers. If you are looking for a way to detect circular references or compare two values while dealing with circular references, you should check out this library:
https://www.npmjs.com/package/#enio.ai/data-ferret
It has the following methods:
hasCircularReference(someValue) // A predicate that returns true when it detects circular reference.
isIdentical(valueA, valueB) // By calling setConfig(options) opt-in circular reference support, this function does an equality check that does not fall into an infinite recursion trap.
Under the hood, it uses a similar algorithm to what #Asbjørn Ulsberg describes, but cleans after itself by removing all flags inserted.
However, the primary difference between the algorithms discussed here and the suggestion is that it can deal with any number of native/custom, iterable/non-iterable classes, meaning it supports circular detection and value comparison beyond the JSON specification and JavaScript's Object and Array.
All that's required for it to handle other classes is to call:
registerClassTypes()
registerIterableClass()
This library comes with 100% code coverage, so you can check out how to use the API by reading the .spec files if you visit the GitHub page.
Disclaimer: I wrote this library, but I do think there is a legitimate reason to mention it as it gives additional features that you might need when dealing with circular dependencies.

Categories

Resources