React not rendering new state - reactjs

I am trying to learn the new React hooks. I have written a simple todolist that users can type in input to create a new todo, and click on the todo to remove it. However, it is not re-rendering after an todo item is removed.
Here is my code
import React, { useState, Fragment } from "react";
const Todo = () => {
const [inputVal, setInput] = useState("");
const [list, setList] = useState([]);
const handleInput = e => {
setInput(e.target.value);
};
const handleClick = () => {
setList([...list, inputVal]);
setInput("");
};
const handleDelete = index => {
setList([...list.splice(index, 1)]);
console.log(list);
};
const renderList = () =>
list.map((item, index) => {
return (
<div key={index} onClick={() => handleDelete(index)}>
{item}
</div>
);
});
return (
<Fragment>
<div>
<input value={inputVal} onChange={handleInput} />
<button onClick={handleClick}>submit</button>
</div>
<ul>{renderList()}</ul>
</Fragment>
);
};
export default Todo;

You have two issues here.
array.splice is mutating and returns the removed items.
When you run
setList([...list.splice(index, 1)]);
This removes one item from the object list and then calls setList([removed_item]).
You could replace this line with
setList(list.slice(0, index).concat(list.slice(index + 1))
What is currently happening is that you are setting the state as the same object as before (but mutated), so it doesn't trigger a re-render.
You are not using key attributes correctly. You can read the docs here
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.
Your keys should uniquely identify the elements of the todo list, without reference to the list itself. The text in the item is a good choice, except for the possible problem of non-uniqueness.
When you re-render the todo list, React uses the keys to decide which children to re-render. If you use indexes as keys, and then remove the item at index 0, then the 0th child will not be told to update to the new 0th element because it still gets the same key.

Related

Why when you delete an element out of react array, the inner element you pass it to remains

In a react component I have an array of things. I iterate through that array display the name of the thing in a plain div, then pass each element to another component to display details.
What's happening: if I delete an element from anywhere except the bottom (last array element) the header that is displayed in the main element containing the array is correct (the one I clicked "delete" on disappeared), but the "body" (which is another component) remains. Instead, the inner component is acting as if I deleted the last element of the array and kind of "moves" up the array.
It's hard to describe in words. See example below. Delete the top element or one of the middle ones and see how the header for the section starts not matching the contents.
I'm trying to understand why this is happening.
(EDIT/NOTE: State IS needed in the child component because in real life it's a form and updates the object being passed in. I Just removed the updating here to make the example shorter and simpler)
Example code (delete the middle element of the array and see what happens):
https://codesandbox.io/s/confident-buck-dodvgu?file=/src/App.tsx
Main component:
import { useState, useEffect } from "react";
import InnerComponent from "./InnerComponent";
import Thing from "./Thing";
import "./styles.css";
export default function App() {
const [things, setThings] = useState<Thing[]>([]);
useEffect(() => resetThings(), []);
const resetThings = () => {
setThings([
{ name: "dog", num: 5 },
{ name: "cat", num: 7 },
{ name: "apple", num: 11 },
{ name: "book", num: 1}
]);
};
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
return (
<div className="App">
{things.map((thing, index) => (
<div key={`${index}`} className="thing-container">
<h2>{thing.name}</h2>
<InnerComponent
thing={thing}
index={index}
onDelete={onDeleteThing}
/>
</div>
))}
<div>
<button onClick={resetThings}>Reset Things</button>
</div>
</div>
);
}
Inner component:
import { useEffect, useState } from "react";
import Thing from "./Thing";
interface InnerComponentParams {
thing: Thing;
index: number;
onDelete: (indexToDelete: number) => void;
}
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
const [name, setName] = useState(thing.name);
const [num, setNum] = useState(thing.num);
return (
<div>
<div>Name: {name}</div>
<div>Num: {num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
export default InnerComponent;
You are creating unnecessary states in the child component, which is causing problems when React reconciles the rearranged Things. Because you aren't setting the state in the child component, leave it off entirely - instead, just reference the prop.
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
return (
<div>
<div>Name: {thing.name}</div>
<div>Num: {thing.num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
The other reason this is happening is because your key is wrong here:
{things.map((thing, index) => (
<div key={`${index}`}
Here, you're telling React that when an element of index i is rendered, on future renders, when another element with the same i key is returned, that corresponds to the JSX element from the prior render - which is incorrect, because the indicies do not stay the same. Use a proper key instead, something unique to each object being iterated over - such as the name.
<div key={thing.name}
Using either of these approaches will fix the issue (but it'd be good to use both anyway).
This is also wrong. You're removing everything except the index.
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
Use filter:
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things].filter(
(thing, index) => index !== indexToDelete
);
setThings(newThings);
};

Components not re render on state change

I am trying to update a state of a list. And need to rerender the list as per the ids state. But array takes reference value and everytime I update the ids with setIds, I am unable to get the updated list as the component is not rerendered. Anyway to achieve this. I have tried adding const [idsLength, setIdsLength] = useState(0) and passing and updating it in the list. But I feel there is a better approach for this.
const Component1 = () => {
const [ids, setIds] = useState([]);
const handleIdClick = () => {
const temp = ids
temp.push(ids.length)
setIds(temp)
console.log(ids)
}
return(
<div>
<div onClick={handleIdClick}>
Id Adder
</div>
{ids.map(id=>{
return(
<div key={id}>
{id}
</div>
)
})}
</div>
)
}
you just need to use useEffect hook
useEffect(() => {
console.log("ids :" + ids);
}, [ids]);
You can create and pass in a new array with updated contents to setIds
const handleIdClick = useCallback(() => {
setIds([...ids, ids.length]);
}, [ids])

React - UseEffect not re-rendering with new data?

This is my React Hook:
function Student(props){
const [open, setOpen] = useState(false);
const [tags, setTags] = useState([]);
useEffect(()=>{
let input = document.getElementById(tagBar);
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById(tagButton).click();
}
});
},[tags])
const handleClick = () => {
setOpen(!open);
};
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
tagList.push(input.value);
console.log("tag");
console.log(tags);
console.log("taglist");
console.log(tagList);
setTags(tagList);
}
const tagDisplay = tags.map(t => {
return <p>{t}</p>;
})
return(
<div className="tags">
<div>
{tagDisplay}
</div>
<input type='text' id={tagBar} className="tagBar" placeholder="Add a Tag"/>
<button type="submit" id={tagButton} className="hiddenButton" onClick={addTag}></button>
<div>
);
What I am looking to do is be able to add a tag to these student elements (i have multiple but each are independent of each other) and for the added tag to show up in the tag section of my display. I also need this action to be triggerable by hitting enter on the input field.
For reasons I am not sure of, I have to put the enter binding inside useEffect (probably because the input element has not yet been rendered).
Right now when I hit enter with text in the input field, it properly updates the tags/tagList variable, seen through the console.logs however, even though I set tags to be the re-rendering condition in useEffect (and the fact that it is also 1 of my states), my page is not updating with the added tags
You are correct, the element doesn't exist on first render, which is why useEffect can be handy. As to why its not re-rendering, you are passing in tags as a dependency to check for re-render. The problem is, tags is an array, which means it compares the memory reference not the contents.
var myRay = [];
var anotherRay = myRay;
var isSame = myRay === anotherRay; // TRUE
myRay.push('new value');
var isStillSame = myRay === anotherRay; // TRUE
// setTags(sameTagListWithNewElementPushed)
// React says, no change detected, same memory reference, skip
Since your add tag method is pushing new elements into the same array reference, useEffect thinks its the same array and is not re-triggers. On top of that, React will only re-render when its props change, state changes, or a forced re-render is requested. In your case, you aren't changing state. Try this:
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
// Create a new array reference with the same contents
// plus the new input value added at the end
setTags([...tagList, input.value]);
}
If you don't want to use useEffect I believe you can also use useRef to get access to a node when its created. Or you can put the callback directly on the node itself with onKeyDown or onKeyPress
I can find few mistake in your code. First, you attaching event listeners by yourself which is not preferred in react. From the other side if you really need to add listener to DOM inside useEffect you should also clean after you, without that, another's listeners will be added when component re-rendered.
useEffect( () => {
const handleOnKeyDown = ( e ) => { /* code */ }
const element = document.getElementById("example")
element.addEventListener( "keydown", handleOnKeyDown )
return () => element.removeEventListener( "keydown", handleOnKeyDown ) // cleaning after effect
}, [tags])
Better way of handling events with React is by use Synthetic events and components props.
const handleOnKeyDown = event => {
/* code */
}
return (
<input onKeyDown={ handleOnKeyDown } />
)
Second thing is that each React component should have unique key. Without it, React may have trouble rendering the child list correctly and rendering all of them, which can have a bad performance impact with large lists or list items with many children. Be default this key isn't set when you use map so you should take care about this by yourself.
tags.map( (tag, index) => {
return <p key={index}>{tag}</p>;
})
Third, when you trying to add tag you again querying DOM without using react syntax. Also you updating your current state basing on previous version which can causing problems because setState is asynchronous function and sometimes can not update state immediately.
const addTag = newTag => {
setState( prevState => [ ...prevState, ...newTage ] ) // when you want to update state with previous version you should pass callback which always get correct version of state as parameter
}
I hope this review can help you with understanding React.
function Student(props) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");
const handleOnKeyDown = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
addTag();
}
};
function addTag() {
setTags((prev) => [...prev, inputValue]);
setInputValue("");
}
return (
<div className="tags">
<div>
{tags.map((tag, index) => (
<p key={index}>{tag}</p>
))}
</div>
<input
type="text"
onKeyDown={handleOnKeyDown}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a Tag"
/>
<button type="submit" onClick={addTag}>
ADD
</button>
</div>
);
}

useLoopCallback -- useCallback hook for components created inside a loop

I'd like to start a discussion on the recommended approach for creating callbacks that take in a parameter from a component created inside a loop.
For example, if I'm populating a list of items that will have a "Delete" button, I want the "onDeleteItem" callback to know the index of the item to delete. So something like this:
const onDeleteItem = useCallback(index => () => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<div>
<span>{item}</span>
<button type="button" onClick={onDeleteItem(index)}>Delete</button>
</div>
)}
</div>
);
But the problem with this is that onDeleteItem will always return a new function to the onClick handler, causing the button to be re-rendered, even when the list hasn't changed. So it defeats the purpose of useCallback.
I came up with my own hook, which I called useLoopCallback, that solves the problem by memoizing the main callback along with a Map of loop params to their own callback:
import React, {useCallback, useMemo} from "react";
export function useLoopCallback(code, dependencies) {
const callback = useCallback(code, dependencies);
const loopCallbacks = useMemo(() => ({map: new Map(), callback}), [callback]);
return useCallback(loopParam => {
let loopCallback = loopCallbacks.map.get(loopParam);
if (!loopCallback) {
loopCallback = (...otherParams) => loopCallbacks.callback(loopParam, ...otherParams);
loopCallbacks.map.set(loopParam, loopCallback);
}
return loopCallback;
}, [callback]);
}
So now the above handler looks like this:
const onDeleteItem = useLoopCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
This works fine but now I'm wondering if this extra logic is really making things faster or just adding unnecessary overhead. Can anyone please provide some insight?
EDIT:
An alternative to the above is to wrap the list items inside their own component. So something like this:
function ListItem({key, item, onDeleteItem}) {
const onDelete = useCallback(() => {
onDeleteItem(key);
}, [onDeleteItem, key]);
return (
<div>
<span>{item}</span>
<button type="button" onClick={onDelete}>Delete</button>
</div>
);
}
export default function List(...) {
...
const onDeleteItem = useCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
)}
</div>
);
}
Performance optimizations always come with a cost. Sometimes this cost is lower than the operation to be optimized, sometimes is higher. useCallback it's a hook very similar to useMemo, actually you can think of it as a specialization of useMemo that can only be used in functions. For example, the bellow statements are equivalents
const callback = value => value * 2
const memoizedCb = useCallback(callback, [])
const memoizedWithUseMemo = useMemo(() => callback, [])
So for now on every assertion about useCallback can be applied to useMemo.
The gist of memoization is to keep copies of old values to return in the event we get the same dependencies, this can be great when you have something that is expensive to compute. Take a look at the following code
const Component = ({ items }) =>{
const array = items.map(x => x*2)
}
Uppon every render the const array will be created as a result of a map performed in items. So you can feel tempted to do the following
const Component = ({ items }) =>{
const array = useMemo(() => items.map(x => x*2), [items])
}
Now items.map(x => x*2) will only be executed when items change, but is it worth? The short answer is no. The performance gained by doing this is trivial and sometimes will be more expensive to use memoization than just execute the function each render. Both hooks(useCallback and useMemo) are useful in two distinct use cases:
Referencial equality
When you need to ensure that a reference type will not trigger a re render just for failing a shallow comparison
Computationally expensive operations(only useMemo)
Something like this
const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}
Now you have a reason to memoized the operation and lazily retrieve the serializedValue everytime props.item changes:
const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])
Any other use case is almost always worth to just re compute all values again, React it's pretty efficient and aditional renders almost never cause performance issues. Keep in mind that sometimes your efforts to optimize your code can go the other way and generate a lot of extra/unecessary code, that won't generate so much benefits (sometimes will only cause more problems).
The List component manages it's own state (list) the delete functions depends on this list being available in it's closure. So when the list changes the delete function must change.
With redux this would not be a problem because deleting items would be accomplished by dispatching an action and will be changed by a reducer that is always the same function.
React happens to have a useReducer hook that you can use:
import React, { useMemo, useReducer, memo } from 'react';
const Item = props => {
//calling remove will dispatch {type:'REMOVE', payload:{id}}
//no arguments are needed
const { remove } = props;
console.log('component render', props);
return (
<div>
<div>{JSON.stringify(props)}</div>
<div>
<button onClick={remove}>REMOVE</button>
</div>
</div>
);
};
//wrap in React.memo so when props don't change
// the ItemContainer will not re render (pure component)
const ItemContainer = memo(props => {
console.log('in the item container');
//dispatch passed by parent use it to dispatch an action
const { dispatch, id } = props;
const remove = () =>
dispatch({
type: 'REMOVE',
payload: { id },
});
return <Item {...props} remove={remove} />;
});
const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }];
//Reducer is static it doesn't need list to be in it's
// scope through closure
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
//remove the id from the list
return state.filter(
item => item.id !== action.payload.id
);
}
return state;
};
export default () => {
//initialize state and reducer
const [list, dispatch] = useReducer(
reducer,
initialState
);
console.log('parent render', list);
return (
<div>
{list.map(({ id }) => (
<ItemContainer
key={id}
id={id}
dispatch={dispatch}
/>
))}
</div>
);
};

Unnecessary deleting elements from dom without making an action on this elements

import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const CreateCriteria = () => {
const [criteria, setCriteria] = useState({})
const onClose = useCallback((key) => {
delete criteria[key]
setCriteria(criteria)
}, [criteria])
const onClick = useCallback(() =>{
const key = Math.random()
setCriteria({
...criteria,
[key]: <div onClick={() => onClose(key)}>{
key
}</div>
})
}, [criteria, onClose])
return (
<div>
{
Object.keys(criteria).map((entity) => criteria[entity])
}
<button onClick={onClick}>
add
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<CreateCriteria />, rootElement);
Imagine that I have this snippet of code. When I click on add button I want to get new elements in the DOM and in the criteria object. I do it by generating new par ( key/value ) in criteria object and then parse this new data in DOM. I do it because I want also to have the opportunity to delete this object from dom by clicking on this object. When I click on elements from last to first ( from below to above), then it works fine, but for example, if I have 3 elements 1,2,3 and I click on 2, then it also delete 3, and I can't find out why does this happen. You can use CodeSandbox for checking it :
https://codesandbox.io
You did two errors
You assidetially created closure when adding new keys in creteria object. This code
[key]: <div onClick={() => onClose(key)}>{
key
}</div>
not only adds <div> element to creteria object but create closure with function onClose. onClose in its turn create includes in closure creteria object as it is in time of adding new key in creteria object. So when onClose called, it has only keys that was there when new key added. So you see strange behaviour.
To solve problem I suggest to store only keys in creteria object and encapsulate them in <div> during render.
Another problem pointed in comments. You should create new creteria object in onClose. Otherwise React will not update creterai as it only compares new and old state using Object.is
To solve this, use destruct operator to create new copy of creteria object like this
const onClose = useCallback((key) => {
let newCreteria = { ...criteria };
delete newCreteria[key];
setCriteria(newCreteria);
}, [criteria])
Also useCallback is useless. But code works with them in palce.
Working sample is here
const CreateCriteria = () => {
const [criteria, setCriteria] = useState({});
const deleteCriteria = useCallback(
key => {
const newCreteria = { ...criteria };
delete newCreteria[key];
setCriteria(newCreteria);
},
[criteria]
);
const onClick = useCallback(() => {
const key = Math.random();
setCriteria({
...criteria,
[key]: null
});
}, [criteria]);
const CriteriaList = useMemo(() => (
Object.keys(criteria).map(entity => (
<div key={entity} onClick={() => deleteCriteria(entity)}>
{entity}
</div>
))
), [criteria, deleteCriteria])
return (
<div>
{CriteriaList}
<button
onClick={onClick}
>
add
</button>
</div>
);
};

Categories

Resources