React update state array object single property - javascript

I have the following method:
addWidget = (index) => {
var currentState = this.state;
if(currentState.availableWidgets[index].pos === 'start'){
// add it at the start
for(var i = 0; i < currentState.widgets.length; i++){
this.setState({
widgets: [
...currentState.widgets,
currentState.widgets.x = 5
]
})
}
}
else {
var endX = currentState.widgets.reduce((endX, w) => endX + w.w, 0)
if (endX === 12) endX = 0
this.setState({
widgets: currentState.widgets.concat({
...currentState.availableWidgets[index],
i: uuid(),
x: endX,
y: Infinity,
})
})
}
console.log(currentState.widgets);
}
and the state is:
class DashboardContainer extends React.Component {
state = {
widgets: [],
availableWidgets: [
{
type: 'compliance-stats',
config: {
},
w: 1,
h: 1,
pos: 'start',
},
{
type: 'compliance-stats',
config: {
},
w: 3,
h: 2,
}
]
}
...
I am trying to update the "x" property of each object inside "widgets" by doing so:
for(var i = 0; i < currentState.widgets.length; i++){
this.setState({
widgets: [
...currentState.widgets,
currentState.widgets.x = 5
]
})
}
I am aware of setting the state inside a loop is not good at all. However I am currently getting an error.
I am importing widgets in:
const Dashboard = ({ widgets, onLayoutChange, renderWidget }) => {
const layouts = {
lg: widgets,
}
return (
<div>
<ResponsiveReactGridLayout
layouts={layouts}
onLayoutChange={onLayoutChange}
cols={{ lg: 12 }}
breakpoints={{lg: 1200}}>
{
widgets.map(
(widget) =>
<div key={widget.i}>
{renderWidget(widget)}
</div>
)
}
</ResponsiveReactGridLayout>
</div>
)
}

Probably better to change the widgets and then setState only once:
const changedWidgets = currentState.widgets.map(w => ({ ...w, x: 5 }));
this.setState({ widgets: changedWidgets });

The spread operator inside an array will concatenate a new value onto the array, so by seting widgets state to [ ...currentState.widgets, currentState.widgets.x = 5 ] what you're actually trying to do is currentState.widgets.concate(currentState.widgets.x = 5) which is throwing your error.
If you would like to modify the value inside an array you should map out the array and then modify the objects inside the array like so.
const widgets = currentState.widgets.map(widget => { ...widget, x: 5})
this.setState({ widgets })

Related

Toggle the button name on click in ReactJS

I have an array of objects. So for every object, which has subItems, I'm trying to add a button to it. On click of the particular button, the name of the particular button should toggle between 'Expand' and 'Hide'. I'm displaying them using map.
export default function App() {
const [button,setButton] = useState([
{pin: 'Expand', id: 0},
{pin: 'Expand', id: 1},
{pin: 'Expand', id: 2},
]);
const dataObj = [
{
title: 'Home'
},
{
title: 'Service',
subItems: ['cooking','sleeping']
},
{
title: 'Contact',
subItems: ['phone','mobile']
}
];
const expandFunc = (ind) => {
// toggle logic here
}
return (
<div className="App">
{
dataObj.map((arr,ind) => (
<div>
<span>{arr.title}:</span>
{
// console.log(ind)
arr.subItems &&
<button onClick={() => expandFunc(ind)}>{button[ind].pin}</button>
}
</div>
))
}
</div>
);
}
This is how the output looks -
If I click on service button, then the button name should toggle between 'expand' and 'hide'. Could someone help me with this?
You need to update your state by determining new pin based on current state, try using Array.map:
const expandFunc = (ind) => {
const togglePin = oldPin => oldPin === "Expand" ? "Hide" : "Expand";
const updatedButtons = button.map((btn, index) =>
ind === index ? { ...btn, pin: togglePin(btn.pin) } : btn);
setButton(updatedButtons);
}
You can use something like this, I'll also suggest combining dataObj into button state, and using key while mapping elements in React helps to skip them in the rendering process making your site faster.
export default function App() {
const [button, setButton] = useState([{
expanded: false,
id: 0
},
{
expanded: false,
id: 1
},
{
expanded: false,
id: 2
},
]);
const dataObj = [{
title: 'Home'
},
{
title: 'Service',
subItems: ['cooking', 'sleeping']
},
{
title: 'Contact',
subItems: ['phone', 'mobile']
}
];
const toggleExpandFunc = useCallback((ind) => {
// toggle logic here
setButton([...button.map(btn => btn.id === ind ? { ...btn,
expanded: !btn.expanded
} : btn)]);
}, [button]);
return ( <
div className = "App" > {
dataObj.map((arr, ind) => ( <
div >
<
span > {
arr.title
}: < /span> {
// console.log(ind)
arr.subItems &&
<
button onClick = {
() => toggleExpandFunc(ind)
} > {
button[ind].expanded ? 'Expanded' : 'Hide'
} < /button>
} <
/div>
))
} <
/div>
);
}
You can also need another state like Toggle and expnadFunc can be handled like this;
const expandFunc = (ind) => {
let tmp = [...button];
tmp.map((arr, index) => {
if (index === ind) {
return (arr.pin = arr.pin === 'Toggle' ? 'Expand' : 'Toggle');
}
return arr;
});
setButton(tmp);
};

Error: ReactGridLayout: ReactGridLayout.children[0].x must be a number

I'm trying to make a RGL layout where I can add and remove tiles and save the configuration. I need the file to be a function component and not a class component.
Here's what I've got so far:
import React from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import { v4 as uuid } from "uuid";
const ReactGridLayout = WidthProvider(Responsive);
localStorage.setItem("username");
const username = localStorage.getItem('username');
/**
* This layout demonstrates how to sync multiple responsive layouts to localstorage.
*/
export default class Dash extends React.PureComponent {
constructor(props) {
super(props);
this.originalLayouts = this.getLayout("layouts") || {lg:[]};
this.savedLayout = JSON.parse(JSON.stringify(this.originalLayouts));
this.state = {
layout: this.savedLayout.lg.map(function(i, key) {
return {
i: "",
x: i * 2,
y: 0,
w: 2,
h: 2
};
}
),
};
this.id = uuid()
this.onLayoutChange = this.onLayoutChange.bind(this);
this.addItem = this.addItem.bind(this);
this.removeItem = this.removeItem.bind(this);
this.now = this.now.bind(this);
this.saveLayout = this.saveLayout.bind(this);
this.getLayout = this.getLayout.bind(this);
}
// Function to create element on the Dashboard
createElement(el, i) {
const removeStyle = {
position: "absolute",
right: "2px",
top: 0,
cursor: "pointer"
};
return (
<div key={el.i} data-grid={i}>
<span
className="remove"
style={removeStyle}
onClick={this.removeItem.bind(this, el.i)}
>
x
</span>
</div>
);
}
// Function ammends layout array with additional grid item
addItem = (item) => {
const id = uuid();
console.log('adding: ' + id);
this.setState({
// Add a new item. It must have a unique key!
layout: this.state.layout.concat({
i: `${id}`,
x: (this.state.layout.length * 2) % (this.state.cols || 12),
y: 0, // puts it at the bottom
w: 2,
h: 2
})
},
}
// Function to remove grid item on click
removeItem = (i) => {
console.log('removing: ' + i);
this.setState({layout: _.reject(this.state.layout, { i:i }) });
}
// Function when the layout change
onLayoutChange= (layout, layouts) => {
this.saveLayout("layouts", layouts);
this.setState({layout: layout});
}
// Function to save user layout to LS
saveLayout = ( key, value ) => {
if (global.localStorage) {
global.localStorage.setItem(username, JSON.stringify({ [key]: value}) );
console.log("Saving Layout...", value);
}
};
// Function to get user layout from LS
getLayout = (key) => {
let ls = {};
if (global.localStorage) {
try {
ls = JSON.parse(global.localStorage.getItem(username)) || {};
} catch (error) {
console.log('error getting layout')
}
}
return ls[key]
};
render() {
return (
<div id="maincontent" className="content">
<ReactGridLayout
className="layout"
resizeHandles={['se']}
compactType={"vertical"}
draggableHandle=".dragHandle"
onLayoutChange={this.onLayoutChange}
// onBreakpointChange={this.onBreakpointChange}
{...this.props}
>
{_.map(this.state.layout, (elem, i) => this.createElement(elem, i))}
</ReactGridLayout>
</div>
);
}
}
Can you spot where I made a mistake ? I can't find what is the problem. I can start with a blank dashboard, then add an item and it works.
But if I add a second item ! This error is raised.
Thanks !

ReactJS populating NVD3Chart with data from an API call

I cannot get my data to populate my line chart.
The data from the API call comes back fine but when I try to pass that data into the chart it is of length 0. So I assume there is some issue with the response got being called at the right time? But I have tried so many times to sort this and nothing ever works?
import React from 'react';
import NVD3Chart from 'react-nvd3';
import SamplePage from './SamplePage';
function getData(){
const alpha = require('account')({ key: 'xxxxxxxxxxxx' });
var array = [];
alpha.data.intraday(`msft`).then((data) => {
const polished = alpha.util.polish(data);
{Object.keys(polished.data).map((key) => (
array.push(polished.data[key].open)
))}
});
return array;
}
function getDatum() {
let dataArray = getData();
let newArray = [];
for (let index = 0; index < dataArray.length; index++) {
const element = dataArray[index];
newArray.push({
'x': index,
'y': parseFloat(element)
})
}
return [
{
data: newArray,
key: 'OpenPrice',
color: '#A389D4'
}
];
}
class LineChart extends React.Component {
constructor(props) {
super(props);
this.state = {
DataisLoaded: false,
data:[]
};
}
componentDidMount() {
this.state.data = getDatum();
this.setState(
{
DataisLoaded: true,
data: this.state.data
}
)
}
render() {
const { DataisLoaded } = this.state;
if (!DataisLoaded) return <div>
<h1> Please wait.... </h1> </div>;
return (
<div>
{
React.createElement(NVD3Chart, {
xAxis: {
tickFormat: function(d){ return d; },
axisLabel: 'Time (ms)'
},
yAxis: {
axisLabel: 'Voltage (v)',
tickFormat: function(d) {return parseFloat(d).toFixed(2); }
},
type:'lineChart',
datum: this.state.data,
x: 'x',
y: 'y',
height: 300,
renderEnd: function(){
console.log('renderEnd');
}
})
}
</div>
)
}
}
export default LineChart;

React stale state with useEffect loop

Given the following component:
import { useEffect, useState } from "react"
const Test = () => {
const [thing, setThing] = useState({ x: 0, y: 0 });
useEffect(() => {
gameLoop();
}, []);
const gameLoop = () => {
const p = {...thing};
p.x++;
setThing(p);
setTimeout(gameLoop, 1000);
}
return (
<div>
</div>
)
}
export default Test;
Every second a new line is printed into the console. The expected output should of course be:
{ x: 1, y: 0 }
{ x: 2, y: 0 }
{ x: 3, y: 0 }
...etc
What is instead printed is:
{ x: 0, y: 0 }
{ x: 0, y: 0 }
{ x: 0, y: 0 }
I've come to the conclusion as this is because of a stale state. Some solutions I've found and tried are:
The dependency array
Adding thing to the dependency array causes it to be called ridiculously fast. Adding thing.x to the array also does the same.
Changing the setState call
One post I found mentioned changing the setState to look like so:
setState(thing => {
return {
...thing.
x: thing.x+1,
y: thing.y
});
Which changed nothing.
I have found a lot of posts about clocks/stopwatches and how to fix this issue with primitive value states but not objects.
What is my best course of action to fix the behavior while maintaining that it only runs once a second?
EDIT: The comments seem to be stating I try something like this:
import { useEffect, useState } from "react"
const Test = () => {
const [thing, setThing] = useState({ x: 0, y: 0 });
useEffect(() => {
setInterval(gameLoop, 1000);
}, []);
const gameLoop = () => {
console.log(thing)
const p = {...thing};
p.x++;
setThing(prevP => ({...prevP, x: prevP.x+1}));
}
return (
<div>
</div>
)
}
export default Test;
This still prints out incorrectly:
{ x: 0, y: 0 }
{ x: 0, y: 0 }
{ x: 0, y: 0 }
I think you can use this solution:
useEffect(() => {
setInterval(() => {
setThing((prev) => ({
...prev,
x: prev.x + 1,
}));
}, 1000);
}, []);
I suggest to control setInterval with the clearInterval when component is destroied; like this:
useEffect(() => {
const interval = setInterval(() => {
setThing((prev) => ({
...prev,
x: prev.x + 1,
}));
}, 1000);
return () => {
clearInterval(interval);
};
}, []);

Toggle item in an array react

I want to toggle a property of an object in an array. The array looks as follows. This is being used in a react component and When a user clicks on a button I want to toggle the winner.
const initialFixtures = [{
teams: {
home: 'Liverpool',
away: 'Manchester Utd'
},
winner: 'Liverpool'
},
{
teams: {
home: 'Chelsea',
away: 'Fulham'
},
winner: 'Fulham'
}, ,
{
teams: {
home: 'Arsenal',
away: 'Tottenham'
},
winner: 'Arsenal'
}
];
My react code looks something like this
function Parent = () => {
const [fixtures, setUpdateFixtures] = useState(initialFixtures)
const toggleWinner = (index) => {
const updatedFixtures = fixtures.map((fixture, i) => {
if (i === index) {
return {
...fixture,
winner: fixture.winner === home ? away : home,
};
} else {
return fixture;
}
})
setUpdateFixtures(updatedFixtures);
}
return <Fixtures fixtures={fixtures} toggleWinner={toggleWinner} />;
}
function Fixtures = ({ fixtures, toggleWinner }) => {
fixtures.map((fixture, index) => (
<div>
<p>{fixture.winner} </p>
<button onClick = {() => toggleWinner(index)}> Change Winner</button>
</div>
))
}
the code works but it feels like it is a bit too much. I am sure there is a better more succinct way of doing this. Can anyone advise? I do need to pass the fixtures in from the parent of the Fixture component for architectural reasons.
const updatedFixtures = [...fixtures];
const fixture = updatedFixtures[i];
updatedFixtures[i] = {
...fixture,
winner: fixture.winner === fixture.teams.home ? fixture.teams.away : fixture.teams.home,
};
You can slice the fixtures array into three parts:
from 0 to index: fixtures.slice(0, index). This part is moved to the new array intact.
The single item at index. This part/item is thrown away because of being changed and a new item is substituted.
The rest of the array: fixtures.slice(index + 1).
Next, put them into a new array:
const newFixtures = [
...fixtures.slice(0, index), // part 1
{/* new item at 'index' */}, // part 2
...fixtures.slice(index + 1) // part 3
];
To construct the new item:
Using spread operator:
const newFixture = {
...oldFixture,
winner: /* new value */
};
Using Object.assign:
const newFixture = Object.assign({}, oldFixture, {
winner: /* new value */
});
if you write your code in such a way - this will do the job.
const toggleWinner = index => {
const { winner, teams: { home, away } } = fixtures[index];
fixtures[index].winner = winner === home ? away : home;
setUpdateFixtures([...fixtures]);
};
Setting a new array of fixtures to state is completely enough to trigger render on Fixtures component.
I have made a working example for you.
You can use libraries like immer to update nested states easily.
const initialFixtures = [{
teams: {
home: 'Liverpool',
away: 'Manchester Utd'
},
winner: 'Liverpool'
},
{
teams: {
home: 'Chelsea',
away: 'Fulham'
},
winner: 'Fulham'
}, ,
{
teams: {
home: 'Arsenal',
away: 'Tottenham'
},
winner: 'Arsenal'
}
];
const newState = immer.default(initialFixtures, draft => {
draft[1].winner = "something";
});
console.log(newState);
<script src="https://cdn.jsdelivr.net/npm/immer#1.0.1/dist/immer.umd.js"></script>
If you are comfortable to use a class based approach, you can try something like this:
Create a class that holds property value for team.
Create a boolean property in this class, say isHomeWinner. This property will decide the winner.
Then create a getter property winner which will lookup this.isHomeWinner and will give necessary value.
This will enable you to have a clean toggle function: this.isHomeWinner = !this.isHomeWinner.
You can also write your toggleWinner as:
const toggleWinner = (index) => {
const newArr = initialFixtures.slice();
newArr[index].toggle();
return newArr;
};
This looks clean and declarative. Note, if immutability is necessary then only this is required. If you are comfortable with mutating values, just pass fixture.toggle to your react component. You may need to bind context, but that should work as well.
So it would look something like:
function Fixtures = ({ fixtures, toggleWinner }) => {
fixtures.map((fixture, index) => (
<div>
<p>{fixture.winner} </p>
<button onClick = {() => fixture.toggle() }> Change Winner</button>
// or
// <button onClick = { fixture.toggle.bind(fixture) }> Change Winner</button>
</div>
))
}
Following is a sample of class and its use:
class Fixtures {
constructor(home, away, isHomeWinner) {
this.team = {
home,
away
};
this.isHomeWinner = isHomeWinner === undefined ? true : isHomeWinner;
}
get winner() {
return this.isHomeWinner ? this.team.home : this.team.away;
}
toggle() {
this.isHomeWinner = !this.isHomeWinner
}
}
let initialFixtures = [
new Fixtures('Liverpool', 'Manchester Utd'),
new Fixtures('Chelsea', 'Fulham', false),
new Fixtures('Arsenal', 'Tottenham'),
];
const toggleWinner = (index) => {
const newArr = initialFixtures.slice();
newArr[index].toggle();
return newArr;
};
initialFixtures.forEach((fixture) => console.log(fixture.winner))
console.log('----------------')
initialFixtures = toggleWinner(1);
initialFixtures.forEach((fixture) => console.log(fixture.winner))
initialFixtures = toggleWinner(2);
console.log('----------------')
initialFixtures.forEach((fixture) => console.log(fixture.winner))
const toggleWinner = (index) => {
let updatedFixtures = [...fixtures].splice(index, 1, {...fixtures[index],
winner: fixtures[index].winner === fixtures[index].teams.home
? fixtures[index].teams.away : fixtures[index].teams.home})
setUpdateFixtures(updatedFixtures);
}

Categories