Reactjs - Keys on custom components in list - javascript

We have an object 'Schema' with inputs for a form. We map through the inputs and return a custom component for each that then does the rest of the work. The problem is that:
1) Custom components can't have a key attribute, as key is a special property used by React and is not passed to the children.
2) The first item in a list must have a key attribute so that React can know how to update the components.
If your first item in the list is a custom Component, then these two are mutually exclusive. We can get around this problem by surrounding the QuestionBlock with a div or any other generic element and adding a key attribute to it. However this results in code and HTML that looks sloppy, having excessive divs just to work around this problem. Is there any other cleaner way to do it. Below is the code we had before:
render() {
return (
<div className="App">
{Object.keys(Schema.inputs).map((key,index) => {
let data = Object.assign({}, Schema.inputs[key]);
data.index = index;
return <QuestionBlock key={index} {...data} />;
}}
</div>
);
}
Warning shown in console when using the above code:
Warning: QuestionBlock: 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.
And below here is the code that works, but looks messy:
render() {
return (
<div className="App">
{Object.keys(Schema.inputs).map((key,index) => {
let data = Object.assign({}, Schema.inputs[key]);
data.index = index;
return <div key={index}>
<QuestionBlock {...data} />
</div>;
}}
</div>
);
}
Many thanks in advance!

Instead of wrapping using div, use a keyed Fragment.
Fragment is a container that doesn't generate HTML so it won't clutter your HTML output.
And also you can use a short-hand version <>, not <Fragment>

Related

React messed up states in list [duplicate]

I am using index to generate key in a list. However, es-lint generates an error for the same. React doc also states that using the item index as a key should be used as last resort.
const list = children.map((child, index) =>
<li key={index}> {child} </li>);
I considered using react-key-index.
npm install react-key-index gives following error:
npm ERR! code E404
npm ERR! 404 Not Found: react-key-index#latest
Are there any suggestions on other packages that allow to generate unique key? Any suggestion on react key generator is appreciated!
When you use index of an array as a key, React will optimize and not render as expected. What happens in such a scenario can be explained with an example.
Suppose the parent component gets an array of 10 items and renders 10 components based on the array. Suppose the 5th item is then removed from the array. On the next render the parent will receive an array of 9 items and so React will render 9 components. This will show up as the 10th component getting removed, instead of the 5th, because React has no way of differentiating between the items based on index.
Therefore always use a unique identifier as a key for components that are rendered from an array of items.
You can generate your own unique key by using any of the field of the child object that is unique as a key. Normal, any id field of the child object can be used if available.
Edit : You will only be able to see the behavior mentioned above happen if the components create and manage their own state, e.g. in uncontrolled textboxes, timers etc. E.g. React error when removing input component
The issue with using key={index} happens whenever the list is modified. React doesn't understand which item was added/removed/reordered since index is given on each render based on the order of the items in the array. Although, usually it's rendered fine, there are still situations when it fails.
Here is my example that I came across while building a list with input tags. One list is rendered based on index, another one based on id. The issue with the first list occurs every time you type anything in the input and then remove the item. On re-render React still shows as if that item is still there. This is đź’Ż UI issue that is hard to spot and debug.
class List extends React.Component {
constructor() {
super();
this.state = {
listForIndex: [{id: 1},{id: 2}],
listForId: [{id: 1},{id: 2}]
}
}
renderListByIndex = list => {
return list.map((item, index) => {
const { id } = item;
return (
<div key={index}>
<input defaultValue={`Item ${id}`} />
<button
style={{margin: '5px'}}
onClick={() => this.setState({ listForIndex: list.filter(i => i.id !== id) })}
>Remove</button>
</div>
)
})
}
renderListById = list => {
return list.map((item) => {
const { id } = item;
return (
<div key={id}>
<input defaultValue={`Item ${id}`} />
<button
style={{margin: '5px'}}
onClick={() => this.setState({ listForId: list.filter(i => i.id !== id) })}
>Remove</button>
</div>
)
})
}
render() {
const { listForIndex, listForId } = this.state;
return (
<div className='flex-col'>
<div>
<strong>key is index</strong>
{this.renderListByIndex(listForIndex)}
</div>
<div>
<strong>key is id</strong>
{this.renderListById(listForId)}
</div>
</div>
)
}
}
ReactDOM.render(
<List />,
document.getElementById('root')
);
.flex-col {
display: flex;
flex-direction: row;
}
.flex-col > div {
flex-basis: 50%;
margin: .5em;
padding: .5em;
border: 1px solid #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root">
<!-- This element's contents will be replaced with your component. -->
</div>
Of course, in React, you are required to pass in a unique key value for all elements of an array. Else, you will see this warning in console.
Warning: Each child in an array or iterator should have a unique “key” prop.
So, as a lazy developer, you would simply pass in the loop’s index value as the key value of the child element.
Reordering a list, or adding and removing items from a list can cause issues with the component state, when indexes are used as keys. If the key is an index, reordering an item changes it. Hence, the component state can get mixed up and may use the old key for a different component instance.
What are some exceptions where it is safe to use index as key?
If your list is static and will not change.
The list will never be re-ordered.
The list will not be filtered (adding/removing item from
the list).
There are no ids for the items in the list.
Key should be unique but only among its siblings.
Do not use array indexes as keys, this is an anti-pattern that is pointed out by the React team in their docs.
It's a problem for performance and for state management. The first case applies when you would append something to the top of a list. Consider an example:
<ul>
<li>Element1</li>
<li>Element2</li>
<li>Element3</li>
</ul>
Now, let say you want to add new elements to the top/bottom of the list, then reorder or sort the list (or even worst - add something in the middle). All the index-based key strategy will collapse. The index will be different over a time, which is not the case if for each of these elements there would be a unique id.
CodePens:
Using index as a key: https://reactjs.org/redirect-to-codepen/reconciliation/index-used-as-key
Using ID as a key: https://reactjs.org/redirect-to-codepen/reconciliation/no-index-used-as-key
Play with it and you'll see that at some point the index-based key strategy is getting lost.
In map method ,react render our array element with respect to key. so key plays a vital role. if we use index as key then in this case , if we re-order our components then , we will get problem because react render our array element with respect to key.
For more details go through this video https://www.youtube.com/watch?v=Deu8GE3Xv50&list=PLBHcDEyoOGlQ4o_Nx6FCXmjYlOAF2am6V&index=4&t=363s
use the following lib "react-uuid" : https://www.npmjs.com/package/react-uuid.
react-uuid basically create random ids when you call it each time.
import React from 'react'
import uuid from 'react-uuid'
const array = ['one', 'two', 'three']
export const LineItem = item => <li key={uuid()}>{item}</li>
export const List = () => array.map(item => <LineItem item={item} />)
and this should solve the issue.

React gives wrong index to Child with React.memo

I've got a problem in my project which is pretty large therefore I can't post every part of the code here, but I'll try to summarize the main parts.
I've got a parent component which state is managed through a useReducer and which render returns a mapping of this state.
Each child has to take both the value and the index of the mapping. I'm also using the Context API to pass the dispatcher
on some of the childs.
function ParentComponent(props) {
const [state, dispatch] = useReducer(initialData, reducer);
return (
<div>
<MyContext.Provider value={dispatch}>
{state.myArray.map((value, index) => (
<ChildComponent value={value} index={index} />
))}
</MyContext.Provider>
</div>
);
}
/* Other file */
function ChildComponent({value, index}) {
const dispatch = useContext(MyContext);
return <div>
{/* Uses value and index to display some data */}
</div>
}
export default React.memo(ChildComponent, (prevProps, nextProps) => !_.isEqual(prevProps, nextProps));
Some of the childs in the tree components have to use React.memo to avoid useless re-renders, ChildComponent is one of them.
I'm using the lodash functions _.isEqual to compare the props to know when the component has to re-render.
The problem is the following. One of my components in the component tree adds items to the myArray attribute of the ParentComponent state.
When I add an item, anyway, each ChildComponent rendered from the ParentComponent mapping receives the same index of 0, creating a lot of problems.
The ParentComponent has the right indices (when I log them inside the mapping they are the right ones) but it's like the ChildComponent isn't getting it.
Is there any way to solve this? Maybe it has something to do with React.memo?
React.memo() takes a callback that should determine whether prevProps and nextProps are equal, but you're returning the negation of that.
Also using the index as a key should be considered a last resort, since this will fail to behave correctly when elements in the array are re-ordered relative to each other. You should always source the key from the data whenever possible.

Nested component list does not update correctly

I have a recursively defined component tree which is something like this:
class MyListItem extends Component {
...
componentDidMount() {
this.listener = dataUpdateEvent.addListener(event, (newState) => {
if(newState.id == this.state.id) {
this.setState(newState)
}
})
}
...
render() {
return (
<div>
<h1>{this.state.title}</h1>
<div>
{this.state.children.map( child => {
return (<MyListItem key={child.id} data={child} />)
})}
</div>
</div>
)
}
}
So basically this view renders a series of nested lists to represent a tree-like data structure. dataUpdateEvent is triggered various ways, and is intended to trigger a reload of the relevant component, and all sub-lists.
However I'm running into some strange behavior. Specifically, if one MyListItem component and its child update in quick succession, I see the top level list change as expected, but the sub-list remains in an un-altered state.
Interestingly, if I use randomized keys for the list items, everything works perfectly:
...
return (<MyListItem key={uuid()} data={child} />)
...
Although there is some undesirable UI lag. My thought is, maybe there is something to do with key-based caching that causes this issue.
What am I doing wrong?
React uses the keys to map changes so you need those. There should be a warning in the console if you don't use unique keys. Do you have any duplicate ids? Also try passing all your data in as props instead of setting state, then you won't need a listener at all.

React weird rendering behavior

Even though printing items logs a populated array before the return function, it doesnt really render anything. I know for a fact its not a problem with improperly displaying the html. So i got suspicious and stringified it inside the return function to see if indeed the data im logging is there and to my dread i realised it isnt. As shown in the code, within the return function i get an empty array!
class Derp extends Component {
constructor(props){
super(props);
mainStore.subscribe(this.render.bind(this));
}
render(){
var items = mainStore.getState().itemReducer.items;
console.log(items); //yields an array of items as expected
return (
<div>
<span>{JSON.stringify(items)} </span> //yields [] in the DOM !!!!!!!
//when it should yield the same as above, a fully populated array
{
items.map(item =>
<div key={item.id}>
{item.name}
</div>
)
}
</div>
)
}
}
I've done this numerous times succesfully but this time around i just cant figure out what could be wrong with it.. Thanks for taking the time.
EDIT 1: I know this will seem cringeworthy ( because it is ) but the component is listening to all state changes like so : mainStore.subscribe(this.render.bind(this)); so it should always have access to updated data.
P.S: I am aware of dumb vs clever components and that im not using ReactRedux, im just experimenting and trying a few different things for curiosity's shake. This is an self-imposed "study" kind of code. This isnt meant for production or a project.
Return the div from your map function:
items.map(item =>
return <div key={item.id}>
item.name
</div>
)
Try escaping the content you wish to render:
items.map(item =>
<div key={item.id}>{item.name}</div>
)
There are two problems I see with your code:
You are trying to return more than one element. If you're before react16, you need to wrap those two elements in a div or something. If you're on react16, you can return them in an array.
You item.name needs to be in curly braces. Any JS within JSX markup needs to have curly braces. (This is how they know it's JS and not markup).
react16+
return [
<span>{JSON.stringify(items)} </span>,
...items.map(item =>
<div key={item.id}>
{item.name}
</div>
)
]
< react16
return (
<div>
<span>{JSON.stringify(items)} </span>
{items.map(item =>
<div key={item.id}>
{item.name}
</div>
)}
</div>
)
It seems like the problem was calling render directly. Instead calling forceUpdate() works fine. I am not 100% why this happens but i suspect that calling render on it's own doesnt mean much as it would probably need to be called in a React context in the React pipeline. I might be horribly off and if so please flag me and describe it a little better that i currently am able to.
Thanks everyone for helping.

Render multiple React components into a single DOM element

I'm looking to render multiple modals into a single ReactDOM element. Here's the HTML structure that React renders to.
<body>
<div id="modal-socket"></div> // Insert multiple here
<div id="wrapper">
// Other content goes here
</div>
</body>
There's a long story behind why I need to render multiple components into #modal-socket but I want to do something akin to this:
ReactDOM.render(<AddMeasurableModal />, document.getElementById("modal-socket"));
ReactDOM.render(<AddMeasurableModal />, document.getElementById("modal-socket"));
ReactDOM.render(<AddMeasurableModal />, document.getElementById("modal-socket"));
Obviously this replaces the current content of #modal-socket on each render call.. So I don't get my end result. Boo.
Did a search and found a few answers on it but none meet my needs.
Cheers.
As you told in a comment, the dynamic way would be something like this
Inside of a main component you could do:
Imagine having an array like:
let myArray = [
{
prop1: 'hello world'
},
{
prop1: 'Hey there!'
}
]
//Then in the render function (you can put that array into the state or something)
render(){
return (
<div>
{myArray.map((entry,index) => {
return <AddMeasurableModal key={index} {...entry} />
})}
</div>
)
}
this will create as many AddMeasurableModal components as there are entrys in the myArray variable and add every property stored as props onto the component (In this case, every AddMeasurableModal component has access to the this.props.prop1 value, because of the {...entry} spread syntax)
Notice how I only put myArray.map() into the render function inside of {}?
React renders every array of components without further configuration inside of the render function. And Array.map() returns an array. Just make sure to return only valid react elements! When doing this, don't forget to add a uniqe key prop to each element to avoid warnings.
EDIT: in this case, the key prop is the current index in the array, but when fetching data from a server I would recommend to use a uniqe id from the database or something to avoid rendering bugs.
If you don't want to map over an array, you can just set a number of components and then loop over them, creating an array of components and put them into the render function.
Wrap your multiple modals into 1 container and render that, eg:
let modals = (
<div>
<AddMeasurableModal />
<AddMeasurableModal />
<AddMeasurableModal />
</div>
);
ReactDOM.render(modals, document.getElementById("modal-socket"));

Categories