React useState changes my constant initial state - reactjs

My selectItem event changes my initialState which I intentionally implemented to stay as a constant.
Panel.js
import React, { useState } from "react";
import "./Panel.css";
import Item from "./Item";
const initialState = [
{ text: 1, isSelected: false },
{ text: 2, isSelected: false },
{ text: 3, isSelected: false },
{ text: 4, isSelected: false },
{ text: 5, isSelected: false }
];
export default function Panel() {
const [items, setItems] = useState(initialState);
const selectItem = k => {
console.log(k);
const updatedItems = [...initialState];
updatedItems.find(item => item.text === k).isSelected = true;
setItems(updatedItems);
};
return (
<div className="Panel">
{items.map(item => (
<Item
key={item.text}
text={item.text}
isSelected={item.isSelected}
clicked={selectItem}
/>
))}
</div>
);
}
Item.js
import React from "react";
import "./Item.css";
export default function Item({ text, isSelected, clicked }) {
return (
<div className="Item" onClick={() => clicked(text)}>
{isSelected ? <div style={{ backgroundColor: "red" }}>{text}</div> : text}
</div>
);
}

Look at it this way, your array is an array of objects, and each index (item) is a reference to some specific object.
Basically when you are creating the updatedItems array, you are spreading all of its references into a new "array that is referencing objects." So, technically, you are using the very same objects. Once you select one and mutate it, you are mutating the object which both initialState and items are referencing.
What you are doing is a shallow clone (copy) of the array.
If you wish to do a deep clone, a simple method would be to do: JSON.parse(JSON.stringify(initialState)).
You can also use utilities like lodash/cloneDeep https://lodash.com/docs/4.17.15#cloneDeep
Keep in mind though, that deep cloning can also be very expensive and not always the desired result.

Change the line that reads ...
const updatedItems = [...initialState];
to ...
const updatedItems = [...items];
The initialState const should only be used to initialize the items state value.
Sandbox ...

Related

Return an object from an array with a Select component

I'd like to return the entire object representation of an item in a list, however, when a selection is made and handleChangeSelectAuto is called the original value from useState is returned, and from there forward the previous selected value is always returned.
How can I select an item from a list and return all its associated data?
import React, { useEffect, useState } from 'react';
import { FormControl, InputLabel, Select, MenuItem } from '#mui/material';
interface AutoSelectorProps {}
const AutoSelector: React.FC<AutoSelectorProps> = () => {
const [auto, setAuto] = useState({} as object);
const [autoName, setAutoName] = useState('' as string);
const [autoList, setAutoList] = useState([
{
id: 1,
name: 'Car',
color: 'red'
},
{
id: 2,
name: 'Truck',
color: 'blue'
},
]);
const handleChangeSelectAuto = async (value: string) => {
const index = autoList.findIndex((item) => {
return item.name === value;
});
setAutoName(value);
setAuto(autoList[index]);
console.log(auto);
// 1st log: {}
// 2nd log: object from previous selection
// 3rd log: object from previous selection, etc.
};
return (
<div>
<FormControl>
<InputLabel>Select Auto</InputLabel>
<Select
value={autoName}
label="Auto"
onChange={(e) => handleChangeSelectAuto(e.target.value as string)}
>
{autoList.map((item) => {
return (
<MenuItem key={item.name} value={item.name}>
{item.name}
</MenuItem>
);
})}
</Select>
</FormControl>
</div>
);
};
export default AutoSelector;
P.S. If I add a button and handler to log auto it will return the correct value, but I'm not seeing a race condition.
useState is asynchronous. It will not show the values immediately. use useEffect to see the updated values
useEffect(() => {
console.log(auto);
}, [auto])

React Button Multi-Select, strange style behaviour

I am trying to create a simple button multi-select in React but I'm currently getting unexpected behaviour. I'd like users to be able to toggle multiple buttons and have them colourise accordingly, however the buttons seem to act a bit randomly.
I have the following class
export default function App() {
const [value, setValue] = useState([]);
const [listButtons, setListButtons] = useState([]);
const BUTTONS = [
{ id: 123, title: 'button1' },
{ id: 456, title: 'button2' },
{ id: 789, title: 'button3' },
];
const handleButton = (button) => {
if (value.includes(button)) {
setValue(value.filter((el) => el !== button));
} else {
let tmp = value;
tmp.push(button);
setValue(tmp);
}
console.log(value);
};
const buttonList = () => {
setListButtons(
BUTTONS.map((bt) => (
<button
key={bt.id}
onClick={() => handleButton(bt.id)}
className={value.includes(bt.id) ? 'buttonPressed' : 'button'}
>
{bt.title}
</button>
))
);
};
useEffect(() => {
buttonList();
}, [value]);
return (
<div>
<h1>Hello StackBlitz!</h1>
<div>{listButtons}</div>
</div>
);
}
If you select all 3 buttons then select 1 more button the css will change.
I am trying to use these as buttons as toggle switches.
I have an example running #
Stackblitz
Any help is much appreciated.
Thanks
I think that what you want to achieve is way simpler:
You just need to store the current ID of the selected button.
Never store an array of JSX elements inside a state. It is not how react works. Decouple, only store the info. React component is always a consequence of a pattern / data, never a source.
You only need to store the necessary information, aka the button id.
Information that doesn't belong to the state of the component should be moved outside. In this case, BUTTONS shouldn't be inside your <App>.
Working code:
import React, { useState } from 'react';
import './style.css';
const BUTTONS = [
{ id: 123, title: 'button1', selected: false },
{ id: 456, title: 'button2', selected: false },
{ id: 789, title: 'button3', selected: false },
];
export default function App() {
const [buttons, setButtons] = useState(BUTTONS);
const handleButton = (buttonId) => {
const newButtons = buttons.map((btn) => {
if (btn.id !== buttonId) return btn;
btn.selected = !btn.selected;
return btn;
});
setButtons(newButtons);
};
return (
<div>
<h1>Hello StackBlitz!</h1>
<div>
{buttons.map((bt) => (
<button
key={bt.id}
onClick={() => handleButton(bt.id)}
className={bt.selected ? 'buttonPressed' : 'button'}
>
{bt.title}
</button>
))}
</div>
</div>
);
}
I hope it helps.
Edit: the BUTTONS array was modified to add a selected property. Now several buttons can be selected at the same time.

How to write a matrix update correctly with 'usestate'?

I want there will be a matrix where a column with a 'name' and a second column would be the 'Delete' button. I defined 'usestate' as an array and tried to insert values in it but it writes the following error: "
Error: Objects are not valid as a React child (found: object with
keys {name, btn})
. If you meant to render a collection of children, use an array
instead."
const [form,setform] = useState([{name:'',btn:''}]);
setform({...form,name:val, btn: (<button id={form.length} on onClick={()=>delet(form.length)}>delete</button>)});
thanks
You can do it like this:
import React, { useState } from "react";
const App = () => {
const [form, setForm] = useState([
{ name: "Item1", btn: "Button1" },
{ name: "Item2", btn: "Button2" },
{ name: "Item3", btn: "Button3" },
{ name: "Item4", btn: "Button4" },
]);
const deleteItemHandler = (name) => {
setForm(form.filter((item) => item.name !== name));
};
return (
<div>
{form.map((item) => (
<div key={item.name}>
<h3>{item.name}</h3>
<button onClick={() => deleteItemHandler(item.name)}>
Delete me
</button>
</div>
))}
</div>
);
};
export default App;
And in this case you can remove btn from item object

Why is the component fully re-rendering when updating a single state through context?

I have created a page which has two columns:
In one column the idea is to display a list of items
On the other column, I should show some info related to the selected item
The code I have so far is:
import { INavLink, INavLinkGroup, INavStyles, Nav } from "#fluentui/react";
import React, { createContext, useContext, useState } from "react";
interface HistoryTtem {
id: string;
}
interface AppState {
selectedItem: string | undefined;
updateSelectedItem: (value: string | undefined) => void;
items: Array<HistoryTtem>;
}
const AppContext = createContext<AppState>({
selectedItem: undefined,
updateSelectedItem: (value: string | undefined) => {},
items: []
});
const App = () => {
const Column1 = () => {
const rootState: AppState = useContext(AppContext);
const getNavLinks: Array<INavLink> = rootState.items.map((item) => ({
name: item.id,
key: item.id,
url: ""
}));
const groups: Array<INavLinkGroup> = [
{
links: getNavLinks
}
];
const navStyles: Partial<INavStyles> = {
root: {
boxSizing: "border-box",
border: `1px solid #eee`,
overflowY: "auto"
}
};
const onItemClick = (
e?: React.MouseEvent<HTMLElement>,
item?: INavLink
) => {
if (item && item.key) {
rootState.updateSelectedItem(item.key);
}
};
return (
<Nav
onLinkClick={onItemClick}
selectedKey={rootState.selectedItem}
ariaLabel="List of previously searched transactions"
styles={navStyles}
groups={groups}
/>
);
};
const Column2 = () => {
return <div>aaa</div>;
};
const [historyItems, setHistoryItems] = useState<Array<HistoryTtem>>([
{
id: "349458457"
},
{
id: "438487484"
},
{
id: "348348845"
},
{
id: "093834845"
}
]);
const [selectedItem, setSelectedItem] = useState<string>();
const updateSelectedItem = (value: string | undefined) => {
setSelectedItem(value);
};
const state: AppState = {
selectedItem: selectedItem,
updateSelectedItem: updateSelectedItem,
items: historyItems
};
return (
<AppContext.Provider value={state}>
<div>
<Column1 />
<Column2 />
</div>
</AppContext.Provider>
);
};
export default App;
As you can see, I have a root state which will serve to drive the update of the second column triggered from inside the first one. But it is not working. When I click on an item, the whole component in the first column is re-rendering, while it should only change the selected item.
Please find here the CodeSandbox.
You shouldn't nest component functions.
The identity of Column1 changes for every render of App since it's an inner function, and that makes React think it needs to reconcile everything.
Move Column1 and Column2 up to the module level.
What makes react rerender is two things:
Change in State
Change in Props
You have an App Component which is the root of your components and it has a selectedItem state which is changing when an item is clicked so you have a new state and the new state will cause rerender

React Hooks useCallback & memo list re-rendering

I'm new to Hooks and trying to create a PureComponent equivalent version with Hooks.
My goal is to create a multiselectable list with a child component that is reponsible for rendering the list items:
const Movie: FunctionComponent<{
title: string,
score: number,
id: number,
isSelected: boolean,
setSelected: React.Dispatch<React.SetStateAction<{}>>
}> = React.memo(({ title, score, isSelected, setSelected, id }) => {
const selectMovie = (): void => {
if (isSelected === null)
return;
setSelected(id);
};
const selected = {
backgroundColor: "blue"
}
console.log("render movie")
return (
<div onClick={selectMovie} style={isSelected ? selected : {}}>
{title}, {score}
</div>
)
})
The parent component have the data as well as the logic for the selection:
const App: FunctionComponent = () => {
const data = [
{
id: 1,
title: "Pulp fiction",
score: 9
},
{
id: 2,
title: "Heat",
score: 8
},
{
id: 3,
title: "American pie",
score: 7
}
]
const [selectedItems, setSelected] = React.useState<{}>({});
const selectMovie = React.useCallback((id: any) => {
const sel: any = {...selectedItems};
if (sel.hasOwnProperty(id)) {
delete sel[id]
} else {
sel[id] = true;
}
setSelected(sel);
}, [selectedItems])
return (
<div>
{
data.map(e => <Movie key={e.id} {...e} setSelected={selectMovie} isSelected={selectedItems.hasOwnProperty(e.id)}/>)
}
</div>
)
}
I made a sandbox where you can try it out: https://codesandbox.io/s/old-sun-38n4u
The selection state is maintained in the parent component in an object and supplied to the child as a boolean. The problem is when I click on a movie all 3 list items re-renders (you can see the log in the console). I have used React.memo and useCallback as well to avoid arrow function re-creation for the props comparison. I'm pretty new to hooks, so it must be something silly I'm overlooking...
That is because your selectMovie is changing every time due to selectedItems dependency changing.
setSelected function can also take a function and you can get the selectedItems value so you don't need to set it as a dependency
Here is the working sandbox

Resources