Why the following react functional component does not work properly? - javascript

It is quite straight forward, when you press "add" it should add(and it adds) and when you press "remove" it should pop the last element and re-render the list but it doesn't. I am make mistake somewhere?
import React, { useState, useEffect } from 'react';
const Test = () => {
const [list, setList] = useState([]);
const add = () => {
setList([list.length, ...list]);
}
const remove = () => {
list.pop();
setList(list);
}
useEffect(() => {
console.log(list)
}, [list])
return (<ul>
<button onClick={add}>add</button>
<button onClick={remove}>remove</button>
{list.map(el => <li>{el}</li>)}
</ul>)
}
export default Test;
UPDATE:
Actually it updates the state by removing the last element but the re-render happen only when button "add" is pressed

It's not recommended to modify the state itself because it is immutable.
So instead using .pop() on the original state of the array, first I suggest to clone that one and remove the required element from there, then the result should passed to setList() function.
Try as the following instead:
const remove = () => {
const copy = [...list];
copy.pop();
setList(copy);
}
Think about the following:
const list = [1,3,5,6,7];
const copy = [...list];
copy.pop();
console.log(list);
console.log(copy);
I hope this helps!

You need to set a new array in this case, setList(list) will not cause React to re-render because it's still the same array you're using.
Try setList([...list]) in your remove function.
There's also an alternative to pop, and doesn't mutate the original variable:
const remove = () => {
const [removed, ...newList] = list
setList(newList)
}

Related

force remount on value change

I have some files that builds a cart in a dropdown for my shop website.
One file adds the selected item to an array which will be my cart. The other file is the CartDropdown component itself. My cart only show the items when I close and open it (remounting), but I want it to remount every time I add a new item.
Adding item function:
const ProductContainer = ({ productInfo }) => {
const { cartProducts, setCartProducts } = useContext(CartContext);
const cartArray = cartProducts;
const addProduct = () => {
productInfo.quantity = 1;
if (cartArray.includes(productInfo)) {
const index = cartArray.findIndex((object) => {
return object === productInfo;
});
cartProducts[index].quantity++;
setCartProducts(cartArray);
} else {
cartArray.push(productInfo);
setCartProducts(cartArray);
}
// setCartProducts(cartArray)
console.log(cartProducts);
// console.log(cartArray)
};
};
dropdown component
const CartDropdown = () => {
const { setCartProducts, cartProducts } = useContext(CartContext);
const { setProducts, currentProducts } = useContext(ProductsContext);
// useEffect(() => {}, [cartProducts])
const cleanCart = () => {
const cleanProducts = currentProducts;
console.log(cleanProducts);
for (let i in cleanProducts) {
if (cleanProducts[i].hasOwnProperty("quantity")) {
cleanProducts[i].quantity = 0;
}
}
setProducts(cleanProducts);
setCartProducts([]);
};
return (
<div className="cart-dropdown-container">
<div className="cart-items">
{cartProducts.map((product) => (
<div key={product.id}>
<img src={product.imageUrl}></img>
</div>
))}
</div>
<button onClick={cleanCart}>CLEAN CART</button>
<Button children={"FINALIZE PURCHASE"} />
</div>
);
};
How can I force the dropdown to remount every time cartProducts changes?
CART CONTEXT:
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => { },
cartProducts: [],
setCartProducts: () => { }
})
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false)
const [cartProducts, setCartProducts] = useState([])
const value = { isCartOpen, setIsCartOpen, cartProducts, setCartProducts };
return (
<CartContext.Provider value={value}>{children}</CartContext.Provider>
)
}
product context
export const ProductsContext = createContext({
currentProducts: null,
setProducts: () => {}
})
export const ProductsProvider = ({children}) => {
const [currentProducts, setProducts] = useState(shop_data)
const value = {currentProducts, setProducts}
return(
<ProductsContext.Provider value={value}>{children}</ProductsContext.Provider>
)
}
You can change the key prop of the component every time you want to remount. Every time cartProduct changes, update the value of key. You can do that using a useEffect with cartProduct as a dependency.
<CartDropdown key={1} />
to
<CartDropdown key={2} />
Edit for more clarification:
const [keyCount, setKeyCount] = useState(0);
useEffect(() => {
setKeyCount(keyCount+1);
}, [cartProducts]);
<CartDropdown {...otherProps} key={keyCount} />
The first issue I see is that you are not using the callback to set the state inside the context but you are doing cartProducts[index].quantity++ and react docs specify
Do Not Modify State Directly
Also after cartProducts[index].quantity++, you call setCartProducts(cartArray); not with cartProducts which you actually updated (this is also the reason why "if I do usestate(console.log('A'), [cartProducts]) its not triggering everytime i add my cart product". But anyway there is an issue even if you would use cartArray for both:
You shouldn't directly do const cartArray = cartProducts since by doing so cartArray will be a reference to cartProducts (not a copy of it) which also shouldn't be modified (because it would mean that you are modifying state directly).
So first 2 things I recommend you to improve would be:
Initialize cartArray as a cartProducts deep copy (if your cartProducts is an array of objects, spread syntax won't do it). So I would reccomand you to check this question answers for creating a deep copy.
After you make sure that cartArray is a deep copy of cartProducts, doublecheck you use cartArray to create a local newValue then set the state of the context with the same value (so basically:
cartArray[index].quantity++;
setCartProducts(cartArray);
)
The deep copy part also apply for const cleanProducts = currentProducts; (you should also create a deep copy here for cleanProducts, instead of saving the object ref).
If you are not using deep copies, your code might still work in some cases, but you might encounter weird behaviors in some other instances (and thoose are really hard to debug). Therefore is a bad practice in general not using deep copies.

useEffect wont run when state gets changed

Why doesn't useEffect run when sorting gets changed? I only want it to run when sorting.options get adjusted, but for some reason, when I add options to sorting.options the useEffect won't run happen; however, when I only have "sorting" it works perfectly even though only options get changed in handleChange? I dont want to have only sorting in there because I don't want the useEffect to run when sorting.value gets changed, for example.
const handleChange=index=>event=>{
let newOptions = sorting.options
newOptions[index]["value"]=event.target.value
setSorting((prevState)=>({
...prevState,
options:newOptions
}))
console.log("sorting changed")
}
useEffect(()=>{
console.log("Change happend")
},[sorting.options])//This works perfectly fine when only sorting is there, but when i put in sorting.options nothing happens
As Quentin said, try
let newOptions = sorting.options.slice(0)
To copy the options array instead of mutating it
The problem is that you keep the reference of the original object throughout different render. Just replace by a copy
import React, { useState, useEffect } from "react";
export default function Home() {
const [sorting, setSorting] = useState({
options: { 1: { value: 2 } },
email: "vapa#dieheli.us",
});
const handleChange = (index) => (event) => {
let newOptions = sorting.options; // Here you are getting the reference of sorting.opions which is constant between renders
// let newOptions = { ...sorting.options }; // Just replace previous line for a copy.
newOptions[index]["value"] = event.target.value; // Then you mutated the same object
setSorting((prevState) => ({
// setSorting creates a new object reference
...prevState,
options: newOptions, // but options references never changed
}));
};
useEffect(() => {
console.log("Change happend", sorting);
}, [sorting.options]);
return (
<div>
Home <input onChange={handleChange(1)} />
</div>
);
}

How to update/add new data to react functional component object?

I am trying to create a function that updates an object in react functional component.
What i was trying to do is:
const [content, setContent] = useState({});
const applyContent = (num: number, key: string, val: string) => {
if (content[num] === undefined) {
content[num] = {};
}
content[num][key] = val;
setNewContent(newInput);
};
But I keep getting an error stating that content doesnt have a num attribute,
In vanilla JS it would work, what am i missing to make it work with react functional component?
The setter for your component state has been incorrectly spelled. Have a look at the code below.
import React, { useState } from 'react';
import './style.css';
export default function App() {
const [content, setContent] = useState({});
const applyContent = (num, key, val) => {
//gets the appropriate inputs
let updatedContent = content;
let value = {};
value[key] = val;
updatedContent[num] = value; //this inserts a new object if not present ot updates the existing one.
setContent({ ...updatedContent });
};
return (
<div>
<h1>Click buttons to change content</h1>
<p>{JSON.stringify(content)}</p>
<button onClick={(e) => applyContent(0, 'a', 'b')}>Add</button>
<button onClick={(e) => applyContent(1, 'c', 'd')}>Add</button>
<button onClick={(e) => applyContent(0, 'e', 'f')}>Add</button>
</div>
);
}
content is a read only value. You must not directly mutate this. Use it only to show data or to copy this data to another helper value.
setContent is a function that sets content.
There are two ways to set data
setContent(value) <-- set directly
setContent(prevState => {
return {
...prevState,
...value
}
})
In second example, you will use the previous value, copy it, and then override it with new value. This is useful if you are only updating a part of an object or array.
If you you are working with a deeply nested object, shallow copy might not be enough and you might need to deepcopy your content value first. If not, then use the prevState example to only update the part of that content
const [content, setContent] = useState({});
const applyContent = (num:number,key:string,val:string) => {
const newContent = {...content} // Shallow copy content
if (content[num] === undefined) {
//content[num] = {}; <-- you cant directly change content. content is a readOnly value
newContent[num] = {}
}
newContent[num][key] = val;
//setNewContent(newInput);
setContent(newContent) // <-- use "setContent" to change "content" value
}

React useEffect and useState interaction

I am using React and Material UI to create a table (XGrid) with some buttons. When you click the row, it should set the row id using useState. When you click the delete button, it should delete the row. It seems that the delete click handler is not using the value from use state. This is either some kind of closure thing or some kind of React thing.
const MyTableThing: React.FC = (props) =>
{
const { data } = props;
const [filename, setFilename] = React.useState<string>("")
const [columns, setColumns] = React.useState<GridColDef[]>([])
const handleDelete = () =>
{
someFunctionThatDeletes(filename); // filename is always ""
setFilename(""); // Does not do anything.. !
}
React.useEffect(() =>
{
if (data)
{
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) =>
{
if (column.field === "delete")
{
column.renderCell = (cellParams: GridCellParams) =>
{
return <Button onClick={handleDelete}>Delete</Button>
}
}
})
setColumns(columns)
}
}, [data?.files])
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) =>
{
console.log(`set selected row to ${param.data.id}`) // This works every time
setFilename(param.data.id)
}
}
The reason for this behavior is that React does not process setState action synchronously. It is stacked up with other state changes and then executed. React does this to improve performance of the application. Read following link for more details on this.
https://linguinecode.com/post/why-react-setstate-usestate-does-not-update-immediately
you can disable your deleteRow button till the filename variable is updated. you can use useEffect or setState with callback function.
useEffect(() => {
//Enable your delete row button, fired when filename is updated
}, filename)
OR
this.setFilename(newFilename, () => {
// ... enable delete button
});
Let me know if this helps! Please mark it as answer if it helps.
The main problem I see here is that you are rendering JSX in a useEffect hook, and then saving the output JSX into columns state. I assume you are then returning that state JSX from this functional component. That is a very bizarre way of doing things, and I would not recommend that.
However, this explains the problem. The JSX being saved in state has a stale version of the handleDelete function, so that when handleDelete is called, it does not have the current value of filename.
Instead of using the useEffect hook and columns state, simply do that work in your return statement. Or assign the work to a variable and then render the variable. Or better yet, use a useMemo hook.
Notice that we add handleDelete to the useMemo dependencies. That way, it will re-render every time handleDelete changes. Which currently changes every render. So lets fix that by adding useCallback to handleDelete.
const MyTableThing: React.FC = (props) => {
const { data } = props;
const [filename, setFilename] = React.useState<string>('');
const handleDelete = React.useCallback(() => {
someFunctionThatDeletes(filename); // filename is always ""
setFilename(''); // Does not do anything.. !
}, [filename]);
const columns = React.useMemo(() => {
if (!data) {
return null;
}
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) => {
if (column.field === 'delete') {
column.renderCell = (cellParams: GridCellParams) => {
return <Button onClick={handleDelete}>Delete</Button>;
};
}
});
return columns;
}, [data?.files, handleDelete]);
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) => {
console.log(`set selected row to ${param.data.id}`); // This works every time
setFilename(param.data.id);
};
return columns;
};

React filter by category feature

Hello I am building photo gallery where I would like to add feature that user will be able filter by Category. I tried some solutions but there are two bugs that I am not able to fix. First is that if I go to the GalleryPage (using Swtich) it does NOT render dynamically added buttons from FilterButton component. I have to click one more time on the link and then it DOES render the buttons. I dont know why it does not work on the first render.
Other issue is that I am able to filter by category but it causes the infinite loop in the useEffect and I dont know how to fix it.
I have got GalleryPage component where I am getting data from API and parsing the data for using later in other components. Here it seems that is all working fine.
const GalleryPage = () => {
const url = 'someurl';
const [data, setData] = useState([]);
const [categoryList, setCategoryList] = useState([]);
const [category, setCategory] = useState('All');
useEffect(() => {
const fetchData = async () => {
const result = await axios(url,);
setData(result.data)
result.data.forEach(item => {
imageUrl.push(item.image)
if (categoryList.indexOf(item.group) === -1) {
categoryList.push(item.group)
}
})
}
fetchData();
}, [])
return (
<FilterButton setCategory={setCategory} categoryList={categoryList}/>
<Gallery data={data} category={category}/>
)
}
If I go to the GalleryPage the h3 and 'All' button is rendered. But I have to click on the link one more time to render the buttons inside the map function:
const FilterButton = ({setCategory, categoryList}) => {
return(
<h3>Gallery</h3>
<button onClick={()=> setCategory('All')}>All</button>
{categoryList.map(item => (
<button key={item} onClick={()=> setCategory(item)}>{item}</button>
))}
)
};
export default FilterButton;
And here I am not able to fix the infinite loop:
const Gallery = ({data, category}) => {
const [photos, setPhotos] = useState([]);
useEffect(()=>{
let temp = []
if (category === 'All'){
setPhotos(data)
}else{
data.map(item => {
temp.push(item)
})
}
setPhotos(temp)
})
return(
photos.map((item =>
<img key={item.id} src={item.image}/>
))
)
};
export default Gallery;
If I add empty array to the useEffect it does not work at all. Also I am using styled components and framer motion but it should not have affect on this I hope.
First, I see that you're never setting your state for categoryList.
After modifying categoryList, you should call setCategoryList() with the new category list. This way, the state variable will be 'remembered' when the component is re-rendered.
You can read about the useState hook here.
Additionally, for the useEffect hook, the 'empty array' you pass in at the end is actually an array of variables to 'watch' for changes. If you pass an empty array, the useEffect will only run once, at the first page load. However, you can pass in something like [category] so that the useEffect is only called when the category variable is modified, which I persume is what you want to do.

Categories

Resources