The ChooseElements Component renders list elements
depending of the state of each list item the item gets displayed differently
The toggleItem function sets for each element a new state if a clicked event occurs
why does using
const selectedItems = new Set(activeItems)
renders the changes immediately
but using const selectedItems = activeItems
the changes are getting displayed after a second re-render ?
export default function ChooseElements() {
const [activeItems, setActiveItems] = useState(new Set())
const toggleItem = (id) => {
const selectedItems = new Set(activeItems)
if (selectedItems.has(id)) {
selectedItems.delete(id)
} else {
selectedItems.add(id)
console.log(selectedItems.add(id))
}
setActiveItems(selectedItems)
}
const listItems = elementObjects.map((object) => (
<ListItem
key={object.id.toString()}
value={object.Element}
isActive={activeItems.has(object.id)}
clickItem={() => toggleItem(object.id)}
/>
))
return (
<div>
<button onClick={() => console.log(activeItems)}>gets sets</button>
<ul>{listItems}</ul>
<button
onClick={() => {
/* with button click get all list items with specific class! */
console.log(elementObjects.filter((o) => activeItems.has(o.id)))
}}
>
get Elements
</button>
</div>
)
}
child component
import React, { useState, useEffect } from 'react'
import './styles/choose-elements.css'
const elementObjects = [
{ id: 1, Element: 'element-1' },
{ id: 2, Element: 'element-2' },
{ id: 3, Element: 'element-3' },
{ id: 4, Element: 'element-4' },
]
function ListItem({ isActive, clickItem, value }) {
return (
<li className={isActive ? 'selected' : null} onClick={clickItem}>
{value}
</li>
)
}
It's pretty simple. Every time you do setActiveItems(), react does check the value of previous state and current state.
When you use const selectedItems = new Set(activeItems) it creates selectedItems in new address space. And updating with this state therefore triggers a re render. (and in the other case, the address space remains the same)
I'd illustrate further by this:
const arr1 = ["foo"];
const arr2 = arr1;
arr2.push("bar");
console.log(arr1 === arr2); // new elements are pushed but address remains the same
I'd recommend you to read more about shallow and deep copy in JS and how react decides when to re render its component.
Oh, you are creating activeItems using new Set() so when you perform activeItems.add you are mutating your existing state. And react doesn't detect that you have updated it event if you perform setActiveItems(selectedItems). Because selectedItems reference the same object as activeItems React under the hood just compare objects like references
if (obj1 !== obj2) {//run re-render cycle}
So you should create new Set when you are updating your state
Related
I would like to write an App that allows multiple components to add and remove entries to a set by accessing the global context.
So far I am initialising a Set with useState and feeding the state and the changeState values down to the child components using a context provider, I am then adding and removing items from the Set based on the props passed down into that component.
The code:
The main App file
/App.js
export const myListContext = createContext(new Set());
function App() {
const [myList, changeMyList] = useState(() => new Set());
const Alphabet = {
A: 'a',
B: 'b',
};
useEffect(() => {
console.log(myList);
}, [myList]);
return (
<myListContext.Provider value={{ myList, changeMyList }}>
<div className="my-box">
<Checkbox myEnum={Alphabet.A} title={"Add A to Set"}></Checkbox>
<Checkbox myEnum={Alphabet.B} title={"Add B to Set"}></Checkbox>
</div>
</myListContext.Provider>
);
}
/Checkbox.js
const Checkbox = ({ myEnum, title }) => {
const { myList, changeMyList } = useContext(myListContext);
const [index, setIndex] = useState(0);
function changeIndex() {
setIndex(index + 1);
if (index >= 3) {
setIndex(1);
}
}
function addListItem(item) {
changeMyList((prev) => new Set(prev).add(item));
}
function removeMyListItem(item) {
changeMyList((prev) => {
const next = new Set(prev);
next.delete(item);
return next;
});
}
function filterItem(item) {
changeIndex();
if (index === 0) {
addListItem(item);
}
if (index === 1) {
removeMyListItem(item);
}
}
return (
<div className="my-checkbox">
<div
className="my-checkbox-inner"
onClick={() => {
filterItem(myEnum);
}}
></div>
<div className="the-title">{title}</div>
</div>
);
}
The reason for this set up is to Add or Remove Items based on how many times the user has pressed the div, each component needs to track the status of its own index.
Unfortunately, the effect hook on the top layer of the app reports that myList does not contain the new entries.
Why does changeMyList not have a global impact despite change the state via a context.
What am I doing wrong?
console.log(myList) prints (0) depite clicking the buttons that aught to trigger a new entry based on myEnum.
Thanks!
Set is different than Array and it only contains distinct elements.
The Set object lets you store unique values of any type, whether primitive values or object references, according to MDN
Therefore, your code already worked. If you console log as myList.size will return 2 and [...myList] will return an array ["a", "b"] with an updated values and you can iterate over it to create necessary jsx and etc.
console.log([...myList]); // ["a", "b"]
console.log(myList.size); // 2
I've spent a few days on this and it is driving me crazy now.
I have a state in a parent component containing an Array[string] of selected squares which is passed to the child component (a map) along with the set function from the hook. The issue is that when I set the new squares they are changed in the parent, but on selection of another square it is not taking into account the already selected squares.
function Parent(props){
const [selectedSquares, setSquares] = useState([]);
useEffect(() => {
console.log('parent useEffect', selectedSquares);
}, [selectedSquares]);
return (
<Child selectedSquares={selectedSquares}
handleSquaresChange={setSquares}
/>
)
}
function Child(props){
const {selectedSquares, handleSquaresChange} = props;
useEffect(() => {
console.log('child useEffect', selectedSquares)
}, [selectedSquares]);
const handleSelect = evt => {
if(evt.target){
const features = evt.target.getFeatures().getArray();
let selectedFeature = features.length ? features[0] : null;
if (selectedFeature) {
console.log('select (preadd):', selectedSquares);
const newTile = selectedFeature.get('TILE_NAME');
const newSquares = [...selectedSquares];
newSquares.push(newTile);
const newTest = 'newTest';
handleSquaresChange(newSquares);
console.log('select (postadd):', newSquares);
}
}
return(
<Map>
<Select onSelect={handleSelect}/>
</Map>
)
}
On the first interactionSelect component I get this output from the console:
parent useEffect: [],
child useEffect: [],
select (preadd):[],
child useEffect:['NX'],
parent useEffect: ['NX'],
select (postadd): ['NX'].
Making the second selection this is added to the console:
select (preadd):[],
select (postadd): ['SZ'],
child useEffect:['SZ'],
parent useEffect: ['SZ'].
Turns out there is an addEventListener in the library I am using that is going wrong. Thanks to everyone who responded but turns out the issue was not with React or the state stuff.
Consider something like the code below. Your parent has an array with all your options. For each option, you render a child component. The child component handles the activity of its own state.
function Parent(props){
// array of options (currently an array of strings, but this can be your squares)
const allOptions = ['opt 1', 'opt 2', 'opt 3', 'etc'];
return (
<>
// map over the options and pass option to child component
{allOptions.map((option) => <Child option={option}/>)}
</>
)
}
function Child({ option }){
const [selected, setSelected] = useState(false); // default state is false
return (
<>
// render option value
<p>{option}</p>
// shows the state as selected or not selected
<p>Option is: {selected ? "selected" : "not selected"}</p>
// this button toggles the active state
<button onClick={() => setSelected(!selected)}>Toggle</button>
</>
)
}
I have a page wherein there are Listings.
A user can check items from this list.
Whenever the user checks something it gets added to a globally declared Set(each item's unique ID is added into this set). The ID's in this set need to be accessed by a seperate Component(lets call it PROCESS_COMPONENT) which processes the particular Listings whose ID's are present in the set.
My Listings code roughly looks like:
import React from "react";
import { CheckBox, PROCESS_COMPONENT } from "./Process.jsx";
const ListItem = ({lItem}) => {
return (
<>
//name,image,info,etc.
<CheckBox lId={lItem.id}/>
</>
)
};
function Listings() {
// some declarations blah blah..
return (
<>
<PROCESS_COMPONENT /> // Its a sticky window that shows up on top of the Listings.
//..some divs and headings
dataArray.map(item => { return <ListItem lItem={item} /> }) // Generates the list also containing the checkboxes
</>
)
}
And the Checkbox and the PROCESS_COMPONENT functionality is present in a seperate file(Process.jsx).
It looks roughly like:
import React, { useEffect, useState } from "react";
let ProcessSet = new Set(); // The globally declared set.
const CheckBox = ({lID}) => {
const [isTicked, setTicked] = useState(false);
const onTick = () => setTicked(!isTicked);
useEffect( () => {
if(isTicked) {
ProcessSet.add(lID);
}
else {
ProcessSet.delete(lID);
}
console.log(ProcessSet); // Checking for changes in set.
}, [isTicked]);
return (
<div onClick={onTick}>
//some content
</div>
)
}
const PROCESS_COMPONENT = () => {
const [len, setLen] = useState(ProcessSet.size);
useEffect( () => {
setLen(ProcessSet.size);
}, [ProcessSet]); // This change is never being picked up.
return (
<div>
<h6> {len} items checked </h6>
</div>
)
}
export { CheckBox, PROCESS_COMPONENT };
The Set itself does get the correct ID values from the Checkbox. But the PROCESS_COMPONENT does not seem to be picking up the changes in the Set and len shows 0(initial size of the set).
I am pretty new to react. However any help is appreciated.
Edit:
Based on #jdkramhoft
's answer I made the set into a state variable in Listings function.
const ListItem = ({lItem,set,setPSet}) => {
//...
<CheckBox lID={lItem.id} pset={set} setPSet={setPSet} />
)
}
function Listings() {
const [processSet, setPSet] = useState(new Set());
//....
<PROCESS_COMPONENT set={processSet} />
dataArray.map(item => {
return <ListItem lItem={item} set={processSet} setPSet={setPSet} />
})
}
And corresponding changes in Process.jsx
const CheckBox = ({lID,pset,setPSet}) => {
//...
if (isTicked) {
setPSet(pset.add(lID));
}
else {
setPSet(pset.delete(lID));
}
//...
}
const PROCESS_COMPONENT = ({set}) => {
//...
setLen(set.size);
//...
}
Now whenever I click the check box I get an error:
TypeError: pset.add is not a function. (In 'pset.add(lID)', 'pset.add' is undefined)
Similar error occurs for the delete function as well.
First of all, the set should be a react state const [mySet, setMySet] = useState(new Set()); if you want react to properly re-render with detected changes. If you need the set to be available to multiple components you can pass it to them with props or use a context.
Secondly, React checks if dependencies like [ProcessSet] has been changed with something like ===. Even though the items in the set are different, no change is detected because the object is the same and there is no re-render.
Update:
The setState portion of [state, setState] = useState([]); is not intended to mutate the previous state - only to provide the next state. So to update your set you would do something like:
const [set, setSet] = useState(new Set())
const itemToAdd = ' ', itemToRemove = ' ';
setSet(prev => new Set([...prev, itemToAdd]));
setSet(prev => new Set([...prev].filter(item => item !== itemToRemove)));
As you might notice, this makes adding and removing from a set as slow as a list. So unless you need to make a lot of checks with set.has() I'd recommend using a list:
const [items, setItems] = useState([])
const itemToAdd = ' ', itemToRemove = ' ';
setItems(prev => [...prev, itemToAdd]);
setItems(prev => prev.filter(item => item !== itemToRemove));
My app allows users to click on player cards in a Field section, and then for the selected player cards to appear in a Teams section. I have an array (called selectedPlayers) and initially, each element has a default player name and default player image. As the users select players, the elements in the array are replaced one-by-one by the name and image of the selected players.
The state of the array is set in a parent component and then the array is passed to a TeamsWrapper component as a prop. I then map through the array, returning a TeamsCard component for each element of the array. However, my TeamsCards are always one selection behind reality. In other words, after the first player is selected, the first card still shows the default info; after the second player is selected, the first card now reflects the first selection, but the second card still shows the default info. The code for the TeamsWrapper component is below:
import React from "react";
import "./style.css";
import TeamsCard from "../TeamsCard";
function TeamsWrapper(props) {
const { selectedPlayers } = props;
console.log('first console.log',selectedPlayers)
return (
<div className="teamsWrapper">
{selectedPlayers.map((el, i) => {
console.log('second console.log',selectedPlayers)
console.log('third console.log',el)
return (
<div key={i}>
<TeamsCard
image={el.image}
playerName={el.playerName}
/>
</div>
);
})}
</div>
);
}
export default TeamsWrapper;
I did have this working fine before when the parent was a class-based component. However, I changed it to a function component using hooks for other purposes. So, I thought the issue was related to setting state, but the console logs indicate something else (I think). After the first player is selected:
the first console log shows a correctly updated array (i.e. the first element reflects the data for the selected player, not the placeholder data)
the second console log reflects the same
but the third print still shows the placeholder data for the first element
As mentioned above, as I continue to select players, this third print (and the TeamsCards) is always one selection behind.
EDIT:
Here is the code for the parent component (Picks), but I edited out the content that was not relevant to make it easier to follow.
import React, { useState } from "react";
import { useStateWithCallbackLazy } from "use-state-with-callback";
import TeamsWrapper from "../TeamsWrapper";
import FieldWrapper from "../FieldWrapper";
const Picks = () => {
const initialSelectedPlayers = [
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
];
const [count, setCount] = useStateWithCallbackLazy(0);
const [selectedPlayers, setSelectedPlayers] = useState(
initialSelectedPlayers
);
const handleFieldClick = (props) => {
// check to make sure player has not already been picked
const match = selectedPlayers.some(
(el) => el.playerName === props.playerName
);
if (match) {
return;
} else {
setCount(count + 1, (count) => {
updatePickPhase(props, count);
});
}
};
const updatePickPhase = (props, count) => {
if (count <= 15) {
updateTeams(props, count);
}
// elseif other stuff which doesn't apply to this issue
};
const updateTeams = (props, count) => {
const location = [0, 1, 2, 5, 4, 3];
const position = location[count - 1];
let item = { ...selectedPlayers[position] };
item.playerName = props.playerName;
item.image = props.image;
selectedPlayers[position] = item;
setSelectedPlayers(selectedPlayers);
};
return (
<>
<TeamsWrapper selectedPlayers={selectedPlayers}></TeamsWrapper>
<FieldWrapper
handleFieldClick={handleFieldClick}
count={count}
></FieldWrapper>
</>
);
};
export default Picks;
Thank you for your help!
When you update the array you are mutating state (updating existing variable instead of creating a new one), so React doesn't pick up the change and only re-render when count changes, try
const updateTeams = (props, count) => {
const position = count - 1;
const newPlayers = [...selectedPlayers];
newPlayers[position] = {playerName:props.playerName, image:props.image}
setSelectedPlayers(newPlayers);
};
This way React will see it is a new array and re-render.
When use useState hook to store list of object (ex. [{ a:1 }, { a:2 }]), If I change list's element(object)'s content, react DO NOT update component.
For example in below code,
If I press first button, first h1 component's content will be 24. BUT even though I press first button, first h1 component component DO NOT update.
If I press second button after press first button, component DO update.
const [tempList, setTempList] = useState([
{ a: 1 },
{ a: 2 }
])
return (
<div>
{
tempList.map((item, idx) => {
return <h1>{item.a}</h1>
})
}
<button onClick={() => {
let temp = tempList;
temp[0]['a'] = 24
setTempList(temp)
}}>modify list</button>
<button onClick={() => {setTempList(...tempList, {a: 3})}}>insert list</button>
</div>
)
I already used useReducer hook. But it is not solution.
How Can I update component?
React re-render the component when the state or props change. And it determines that the state has changed by only looking at the memory address of the state.
In the callback of the first button, by declaring the variable temp, you are only creating a shallow copy of the tempList array. Therefore, even after modifying the first object, the id of the array does not change and react does not know that the state have been changed.
And also, by putting a callback in the setState function, you can always have a fresh reference to the current state:
const [state, setState] = useState(0);
setState(state+1) <-- the state can be stale
setState(state=>state+1) <-- increment is guaranteed
Try building an another array:
<button onClick={()=>{
setTempList(tempList=>tempList.map((item,ind)=>{
if(ind===0){
return {a:24};
}
else{
return item;
}
})
}}>modify list</button>
You had a syntax error in the second callback. In addition to the fix, I recommend again to put a callback function to the setTempList function.
<button onClick={() => {
setTempList(tempList=>[...tempList, {a: 3}])
}}>insert list</button>
You seems to be updating same reference of object instead of pushing a new object. Try this -
const [tempList, setTempList] = useState([
{ a: 1 },
{ a: 2 }
])
return (
<div>
{
tempList.map((item, idx) => {
return <h1>{item.a}</h1>
})
}
<button onClick={() => {
let temp = [...tempList];
temp[0] = { a: 24 };
setTempList(temp)
}}>modify list</button>
<button onClick={() => {setTempList(...tempList, {a: 3})}}>insert list</button>
</div>
)