Using object instance as a key in React list - reactjs

I have a list of objects which I want to render as a list using React.
React requires the special key attribute on the list element in order to track changes between DOM and virtual DOM.
My object doesn't have any special unique properties like id, in fact, two objects could even have the same properties, but the object itself is unique.
Is there a way to use object's reference as a key to render React list?
Otherwise, what workarounds could be suggested in this case?
Also, I'm receiving the list of items from a third party, so I can't just attach a random ID to each item, because it will make the React to re-render all items each time I receive updated data.

If you don't have a unique ID (returned by the back-end, or you can't build a composite key on the front-end), then you can use the array index as key:
render() {
return array.map((item, index) => <Item key={index} />)
}
Without having an unique ID, whatever approach you choose, if the order of the items is changed, then the list can get totally messed up.
It would be safe enough to use this approach if your case met the following 3 conditions, thanks to this article:
the list and items are static–they are not computed and do not change.
the items in the list have no ids.
the list is never reordered or filtered.

Using array's index as a key would be a disaster, because if array changes, the view will not be properly updated or will be updated partially in an unpredictable manner.
The only solution I've found is to generate the dummy IDs as suggested in the comments.
The idea is to implement the generator, which will create an ID for each item, but at the same time will remember previously generated IDs.
The WeakMap JavaScript class will allow us to store the mapping information between the item object reference and generated ID:
export class IdGenerator {
// Using WeakMap as index
map = new WeakMap();
// Internal counter
lastId = 0;
getItemId(item) {
// Getting previously generated ID for the specified item
let id = this.map.get(item);
// If item is new, generating the ID and storing it for future use
if (undefined === id) {
id = ++(this.lastId);
this.map.set(item, id);
}
// Returning ID to the caller in either case
return id;
}
}
Then it could be used to assign generated IDs to each item before rendering the list:
class ListBox extends Component {
// Instantiating the generator
itemsIdGenerator = new IdGenerator();
render() {
return (
<ul>
{this.props.items.map(item => (
<li key={this.itemsIdGenerator.getItemId(item)}>
{item.title}
</li>
))}
</ul>
);
}
}
Make sure to use different instance of the generator for each instance of the component though and create generator only once with the component.
Also, calling the generator for each item in component's render method could be sub-optimal. However, I didn't want to touch the original items or mutate them. If this is an issue, consider using various lifecycle methods provided by React to set IDs when props changes.

Related

Dynamic, unique values for DOM elements

I want advice on if my solution is conventional and/or if there are better, more common approaches using React for this situation:
I have a comments section. I retrieve a list of comments from the server as a prop to my functional component. Within the JSX, I map through each comment and, for each, return a TextArea to display the comment. One of the functionalities is that if you created the comment an edit button will appear, rendering the textarea editable.
This means that I need a unique identifier for each textarea, because once the user edits the comment's content and clicks the save button, I need to post to my server with the correct _id of the comment in the DB, along with the updated content.
My most idiomatic solution thus far is to use refs, code below:
I create a ref containing an array, a function to pass objects mapping the comment._id to the element, and add that to the commentRefs array whenever the function is called. The function to add to the ref array is the ref proeprty's value for the textarea:
const addCommentRef = _id => el => {
if(el && !commentRefs.current.includes(el) && el) {
const commentRef = {
_id,
el,
content: '',
}
commentRefs.current.push(commentRef);
}
}
<textarea ref={addCommentRef(comment._id)}
On the textarea onChange, I call a function called editCommentRef, which maps through the commentRefs, compares the ID's passed as param with each _id value in the array and if found, updates the content to what has been typed into the textarea:
const editCommentViaRef = _id => event => {
for(let i = 0; i < commentRefs.current.length; i++) {
const commentRef = commentRefs.current[i];
if(_id == commentRef._id) {
commentRefs.current[i] = {
...commentRefs.current[i],
content: event.target.value
}
}
}
}
<textarea onChange= {editCommentRefs(comment._id)}...
Lastly, once the save button is clicked, its onClick handler calls a 3rd function named "saveCommentEdit", where it passes the _id of the comment. We again traverse through the commentRefs array and find the _id param matching the _id of the saved object. Once found, I have what I need to post to the server/db and update the comment:
const saveCommentEdit = _id => {
commentRefs.current.forEach((comment) => {
if(_id == comment._id) {
console.log('found comment to save to db ', comment._id + ' saving content - \n' + comment.content);
}
});
}
<button onSubmit={saveCommentEdit(comment._id)} ...
This works, but is this too complicated, and are there any better and common solutions? Other solutions I have tried/considered:
States - I imagine I would have the same issue with states as the state names need to be referred to in code, so I'd need a different variable name for each state. Also, states would require the entire component to re-render with every character typed into the textarea which seems more costly than refs.
Document selectors - The simplest solution is to call a function which targets the DOM for the elements using "findElementById" and etc.. in which case I provide a unique identifier by making the ID of each textarea contain the comment._id. The obvious issue here is that targeting the DOM directly in a React app is discouraged, and fundamentally contradicting to the React framework.
Thanks in advance!
You don't need to use Ref - it's meant for something else (to communicate with the DOM) and your task is different. You can store the edit state and record id from the database in each component. You can store the author's id to know exactly who can edit. On the server, update the record, provided that the id and author are the same. Also, you do not need to do any loops, a comment component can take care of sending data itself according to OOP.
For example you may have a table comments in your DB:
id
post_id
author_id
comment
1
1000
user100
cool
2
1001
user42
hello
Then get all comments by post_id if current user_id equal author_id you can pass to component property editable for show button edit/save (caption depends of mode)

why do I get "`key` is not a prop." in React?

So I have the code below, with bookList_object being a bunch of JSON objects I fetched from backend.
_renderBookList = () => {
const books = this.state.bookList_object.map((this_book, index) => {
return <BookList
body={this_book.body}
created_at={this_book.created_at}
key = {index}
/>
})
Now, I've learned that there has to be key when having multiple children. So I've used index parameter to assign each id to the key prop. But then I get this error:
"BookList: key is not a prop. Trying to access it will result in undefined being returned. If you need to access the same value within the child component, you should pass it as a different prop."
What have I done wrong?
Also, I already have id field in each of my object. Is there a way to use id instead of key?
Thank you very much in advance. :)
I recently had this Warning. It happened because I tried to assign a key to some elements in two different parts of the code. The warning pops up when the key prop is accessed a second time.
Find out where else the component is being assigned a key. Then choose only one place to assign a key.

Each child in an array or iterator should have a unique "key" prop. Not sure why

const list = props.things.toDo.map(
function(thing) {
return(
<li>{thing.name}</li>
);
}
);
Cant figure this out keep getting this react warning?!
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:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
The best way to pick a key is to use a string that uniquely identifies a list item among its siblings. Most often you would use IDs from your data as keys:
const list = props.things.toDo.map(
function(thing) {
return(
<li key={thing.id}>{thing.name}</li>
);
}
);
When you don’t have stable IDs for rendered items, you may use the item index as a key as a last resort:
const list = props.things.toDo.map(
function(thing, index) {
return(
<li key={index}>{thing.name}</li>
);
}
);
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.
Reference: https://reactjs.org/docs/lists-and-keys.html#keys
As a quick fix, try this:
<li key={thing.name}>{thing.name}</li>
Then read up on the React docs for Lists and Keys.
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.
You'll probably want to replace the key with a truly unique value, if thing.name is not guaranteed to be the same across items.
const list = props.things.toDo.map((x, i) => <li key={i}>{thing.name}</li>);
Your code can be condensed down to the above code instead. As other has mentioned, whenever we rendering a list, React will need each and every individual element to be unique which in that case it needs a key prop, but instead of using key={thing.name} which I believe there is a chance that thing.name is not unique, I would normally use index instead which is 100% guaranteed to be unique

Reactjs Filtering a huge array of objects best practices

in a given app, lets say i have an array that hold 100,000+ object.
objects look like following
let collection = [{
name:'name',
price: 10,
group_id: 1
},
{
name:'name 2',
price: 8,
group_id: 2
},
];
now lets say i have a view which show all 100,000 object, and allow users to filter list by price range or group_id.
--given that such list can be a static list loaded from remote json, or a redux saved collection.
what is best method to filter this filters using price & group filters ?
my first thinking was to keep another array called "filtered_list" in the state of parent component, and when ever a filter change, the filtered_list will reconstruct by filtering parent Collection (looping 100,000+ object again).
but this look not so performant, so i consider may be i should write some logic that decied if the new filter should use the already filtered_list or use parent collection
example :- if group_filter was group_id = 1, then a price filter was added then i should filter only filtered_list, since all inputs of group_1 is already in filtered_list, otherwise if group_id changed from 1 -> 2, then i must refilter the whole big collection, because filtered_list doesnot contain group_id =2 items, its on collection only...
this bring me to another problem.. where should i maintain the filtered_list variable ?
if i added it to redux store, this violate princible of redux that store should only hold information needed to build data, and not calculated data.
const App = props =><div>
<aside>
<Filtes />
</aside>
<article>
{props.collection.map(item=><label>item.name</label>)}
</article>
</div>
export default connect( ({collection})=>({collection}) )(App)
I've been using the reselect library which is recommended in the Redux docs for computing derived data:
Reselect is a simple library for creating memoized, composable
selector functions. Reselect selectors can be used to efficiently
compute derived data from the Redux store.
http://redux.js.org/docs/recipes/ComputingDerivedData.html
So you could have a selector that looked something like this:
const getGroupdId= state => state.selectedGroupId
const getIteams = state => state.collection
const getVisibleItems= createSelector(
[getItems, getGroupId],
(items, groupId) =>
items.filter(i=> i.group_id === groupId)
)
Since the selector is memoized, it will only be recomputed when the selected groupId changes. I've also combined reselect with an ImmutableJS state in the past as well, which in my experience increased performance when querying large data structures.

React state array updates/renders entire array, is there a work-around?

Here's the basic idea...
constructor(props) {
super(props);
this.state = {
children: [{id:1},{id:2}], // HERE ARE THE TWO OBJECTS
style: {top: 0}
};
}
Now let's say I update one of those two objects but those objects are mapped to an array of components.
<div className="thread">
{this.state.children.map((child) =>
<Post
key={child.id}
id={child.id}
/>
)}
</div>
Now... when I update one of those objects...
changeID (id, newID) { // this is a different function, just an example
let temp = this.state.children;
for (let i = 0; i < temp.length; i++) {
if (temp[i].id === id) {
temp[i].id = newID; // <- update this
}
}
this.setState({
children: temp // <- plug temp[] back as the updated state
});
}
I throw the new state back in, but it updates each of the mapped objects.
// on one change to one element
id 1 POST UPDATED
id 2 POST UPDATED
1) Does it re-render EVERY component (in the mapped array) or is it smart enough to tell that the state values passed as props to the mapped component are the same?
2) Is it incredibly expensive in processing power if that array is significantly longer?
3) If yes, how can I go about this better? Constructive criticism is appreciated.
Thank you!
This is how react works, whenever you change any state variable it will re-render the complete component and update the virtual dom not the actual dom, then it will check the diff between these two and change only that particular element in actual dom.
As per DOC:
React provides a declarative API so that you don't have to worry about
exactly what changes on every update.
React Only Updates What's Necessary:
React DOM compares the element and its children to the previous one,
and only applies the DOM updates necessary to bring the DOM to the
desired state.
DOM Elements Of The Same Type:
When comparing two React DOM elements of the same type, React looks at
the attributes of both, keeps the same underlying DOM node, and only
updates the changed attributes.
For Example:
<div className="before" title="stuff" />
<div className="after" title="stuff" />
By comparing these two elements, React knows to only modify the className on the underlying DOM node.
Use of Keys:
React supports a key attribute. When children have keys, React uses
the key to match children in the original tree with children in the
subsequent tree.
Imp:
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. Keys used within arrays should be unique among their siblings. However they don't need to be globally unique.
For example:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
Now React knows that the element with key '2014' is the new one, and the elements with the keys '2015' and '2016' have just moved.
Check this article: How Virtual-DOM and diffing works in React

Resources