Removing an Item Causes React to Remove the Last Dom Node Instead of the One Associated with That Item

Removing an item causes React to remove the last DOM node instead of the one associated with that item

You're experiencing this problem because you're using index as your key:

let nodes = items.map((item, index) => {
let idx = index
return (<Item key={index} value={item} index={index} _delete={this._onDelete}/>)
})

React uses the key property during virtual DOM diffing to figure out which element was removed, but indexes will never serve this purpose sufficiently.

Consider this example: you start off with the following array, which results in the following DOM structure:

const arr = [2, 4, 6, 8];

<li key={0}>2</li>
<li key={1}>4</li>
<li key={2}>6</li>
<li key={3}>8</li>

Then imagine you remove the element at index 2. You now have the following array, and the following DOM structure:

const arr = [2, 4, 8];

<li key={0}>2</li>
<li key={1}>4</li>
<li key={2}>8</li>

Notice that the 8 now resides in index 2; React sees that the difference between this DOM structure and the last one is that the li with key 3 is missing, so it removes it. So, no matter which array element you removed, the resulting DOM structure will be missing the li with key 3.

The solution is to use a unique identifier for each item in the list; in a real-life application, you might have an id field or some other primary key to use; for an app like this one, you can generate a incrementing ID:

let id = 0;
class List extends Component {
constructor() {
this.state = {
items: [{id: ++id, value: 1}, {id: ++id, value: 2}]
}

// ...
}

_onClick(e) {
this.state.items.push({id: ++id, value: Math.round(Math.random() * 10)})
this.setState({items: this.state.items})
}

// ...

render() {
let items = this.state.items
let nodes = items.map((item, index) => {
let idx = index
return (<Item key={item.id} value={item.value} index={index} _delete={this._onDelete}/>)
})

// ...
}
}

Working example: http://jsbin.com/higofuhuni/2/edit

DOM remove method in react js

You should likely use a ref:

https://reactjs.org/docs/refs-and-the-dom.html

So you attach the ref to the DOM element, then you can imperatively remove that element just like you specified.

So in the component function body:

const myRef = useRef();

Then in the component render:

<Component ref={myRef} />

Then you can use the code in your question to remove the element from the DOM.

ref.remove();

If you need to do it when the page loads look at using useEffect to achieve this. Although if you're removing an element on page load, I question why you even need the element there in the first place ;).

React CSSTransitionGroup deleted item shifted to end

Your remove statement goes wrong because of react's key specifics.
See explanation here of dynamic components (which is what you have), and how to use them with 'key`.

To repair, change your return statement inside the mapping of children to:

return <Item key={item} item={item}/>;

And create a new (pure) component called <Item>:

var Item = React.createClass({
render: function() {
return <div>{this.props.item}</div>;
}
});

I tried this in your fiddle and it works. Hopefully this link will give you the updated fiddle.

BTW: In line with fiddle, my changes are quick and dirty solution: react expects keys to be unique AND related to the content of the item in the list. My example does meet the second requirement (Please do NOT use the mapping index as the key), but it fails on the first: if you add, then delete, then add, the example code would produce a next item with the same number (not unique), so it fails.

Remove dynamic rendered element from dom in ReactJS

The problem is that in the first render you have {cards.length} calls to hook "useState" within GeraCard, but after deletion of one card, you will have {cards.length-1} calls to hook "useState". As the React docs state:

Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function. By
following this rule, you ensure that Hooks are called in the same
order each time a component renders. That’s what allows React to
correctly preserve the state of Hooks between multiple useState and
useEffect calls.

You should extract the content of map callback into separate a component.

const GeraCards = (cards, cart = false) => {
return cards.map((v, i) =>
<GeraCard card={v} index={i} cart={cart} />
);
};

const GeraCard = ({ card, index, cart }) => {
const [show, setShow] = useState(true);
const v = card;
return (
<div key={index}>
{show ?
<div className={styles.card}>
<div onClick={() => urlRender(v.url)} className={styles.cardContent}>
<div>
<span className={styles.cardTitulo}>{v.Nome}</span>
</div>
<div>
<span className={styles.cardData}>{v.Data}</span>
<span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
</div>
{cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
</div>
<span className={styles.trash}>
<FontAwesomeIcon
icon={faTrash}
color={"#3c3c3c77"}
onClick={(e) => {
setShow(false);
e.persist()
TrashHandler(v.Nome, e)
}}
/>
</span>
</div> :
null
}
</div>
);
}

React list rendering wrong data after deleting item

It's because you're using key={index} instead of a value unique to the student.

When the array is modified the students after the index removed will have the wrong key and React will not register a key change to rerender with the updated data.

You should instead use something like this...

<div key={student.name}>

assuming that student.name is unique.

React Context API context API issues , when adding and removing an items from UI

After a day or so tinkering around with your sandbox I decided it had enough going against it that it needed more of a rewrite. The biggest issue was the storing of <Item /> component JSX into the component state. Storing JSX into component state is anti-pattern in React.

Solution

Give each row component an id that can be used to identify it in state. Pass the row id with each dispatched action to add/remove items from that row's data.

Adding items

appContext - Convert the items array to a rows object

const CentContext = createContext({
Icons: null,
rows: {},
addItem: () => {},
removeItem: () => {}
});

appReducer - When adding items store only a new element id. When removing items used the passed element id to filter the row's data.

import { v4 as uuidV4 } from "uuid";

...

export default (state, action) => {
switch (action.type) {
...

case ADD_ITEM:
console.log("add item");
return {
...state,
rows: {
...state.rows,
[action.rowId]: [
...(state.rows[action.rowId] ?? []),
{ id: uuidV4() }
]
}
};

case REMOVE_ITEM:
return {
...state,
rows: {
...state.rows,
[action.rowId]: state.rows[action.rowId]?.filter(
(el) => el.id !== action.elementId
)
}
};

default:
return state;
}
};

appState - Swap the items state for a rows object state. Update the add/remove item action creators to consume the appropriate row and element ids. Pass the rows state to the context provider.

const AppState = (props) => {
const initialState = {
Icons: false,
rows: {}
};

const [state, dispatch] = useReducer(AppReducer, initialState);

...

const addItem = (rowId) => {
dispatch({
type: ADD_ITEM,
rowId,
});
};

const removeItem = (rowId, elementId) => {
dispatch({
type: REMOVE_ITEM,
rowId,
elementId,
});
};

return (
<appContext.Provider
value={{
Icons: state.Icons,
Items: state.Items,
rows: state.rows,
addItem,
removeItem
}}
>
{props.children}
</appContext.Provider>
);
};

App - Give each Row component an id prop to uniquely identify it.

<AppState>
<Row id={1} />
<Row id={2} />
<Row id={3} />
</AppState>

Row - Pass the rows id prop to the addItem handler. Map the rows state to the Item component.

function Row(props) {
// getting the context needed
const context = useContext(appContext);
return (
<li className="day-row check faderin">
<div className="income-outcome">
<div className="vl-changable"></div>
<ul className="global income dayIncome">
{context.rows[props.id]?.map((el) => (
<Item key={el.id} rowId={props.id} id={el.id} />
))}
<FontAwesomeIcon
className="changable margin-left"
onClick={() => context.addItem(props.id)}
icon={["fas", "plus-circle"]}
size="lg"
/>
</ul>
</div>
</li>
);
}

Removing a single element

Removing a single element from state was covered above, but you need to pass the correct row and element ids to the removeItem handler.

function ChangeablelItem(props) {
const { rowId, id } = props;
const context = useContext(appContext);
...
return (
<li className="day-item">
... editable div ...
<div className="icons">
{iconsVisble && (
<RemoveIcon
removeItem={() => context.removeItem(rowId, id)}
/>
)}
</div>
</li>
);
}

Hiding an icon after click outside of the item

This was actually the easiest issue to resolve. Use onFocus and onBlur handlers to set the iconVisible state to show the delete button. I used a setTimeout on the blur handler to keep it the delete button mounted long enough when clicked to call the removeItem handler.

// Icon State
const [iconsVisble, setIconVisble] = useState(false);

...

<div
className="number amount"
onFocus={() => setIconVisble(true)}
onBlur={() => setTimeout(() => setIconVisble(false), 200)}
contentEditable="true"
/>

Edit react-context-api-context-api-issues-when-adding-and-removing-an-items-from-ui

Removing object from array using hooks (useState)

First of all, the span element with the click event needs to have a name property otherwise, there will be no name to find within the e.target. With that said, e.target.name is reserved for form elements (input, select, etc). So to actually tap into the name property you'll have to use e.target.getAttribute("name")

Additionally, because you have an array of objects, it would not be effective to use list.indexOf(e.target.name) since that is looking for a string when you are iterating over objects. That's like saying find "dog" within [{}, {}, {}]

Lastly, array.slice() returns a new array starting with the item at the index you passed to it. So if you clicked the last-item, you would only be getting back the last item.

Try something like this instead using .filter(): codesandbox

import React, { useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

const App = () => {
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
];

const [list, updateList] = useState(defaultList);

const handleRemoveItem = (e) => {
const name = e.target.getAttribute("name")
updateList(list.filter(item => item.name !== name));
};

return (
<div>
{list.map(item => {
return (
<>
<span name={item.name} onClick={handleRemoveItem}>
x
</span>
<span>{item.name}</span>
</>
);
})}
</div>
);
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

How to unmount, unrender or remove a component, from itself in a React/Redux/Typescript notification message

Just like that nice warning you got, you are trying to do something that is an Anti-Pattern in React. This is a no-no. React is intended to have an unmount happen from a parent to child relationship. Now if you want a child to unmount itself, you can simulate this with a state change in the parent that is triggered by the child. let me show you in code.

class Child extends React.Component {
constructor(){}
dismiss() {
this.props.unmountMe();
}
render(){
// code
}
}

class Parent ...
constructor(){
super(props)
this.state = {renderChild: true};
this.handleChildUnmount = this.handleChildUnmount.bind(this);
}
handleChildUnmount(){
this.setState({renderChild: false});
}
render(){
// code
{this.state.renderChild ? <Child unmountMe={this.handleChildUnmount} /> : null}
}

}

this is a very simple example. but you can see a rough way to pass through to the parent an action

That being said you should probably be going through the store (dispatch action) to allow your store to contain the correct data when it goes to render

I've done error/status messages for two separate applications, both went through the store. It's the preferred method... If you'd like I can post some code as to how to do that.

EDIT: Here is how I set up a notification system using React/Redux/Typescript

Few things to note first. this is in typescript so you would need to remove the type declarations :)

I am using the npm packages lodash for operations, and classnames (cx alias) for inline classname assignment.

The beauty of this setup is I use a unique identifier for each notification when the action creates it. (e.g. notify_id). This unique ID is a Symbol(). This way if you want to remove any notification at any point in time you can because you know which one to remove. This notification system will let you stack as many as you want and they will go away when the animation is completed. I am hooking into the animation event and when it finishes I trigger some code to remove the notification. I also set up a fallback timeout to remove the notification just in case the animation callback doesn't fire.

notification-actions.ts

import { USER_SYSTEM_NOTIFICATION } from '../constants/action-types';

interface IDispatchType {
type: string;
payload?: any;
remove?: Symbol;
}

export const notifySuccess = (message: any, duration?: number) => {
return (dispatch: Function) => {
dispatch({ type: USER_SYSTEM_NOTIFICATION, payload: { isSuccess: true, message, notify_id: Symbol(), duration } } as IDispatchType);
};
};

export const notifyFailure = (message: any, duration?: number) => {
return (dispatch: Function) => {
dispatch({ type: USER_SYSTEM_NOTIFICATION, payload: { isSuccess: false, message, notify_id: Symbol(), duration } } as IDispatchType);
};
};

export const clearNotification = (notifyId: Symbol) => {
return (dispatch: Function) => {
dispatch({ type: USER_SYSTEM_NOTIFICATION, remove: notifyId } as IDispatchType);
};
};

notification-reducer.ts

const defaultState = {
userNotifications: []
};

export default (state: ISystemNotificationReducer = defaultState, action: IDispatchType) => {
switch (action.type) {
case USER_SYSTEM_NOTIFICATION:
const list: ISystemNotification[] = _.clone(state.userNotifications) || [];
if (_.has(action, 'remove')) {
const key = parseInt(_.findKey(list, (n: ISystemNotification) => n.notify_id === action.remove));
if (key) {
// mutate list and remove the specified item
list.splice(key, 1);
}
} else {
list.push(action.payload);
}
return _.assign({}, state, { userNotifications: list });
}
return state;
};

app.tsx

in the base render for your application you would render the notifications

render() {
const { systemNotifications } = this.props;
return (
<div>
<AppHeader />
<div className="user-notify-wrap">
{ _.get(systemNotifications, 'userNotifications') && Boolean(_.get(systemNotifications, 'userNotifications.length'))
? _.reverse(_.map(_.get(systemNotifications, 'userNotifications', []), (n, i) => <UserNotification key={i} data={n} clearNotification={this.props.actions.clearNotification} />))
: null
}
</div>
<div className="content">
{this.props.children}
</div>
</div>
);
}

user-notification.tsx

user notification class

/*
Simple notification class.

Usage:
<SomeComponent notifySuccess={this.props.notifySuccess} notifyFailure={this.props.notifyFailure} />
these two functions are actions and should be props when the component is connect()ed

call it with either a string or components. optional param of how long to display it (defaults to 5 seconds)
this.props.notifySuccess('it Works!!!', 2);
this.props.notifySuccess(<SomeComponentHere />, 15);
this.props.notifyFailure(<div>You dun goofed</div>);

*/

interface IUserNotifyProps {
data: any;
clearNotification(notifyID: symbol): any;
}

export default class UserNotify extends React.Component<IUserNotifyProps, {}> {
public notifyRef = null;
private timeout = null;

componentDidMount() {
const duration: number = _.get(this.props, 'data.duration', '');

this.notifyRef.style.animationDuration = duration ? `${duration}s` : '5s';


// fallback incase the animation event doesn't fire
const timeoutDuration = (duration * 1000) + 500;
this.timeout = setTimeout(() => {
this.notifyRef.classList.add('hidden');
this.props.clearNotification(_.get(this.props, 'data.notify_id') as symbol);
}, timeoutDuration);

TransitionEvents.addEndEventListener(
this.notifyRef,
this.onAmimationComplete
);
}
componentWillUnmount() {
clearTimeout(this.timeout);

TransitionEvents.removeEndEventListener(
this.notifyRef,
this.onAmimationComplete
);
}
onAmimationComplete = (e) => {
if (_.get(e, 'animationName') === 'fadeInAndOut') {
this.props.clearNotification(_.get(this.props, 'data.notify_id') as symbol);
}
}
handleCloseClick = (e) => {
e.preventDefault();
this.props.clearNotification(_.get(this.props, 'data.notify_id') as symbol);
}
assignNotifyRef = target => this.notifyRef = target;
render() {
const {data, clearNotification} = this.props;
return (
<div ref={this.assignNotifyRef} className={cx('user-notification fade-in-out', {success: data.isSuccess, failure: !data.isSuccess})}>
{!_.isString(data.message) ? data.message : <h3>{data.message}</h3>}
<div className="close-message" onClick={this.handleCloseClick}>+</div>
</div>
);
}
}


Related Topics



Leave a reply



Submit