Why is updating state not causing a re-render (updating state using drag and drop)? - reactjs

I am making a card system that can swap cards using drag and drop. When I'm dragging the cards over each other my state is updating as it's supposed to but it's not re-rendering.
import React, {useRef, useState} from 'react';
import DnDGroup from './DndGroup';
const DragNDrop = ({data}) => {
const [list, setList] = useState(data);
const dragItem = useRef();
const dragNode = useRef();
const getParams = (element) => {
const itemNum = element.toString();
const group = list.find(group => group.items.find(item => item === itemNum))
const groupIndex = list.indexOf(group);
const itemIndex = group.items.indexOf(itemNum);
return {'groupIndex': groupIndex, 'itemIndex': itemIndex}
}
const handleDragstart = (e) => {
dragNode.current = e.target;
setTimeout(() => {
dragNode.current.classList.add('current')
}, 0)
dragItem.current = getParams(e.target.value);
dragNode.current.addEventListener('dragend', () => {
if(dragNode.current !== undefined) {
dragNode.current.classList.remove('current')
}
})
}
const handleDragEnter = e => {
if(dragNode.current !== e.target && e.target.value !== undefined) {
const node = getParams(e.target.value);
const currentItem = dragItem.current;
[data[currentItem.groupIndex].items[currentItem.itemIndex], data[node.groupIndex].items[node.itemIndex]] = [data[node.groupIndex].items[node.itemIndex], data[currentItem.groupIndex].items[currentItem.itemIndex]];
setList(data);
console.log(list)
}
}
return (
<div className='drag-n-drop'>
{list.map(group => (
<DnDGroup
key={group.title}
group={group}
handleDragstart={handleDragstart}
handleDragEnter={handleDragEnter}
/>
))}
</div>
);
};
export default DragNDrop;
I also tried to do this:
setList([...data])
Using this it renders according to the state changes and works great inside one group, but when I want to drag a card to the other group, the state constantly changes back and forth like crazy, it also gives tons of console.logs.
How can I fix this?

With this line:
[data[currentItem.groupIndex].items[currentItem.itemIndex], data[node.groupIndex].items[node.itemIndex]] = [data[node.groupIndex].items[node.itemIndex], data[currentItem.groupIndex].items[currentItem.itemIndex]];
you're mutating a deeply nested object inside the data array. Never mutate state in React; it can result in components not re-rendering when you expect them to, in addition to other unexpected (and undesirable) side-effects.
Another thing that looks like it's probably an unintentional bug is that you're changing the data (original state) instead of the stateful list array.
Change it to:
const itemA = list[node.groupIndex].items[node.itemIndex];
const itemB = list[currentItem.groupIndex].items[currentItem.itemIndex];
const newList = [...list];
newList[currentItem.groupIndex] = {
...newList[currentItem.groupIndex],
items: newList[currentItem.groupIndex].map(
(item, i) => i !== currentItem.itemIndex ? item : itemB
)
};
newList[node.groupIndex] = {
...newList[node.groupIndex],
items: newList[node.groupIndex].map(
(item, i) => i !== node.itemIndex ? item : itemA
)
};
setList(newList);
This avoids the mutation of the nested items subarray.

Related

Error message "Cannot read properties of null (reading 'filter')"

I'm new to learning react and have been having problems getting the array to filter using the .filter() method. I'm trying to create a grocery list and I keep getting the error message "Cannot read properties of null (reading 'filter')" Can someone please assist me on getting this work? Here is the code that I have.
import Header from './Header';
import SearchItem from './SearchItem';
import AddItem from './AddItem';
import Content from './Content';
import Footer from './Footer';
import { useState, useEffect } from 'react';
function App() {
const [items, setItems] = useState(JSON.parse(localStorage.getItem('shoppinglist')));
const [newItem, setNewItem] = useState('')
const [search, setSearch] = useState('')
console.log('before useEffect')
//useEffect looks to it's dependency and if the dependency changes then it will run the anonymous function
useEffect(() => {
console.log('inside useEffect')
},[items])
const setAndSaveItems = (newItems) => {
setItems(newItems);
localStorage.setItem('shoppinglist', JSON.stringify(newItems));
}
console.log('after useEffect')
const addItem = (item) => {
const id = items.length ? items[items.length - 1].id + 1 : 1;
const myNewItem = { id, checked: false, item };
const listItems = [...items, myNewItem];
setAndSaveItems(listItems);
}
const handleCheck = (id) => {
const listItems = items.map((item) => item.id === id ? { ...item, checked: !item.checked } : item);
setAndSaveItems(listItems);
}
const handleDelete = (id) => {
const listItems = items.filter((item) => item.id !== id);
setAndSaveItems(listItems);
}
const handleSubmit = (e) => {
e.preventDefault();
if (!newItem) return;
addItem(newItem);
setNewItem('');
}
return (
<div className="App">
<Header title="Grocery List" />
<AddItem
newItem={newItem}
setNewItem={setNewItem}
handleSubmit={handleSubmit}
/>
<SearchItem
search={search}
setSearch={setSearch}
/>
<Content
items={items.filter(item => ((item.item).toLowerCase()).includes(search.toLowerCase()))}
handleCheck={handleCheck}
handleDelete={handleDelete}
/>
<Footer length={items.length} />
</div>
);
}
export default App;
I feel that you're mentioning about this code excerpt:
items.filter((item) => item.id !== id);
can you please check if the items array is null or not. Only if items is null, filtering wouldn't be applicable and you will receive such error messages
can you log items before deletion?
Few pointers that could help
initilize the items in an useEffect as it could be null, it will make it easy to fetch data a api later
const [items, setItems] = useState([]);
useEffect(() => {
try {
const items = JSON.parse(localStorage.getItem('shoppinglist'))
setItems(items)
} catch(error) {
}
}, [])
// put ?. checks on items when calling filter, map
const handleDelete = (id) => {
const listItems = items?.filter((item) => item.id !== id);
if (listItems) {
setAndSaveItems(listItems);
}
}
Id generated will clash and cause bugs
const id = items.length ? items[items.length - 1].id + 1 : 1;
if the person deletes on item and adds another the new item will have the same id as the last one
item { id: 1}
item { id: 2}
item { id: 3}
after deleting id 2, when you add new items it will have id 3
and will cause bugs with select
either use a id that is a timestamp or check for unique ids
Save the items in local storage on submit, as calls get/set items to localstorage can lead to performace issues in the UI
Checkout the new docs on working with arrays
Hope it helps

Data from useEffect not being rendered

I have a function component:
// MovieOverview.tsx
const MovieOverview = () => {
const {data} = useQuery(resolvers.queries.ReturnAllMovies);
const movie: IMovie = useReactiveVar(moviesVar);
let movies: IMovie[] = data?.movies;
let movieRows;
const setMoviesInRow = (movies: IMovie[]) => {
const numberOfMovies = Math.floor((window.innerWidth -24) / 185);
return chunk(movies, numberOfMovies);
};
useEffect(() => {
movie ? movies = movies?.concat(movie) : null;
movies ? movieRows = setMoviesInRow(movies) : null;
movies ? console.log(movieRows) : null;
}, [movies, movie]);
return (
<div>
{movieRows?.length}
</div>
);
};
The console.log(movieRows) shows what I expect, an array with some objects. But in the template movieRows is undefined.
Setting a variable (movieRows = setMoviesInRow(movies)) doesn't cause a render, which is why you don't see it. In order to cause a render using React Hooks you must use a "set" function that is returned from the hook (like what is returned from useState). This function updates the variable and triggers a render.
For your purpose, useQuery doesn't return a "set" function (since you shouldn't need to change the return values of a query). What you should do is use window.innerWidth as a state variable that will update with its own useEffect - something like this example. You might be able to use an existing package like this so you don't have to write the hook yourself.
Then you can use it in your component without another useEffect:
// MovieOverview.tsx
const MovieOverview = () => {
const windowInnerWidth: Number = useWindowInnerWidth();
const {data} = useQuery(resolvers.queries.ReturnAllMovies);
const movie: IMovie = useReactiveVar(moviesVar);
let movies: IMovie[] = data?.movies;
let movieRows;
movie ? movies = movies?.concat(movie) : null;
const numberOfMovies = Math.floor((windowInnerWidth -24) / 185);
movies ? movieRows = chunk(movies, numberOfMovies) : null;
return (
<div>
{movieRows?.length}
</div>
);
};
The component will be re-rendered once when the window is ready and again when the query results arrive.

How to update react context between components?

I have two sibling components (A & B) that I'm trying to pass react context between. Not static context. The context is created and updated in component A like so:
export const ModelContext = React.createContext({ model: '' });
function Models() {
const [check, setCheck] = useState({});
const [alert, setAlert] = useState(false);
let model = useContext(ModelContext);
const handleChange = (event) => {
const arr = Object.keys(check);
if (arr.length === 1 && arr.includes(event.target.name) === false) {
setAlert(true);
} else if (arr.length === 1 && arr.includes(event.target.name) === true) {
setAlert(false);
setCheck({});
model = check;
console.log(model);
} else {
setCheck({ ...check, [event.target.name]: event.target.checked });
setAlert(false);
}
};
I update the context (ModelContext) in this component in the handleChange. I can see from the console.log(model) that the context is updating as I desire.
However, in component B the context (ModelContext) is not being updated. I'm not sure where to go from here. Here is component B:
import { ModelContext } from './index';
function Fields() {
const modelSelected = useContext(ModelContext);
}
Is it possible to update the context in this fashion or should I take another approach?
What you are seeing in the console log is after updating the model object. It is no longer referring to the context. It has a new reference now after model=check.
First of all, if you want to pass the context between 2 siblings, the common parent should be wrapped around with context providers. You can create a context provider component.
const ModelContext = useContext({model: ''});
const ContextProvider = ({children, modelData}) => {
const [model, setModel] = useState(modelData);
return <ModelContext.Provider value={{model, setModel}}>{children}</ModelContext.Provider>
}
const Parent = () => {
return (
<ContextProvider modelData={your model data}>
<Models />
<Fields/>
</ContextProvider>
)
}
const Models = () => {
const [check, setCheck] = useState({});
const [alert, setAlert] = useState(false);
const {model, setModel} = useContext(ModelContext);
const handleChange = (event) => {
const arr = Object.keys(check);
if (arr.length === 1 && arr.includes(event.target.name) === false) {
setAlert(true);
}
else if (arr.length === 1 && arr.includes(event.target.name)===true) {
setAlert(false);
setCheck({});
setModel(check);
console.log(model);
} else {
setCheck({ ...check, [event.target.name]: event.target.checked });
setAlert(false);
}
};
const Fields = () => {
const {model: modelSelected} = useContext(ModelContext);
console.log(modelSelected);
}
You have to do this because, the context doesn't automatically get to the siblings if you do not wrap their parent with the context provider. This
blog might be helpful for you to understand the usage of context

Modifying object inside array with useContext

I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.

React why the state is not updating when calling a function to initialize it?

Playing with React those days. I know that calling setState in async. But setting an initial value like that :
const [data, setData] = useState(mapData(props.data))
should'nt it be updated directly ?
Bellow a codesandbox to illustrate my current issue and here the code :
import React, { useState } from "react";
const data = [{ id: "LION", label: "Lion" }, { id: "MOUSE", label: "Mouse" }];
const mapData = updatedData => {
const mappedData = {};
updatedData.forEach(element => (mappedData[element.id] = element));
return mappedData;
};
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
export default function App() {
const [loadedData, setLoadedData] = useState(data);
const [filter, setFilter] = useState("");
const filterData = () => {
return loadedData.filter(element =>
filter ? element.id === filter : true
);
};
//loaded comes from a useEffect http call but for easier understanding I removed it
return (
<div className="App">
<button onClick={() => setFilter("LION")}>change filter state</button>
<ChildComponent dataProp={filterData()} />
</div>
);
}
So in my understanding, when I click on the button I call setFilter so App should rerender and so ChildComponent with the new filtered data.
I could see it is re-rendering and mapData(updatedData) returns the correct filtered data BUT ChildComponent keeps the old state data.
Why is that ? Also for some reason it's rerendering two times ?
I know that I could make use of useEffect(() => setMappedData(mapData(dataProp)), [dataProp]) but I would like to understand what's happening here.
EDIT: I simplified a lot the code, but mappedData in ChildComponent must be in the state because it is updated at some point by users actions in my real use case
https://codesandbox.io/s/beautiful-mestorf-kpe8c?file=/src/App.js
The useState hook gets its argument on the very first initialization. So when the function is called again, the hook yields always the original set.
By the way, you do not need a state there:
const ChildComponent = ({ dataProp }) => {
//const [mappedData, setMappedData] = useState(mapData(dataProp));
const mappedData = mapData(dataProp);
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
EDIT: this is a modified version in order to keep the useState you said to need. I don't like this code so much, though! :(
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
let actualMappedData = mappedData;
useMemo(() => {
actualMappedData =mapData(dataProp);
},
[dataProp]
)
console.log("** Render Child Component **");
return Object.values(actualMappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Your child component is storing the mappedData in state but it never get changed.
you could just use a regular variable instead of using state here:
const ChildComponent = ({ dataProp }) => {
const mappedData = mapData(dataProp);
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};

Resources