Component is not re rendering after state change in react - reactjs

While working on my project I faced this issue and tried for two days to solve it. Finally I decided to post it here.
I am rendering array by calling child component as follows:-
Parent component
{this.state.list.map((item,index)=>{
return <RenderList
insert={this.insert.bind(this,index)}
index={index}
length={this.state.list.length}
EnterPressed={this.childEnterPressed.bind(this,index)}
list={item}
onchange={this.childChange.bind(this)}
editable={this.state.editable[index]}
key={index}
onclick={this.tagClicked.bind(this,index)} />
})}
and
Child component
<Draggable grid={[37, 37]}
onStop={this.handleDrop.bind(this)}
axis="y"
onDrag={this.handleDrag.bind(this)}
bounds={{top: -100, left: 0, right: 0, bottom: 100}}
>
<div onClick={this.tagClicked.bind(this)}>
{ this.props.editable ? (<Input
type="text"
label="Edit Item"
id="listItem"
name="listItem"
onKeyPress={this.handleKeyPress.bind(this)}
defaultValue={this.props.list}
onChange={this.onChange.bind(this)} />) :
(<Chip>{this.props.list}</Chip>) }
</div>
</Draggable>
I am using react-draggble to make each individual item of my list draggble. When ot is dragged in handleDrag function I am just updating my state of distance by which component is draged and in handleDrop function I am calling insert function which delete the dragged item from its current position and insert at position where it is dropped.
insert=function(index,y,e){
if(y!==0){
var list=this.state.list;
var editable = Array(this.state.editable.length).fill(false);
if(y/37>0){
var temp=list[index];
var i=0;
for(i=index;i<index+y/37;i++){
list[i]=this.state.list[i+1];
}
list[(y/37)+index]=temp;
this.setState({
...this.state,
list:list,
editable:editable
})
}
}
State is updating correctly. I have tested it but component is not re rendering. what surprised me is that if I just display the list in the same component then it re render correctly.any help is appreciated!

React cannot keep track of the items, as your are using the index as key (which is discouraged). Try to create a unique key based on something like a generated uuid which sticks to the item.
When dragging an item, the position (index) in the array changes. This prevents React from syncing the correct elements. Prop/state changes may not propagate as wanted.

Related

Why does removing the correct item in state array, not delete the right component in the UI? - React

When I console.log the index that is being deleted it shows the correct index to be deleted, but the behaviour does not correspond - it always only deletes the last item in the array on the UI. (but in the state array it is deleting the correct index)
example:
(refer to image below)
When I click on delete for the first item (index 0), it removes the last item (index 3) from the UI. But upon viewing the console.log, it actually removed the correct item (index 0) from the state array.
How it looks in the UI:
Note that the red numbers are there just for me to know if the array index is correct. (and it is working correctly)
EditUser.js
// holds the array of data for each RoleMapInput component
const [roleMaps, setRoleMaps] = useState([{organisation:[], roles:[], type:[]}]);
// Used to add a RoleMapInput component to the UI
const handleAddRoleMap = () =>{
setRoleMaps((oldRoleMaps) => [...oldRoleMaps,{organisation:[], roles:[], type:[]}])
}
// Used to delete a RoleMapInput component from the UI
const handleDelRoleMap = (delIndex) =>{
console.log("delIndex: ", delIndex)
setRoleMaps((oldRoleMaps) => {
console.log(oldRoleMaps)
return oldRoleMaps.filter((_,index)=>index !== delIndex)
})
...
...
...
return(
...
// Where the RoleMapInput component is duplicated based on the array roleMaps
{console.log("roleMaps at grid: ",roleMaps)}
{roleMaps.map((roleMap, index) => (
<RoleMapInput key={index} roleMapIndex={index} roleMap={roleMap} handleDelRoleMap={handleDelRoleMap}/>
))}
...
)
RoleMapInput.js
export default function RoleMapInput(props) {
...
...
return (
<>
<Grid item xs={1}>
<FormControl fullWidth>
<IconButton aria-label="delete" color='error' onClick={()=>props.handleDelRoleMap(props.roleMapIndex)}>
{props.roleMapIndex}
<DeleteOutlineOutlinedIcon/>
</IconButton>
</FormControl>
</Grid>
</>
);
}
Without the minimum reproducible example is really hard to tell. But I can tell you this: the filter function is working properly, but using as react docs mention:
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.
You can read more about this here
A possible explanation is that when you are removing the element on pos 0 react might consider that it doesn't need to re-render any child component, but rather remove the last element (because you had rendered <RoleMapInput components with keys 0,1,2,3 and after removing any item from the array you are left with keys 0,1,2)

Office UI Fabric: ScrollablePane's Position is not being reset to the initial position when details list is updated

Replication Steps:
https://developer.microsoft.com/en-us/fluentui#/controls/web/scrollablepane
In the 'DetailsList Locked Header' scroll down to a non zero position exceeding the first page and enter 'sed' in the 'filter by name'. This updates the detailsList and the scrollbar does not go to the initial position.
Bug/Ask:
As we scroll down to let's say a position of 50 and now if there is an update to the details list with say suppose 350. The scrollbar is not returning to the starting position and it rerenders to a random position close to the top(in my case).
Approach/Temporary Hack:
Going through the code, I noticed that the initial scroll position of the scrollablepane is being refreshed only when the 'initialScrollPosition' props change in the implementation.
I tried changing the 'initialScrollPosition' prop in my component whenever the detailList updates by setting it to either 0 or 1.
Debugging this, the props for the initialScrollPosition did not change inside the ScrollablePane component did not change.
This did not work and the scrollable pane is still not set to the start.
Has anyone found a workaround or a solution for this?
I Had the same issue
I'm also using infinitive scroll(react-infinite-scroller) to load the data.
what I finally done , was replace scrollablepane by css.
(Function handleGetNext just loading new data, and variable dataToRender I keep on the state to make concatenation between old state and new)
<div>
{handleSelect()}
<div style={{ overflowY: "scroll", height: 500 }} id="containers">
<InfiniteScroll
pageStart={0}
loadMore={() => {
handleGetNext(overviewData.page);
handleSelect();
}}
hasMore={!loadingNext && !!overviewData && overviewData.page < overviewData.totalPages}
initialLoad={false}
useWindow={false}
>
<ShimmeredDetailsList items={dataToRender} enableShimmer={overviewLoading} .........../>
</InfiniteScroll>
</div>
</div>
And later what is tricky you need to set scroll position, depends where you want to scroll.
const handleSelect = () => {
const elemToScrollTo = document.getElementById("containers");
if (!!elemToScrollTo) {
elemToScrollTo.scrollTop = 2100;//example
}
};

How do I make children's props update when parent state changes for children initialized in a map function?

I have a bunch of child components being initialized in a map function in ComponentDidMount:
this.mappedApplications = this.props.applications.map((application, key) =>
<ApplicationCard
selectedYear={this.state.openApplicationYear}
applicationYear={application.application_year}
handleArrowClick={this.handleApplicationCardArrowClick}
key={key}
/>, this
);
this.setState({
applications: this.mappedApplications,
});
When the state.openApplicationYear changes I want it to change for all of the Application Cards. I do a check in the application card show info if the selected year matches the application year, with the goal that only one card will be open at a time. The cards have dropdown arrows that allow the user to open or close them. Right now the parent's openApplicationYear is changing correctly, but the children's aren't updating.
Here is the render function if it helps:
render() {
return (
<div className="dashboard__card">
<h3 className="mb-3">Applications</h3>
{this.state.applications}
</div>
)
}
I have bound "this" to the map function as seen above. Is there anything else I need to do to update the children when the parent's state changes?

error: Do not use Array index in keys

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 rerender only one child

in my form i have a few dropdown components. Whenever first dropdown option changes i want to update props for the second dropdown and rerender it. My code looks like this
handleProjectChange(option) {
//this.setState({ selectedProject: option })
this.refs.phase.props = option.phases;
//this.refs.forceUpdate()
this.refs.phase.render()
}
render() {
var projectOptions = this.projectOptions
var defaultProjectOption = this.state.selectedProject
var phaseOptions = defaultProjectOption.phaseOptions
var defaultPhaseOption = phaseOptions[0]
var workTypeOptions = api.workTypes().map(x => { return { value: x, label: x } })
var defaultWorkTypeOption = workTypeOptions[0]
return (
<div>
<Dropdown ref='project' options={projectOptions} value={defaultProjectOption} onChange={this.handleProjectChange.bind(this)} />
<Dropdown ref='phase' options={phaseOptions} value={defaultPhaseOption} />
<Dropdown options={workTypeOptions} value={defaultWorkTypeOption} />
<button className="btn btn-primary" onClick={this.handleAddClick.bind(this)}>Add</button>
</div>
)
}
But props are not changed, so it rerenders the same options. At the moment im just rerendering entire form by setting new state on it. Is there any way to rerender only one child/Dropdown with new props?
The way to do this is to put the selected option in first dropdown selectedProject in state.
And inside your render function, fetch/ populate the options in the second dropdown, dependent on the selected project.
Flow will then be:
User selects an option in the first dropdown.
This triggers handleProjectChange()
Inside handleProjectChange(), the newly selected option is put in state, by a this.setState() call
Because state changed, react re-runs the entire render() function.
Under the hood, react figures out that only the second dropdown has changed, so react will only re-render the second drop-down on your screen/ in the DOM.
Although React does have a reconciliation algorithm that dynamically checks whether each component should be rerenader or not in every rendering of its parent, it doesn't always work as we intended.
https://reactjs.org/docs/reconciliation.html
For this kind of issues, you have two options. You can use either React.pureComponent or React.useMemo().

Resources