React hooks: How do I update state with object - reactjs

I have 3 components like this, how I update state in App component
How I update state onclick in Counter component
import React, { useState } from 'react'
import Header from './components/Header'
import Counters from './components/Counters'
const App = () => {
const initialCounters = [
{ id: 1, value: 0 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
]
const [counters, setCounters] = useState(initialCounters)
const onIncrement = (counter) => {
console.log(counter)
}
return (
<>
<Header totalCounters={counters.length} />
<main className='container'>
<Counters counters={counters} onIncrement={onIncrement} />
</main>
</>
)
}
export default App

In your Counters component when you call the OnIncrement method you need to pass it's id as reference.
Then in your OnIncrement you do this
const onIncrement = (counterId) => {
const updatededCounters = counters.map((counter) => {
if(counter.id === counterId){
counter.value++
return counter
}
return counter
})
setCounters(updatededCounters)
}
Just to be clear in your Counters component
import React from "react";
const Counters = ({ counters, onIncrement }) => {
return (
<>
{counters.map((counter) => (
<div key={counter.id}>
<p>My counter : {counter.value}</p>
<button onClick={() => onIncrement(counter.id)}>Increment</button>
</div>
))}
</>
);
};
Full code for the parent component
import React, { useState } from "react";
import Counters from "./Counters";
const App = () => {
const initialCounters = [
{ id: 1, value: 0 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
];
const [counters, setCounters] = useState(initialCounters);
const onIncrement = (counterId) => {
const updatededCounters = counters.map((counter) => {
if (counter.id === counterId) {
counter.value++;
return counter;
}
return counter;
});
setCounters(updatededCounters);
};
return (
<>
<main className="container">
<Counters counters={counters} onIncrement={onIncrement} />
</main>
</>
);
};
export default App;
Codesandbox link: https://codesandbox.io/s/sad-chebyshev-l4hez?file=/src/App.js:0-725
Explanation
What i do the onIncrement method is simple:
i will create a new array with values i want to edit inside of it, then i'll set the state with this new array.
In the .map()
Each instance of counter will be looped, so for each instance i check if one of them is the one im looking for to update (with the same id as the counterId i receive as parameter)
If so, i edit the value of the current counter and then return the counter instance to allow the loop to continue to the next one.
When the counter id is not the same as the counterId received, i just return the counter without any modification.
So at the end, you will get the same array of values, exepct for the specific counter you incremented where you will see it's value updated by one count
I advice you to read some documentation about .map() function since it's really used in react : https://fr.reactjs.org/docs/lists-and-keys.html
And aswell you coud look into Object.keys() beceause it's often used with .map() aswell if you need to loop through object properties : https://reedbarger.com/how-to-transform-javascript-objects-the-power-of-objectkeys-values-entries/

Related

Converting a React Class Component to Function Component

I have build a small application using class component and it's working fine also.
While I convert that application to function component to use react hooks, it's giving me error.
Please check and let me know where it went wrong.
function component
import React ,{useState} from 'react';
import './App.css';
import Counters from './component/counters';
import Navbar from './component/navbar';
function App(props) {
const initialState = [
{ id: 1, value: 0 },
{ id: 2, value: 10 },
{ id: 3, value: 20 },
{ id: 4, value: 30 },
];
const [counters, setCounters] = useState(initialState);
const handleIncrement = (counter) => {
// const counters = [...counters];
// const index = counters.indexOf(counter);
// counters[index] = { ...counter };
// counters[index].value++;
setCounters({ counter : counter.value +1 });
};
const handleDecrement = (counter) => {
// // const counters = [...counters];
// const index = counters.indexOf(counter);
// counters[index] = { ...counter };
// counters[index].value--;
// setCounters({ counters });
};
const handleDelete = (counterId) => {
// const counters = counters.filter((c) => c.id !== counterId);
// setCounters({ counters });
};
return (
<div className="container">
{/* <Navbar totalCounters={counters.reduce((a,c)=>a + c.value,0 )}/> */}
<Navbar totalCounters={counters.filter((c) => c.value > 0).count()}/>
<Counters
counters={counters}
onIncrement={handleIncrement}
onDecrement={handleDecrement}
onDelete={handleDelete}
/>
</div>
);
}
export default App;
class component
import './App.css';
import Counters from './component/counters';
import Navbar from './component/navbar';
import React from 'react';
class App extends React.Component {
state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 10 },
{ id: 3, value: 20 },
{ id: 4, value: 30 },
],
};
handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};
handleDecrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value--;
this.setState({ counters });
};
handleDelete = (counterId) => {
const counters = this.state.counters.filter((c) => c.id !== counterId);
this.setState({ counters });
};
render() {
return (
<div className="container">
<Navbar totalCounters={this.state.counters.reduce((a,c)=>a + c.value,0)}/>
<Counters
counters={this.state.counters}
onIncrement={this.handleIncrement}
onDecrement={this.handleDecrement}
onDelete={this.handleDelete} />
</div>
);
}
}
export default App;
In case, if you want to see the full code then just let me know.
Issue
The issue is your counters state is an array but you are mutating the state invariant to be an object in your handlers.
Solution
App - Use a functional state update to map the counters array from the previous state to the next state, using the id to match the specific counter you want to increment/decrement/delete. This shallow copies the previous array. Notice also that the counter being updated is also shallow copied into a new object reference. When deleting a counter you are correct to use .filter to remove the specific element and return a new array.
function App(props) {
const initialState = [
{ id: 1, value: 0 },
{ id: 2, value: 10 },
{ id: 3, value: 20 },
{ id: 4, value: 30 },
];
const [counters, setCounters] = useState(initialState);
const handleIncrement = (id) => {
setCounters(counters => counters.map(counter => counter.id === id
? {
...counter,
value: counter.value + 1,
}
: counter
));
};
const handleDecrement = (id) => {
setCounters(counters => counters.map(counter => counter.id === id
? {
...counter,
value: counter.value - 1,
}
: counter
));
};
const handleDelete = (id) => {
setCounters(counters => counters.filter((c) => c.id !== id));
};
return (
<div className="container">
<Navbar totalCounters={counters.filter((c) => c.value > 0).count()}/>
<Counters
counters={counters}
onIncrement={handleIncrement}
onDecrement={handleDecrement}
onDelete={handleDelete}
/>
</div>
);
}
Counters - pass the counter id to the handlers
class Counters extends Component {
render() {
const { onIncrement, onDecrement, onDelete, counters } = this.props;
return (
<div>
{counters.map((counter) => (
<Counter
key={counter.id}
counter={counter}
onIncrement={() => {
onIncrement(counter.id);
}}
onDecrement={() => {
onDecrement(counter.id);
}}
onDelete={() => {
onDelete(counter.id);
}}
/>
))}
</div>
);
}
}
Counter - since the id is closed in callback scope above just call the event handlers
class Counter extends Component {
...
render() {
return (
<div>
<span className={this.getBadge()}>{this.getCount()}</span>
<button
className="btn btn-secondary m-2"
onClick={this.props.onIncrement}
>
Increment
</button>
<button
className="btn btn-secondary m-2"
onClick={this.props.onDecrement}
>
Decrement
</button>
<button className="btn btn-danger m-2" onClick={this.props.onDelete}>
Delete
</button>
</div>
);
}
}
In your handleIncrement() function, you're setting an object, when the setCounters method is expecting an array of objects.
You can use ES6 destructuring. Also note the object key should be the id not the object.
I expect the below to fix the error.
setCounters([...counters, { counter.id : counter.value +1 }]);
And I believe below is the implementation you're looking for.
otherCounters = counters.filter(c => c.id != counter.id)
setCounters([...otherCounters, { counter.id : counter.value +1 }])
EDIT: One thing I noticed in your counters.jsx and counter.jsx is that you're calling a function with variables that do not expect one.
From counters.jsx
<Counter
...
// onIncrement of "Counter" (the 1st onIncrement on the left)
// is a function that do not expect a variable.
onIncrement={() => {onIncrement(counter);}}
/>
From counter.jsx
<button>
...
// "onIncrement" is called with "this.props.counter". However this "onIncrement" is not expecting a variable.
onClick={() => this.props.onIncrement(this.props.counter)}
</button>
For the correct syntax, you can change either, but as example you can change counter.jsx to
<button>
...
onClick={() => this.props.onIncrement()}
</button>
In functional component on react when you wanna make state you must be use useState, you can follow my code here:
import './App.css';
import Counters from './component/counters';
import Navbar from './component/navbar';
import { useState } from 'react';
const App = () => {
const [countersState, setCountersState] = useState([
{ id: 1, value: 0 },
{ id: 2, value: 10 },
{ id: 3, value: 20 },
{ id: 4, value: 30 },
]);
const handleIncrement = (counter) => {
const counters = countersState
const index = counters.indexOf(counter);
counters[index] = { ...counter };
setCountersState(counters);
};
const handleDecrement = (counter) => {
const counters = countersState
const index = counters.indexOf(counter);
console.log(index)
counters[index] = { ...counter };
counters[index].value--;
setCountersState(counters);
};
const handleDelete = (counterId) => {
const counters = countersState.filter((c) => c.id !== counterId);
setCountersState(counters)
}
return (
<div className="container">
<Navbar totalCounters={this.state.counters.reduce((a,c)=>a + c.value,0)}/>
<Counters
counters={countersState}
onIncrement={handleIncrement}
onDecrement={handleDecrement}
onDelete={handleDelete} />
</div>
);
};
export default App;

ReactJs can't fix missing semicolon error before "render()"

I was following ProgrammingWithMosh's reactJs tutorial, and I am now stuck in the App.js file (I have other children components in my project too, but I have not posted their code in this question) due to the following error:
';' expected. [Line 41 of the following code]
This error occurs on the line where "render()" is found - right after the "render()" keyword and before its first "{" bracket.
Here is the code:
import React, { Component } from 'react';
import NavBar from "./components/navbar";
import Counters from './components/counters';
import './App.css';
function App() {
state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
],
};
handleReset = () => {
const counters = this.state.counters.map((c) => {
c.value = 0;
return c;
});
this.setState({ counters: counters });
};
handleIncrement = (counter) => {
// console.log(counter);
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
// counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters: counters });
// console.log(this.state.counters[index]);
};
handleDelete = (counterId) => {
// console.log("Event Handler Called", counterId);
const counters = this.state.counters.filter((c) => c.id !== counterId);
// console.log(counters);
this.setState({ counters: counters });
};
render() {
return (
<React.Fragment>
<NavBar />
<main className="container">
<Counters
counters={this.state.counters}
onReset={this.handleReset}
onIncrement={this.handleIncrement}
onDelete={this.handleDelete}
/>
</main>
</React.Fragment>
);
}
}
export default App;
I cannot figure out what semi colons I am missing and I have also checked to see if I am missing any ending brackets from previous functions (which to my knowledge, I am not). Please help me pinpoint the issue, and thank you so much in advance!
You're mixing a function component with a class component.
change
function App() {
to
class App extends Component {

useCallback in React seems to be not letting me update state in parent

I've created a simple example of how useCallback is not allowing me to preserve state changes. When I remove the useCallback, the counters that I store in state update as expected, but adding useCallback (which I was hoping would keep rerenders of all speaker items to not re-render) keeps resetting my state back to the original (0,0,0).
The problem code is here in codesandbox:
https://codesandbox.io/s/flamboyant-shaw-2wtqj?file=/pages/index.js
and here is the actual simple one file example
import React, { useState, memo, useCallback } from 'react';
const Speaker = memo(({ speaker, speakerClick }) => {
console.log(speaker.id)
return (
<div>
<span
onClick={() => {
speakerClick(speaker.id);
}}
src={`speakerimages/Speaker-${speaker.id}.jpg`}
width={100}
>{speaker.id} {speaker.name}</span>
<span className="fa fa-star "> {speaker.clickCount}</span>
</div>
);
});
function SpeakerList({ speakers, setSpeakers }) {
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={useCallback((id) => {
const speakersNew = speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
setSpeakers(speakersNew);
},[])}
key={speaker.id}
/>
);
})}
</div>
);
}
//
const App = () => {
const speakersArray = [
{ id: 1124, name: 'aaa', clickCount: 0 },
{ id: 1530, name: 'bbb', clickCount: 0 },
{ id: 10803, name: 'ccc', clickCount: 0 },
];
const [speakers, setSpeakers] = useState(speakersArray);
return (
<div>
<h1>Speaker List</h1>
<SpeakerList speakers={speakers} setSpeakers={setSpeakers}></SpeakerList>
</div>
);
};
export default App;
first, you can only use a hook at component body, you can't wrap it at speakerClick props function declaration. second, useCallback will keep the original speakers object reference, which will be a stale value. To solve this, you can use setSpeakers passing a callback instead, where your function will be called with the current speakers state:
function SpeakerList({ speakers, setSpeakers }) {
const speakerClick = useCallback(
(id) => {
// passing a callback avoid using a stale object reference
setSpeakers((speakers) => {
return speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
});
},
[setSpeakers] // you can add setSpeakers as dependency since it'll remain the same
);
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={speakerClick}
key={speaker.id}
/>
);
})}
</div>
);
}

How to hide react element when specific value is reached?

I'm planning to make "online shopping cart" template in react. However, I can't figure out how to hide "counters" element when "count" reaches zero
I managed to make separated button which hides element on click, but that's not suitable for this project.
counter.jsx
import React, { Component } from 'react';
import Counters from './counters';
class Counter extends Component {
state = {
count: 1
};
handleIncrement = () => {
this.setState({count: this.state.count + 1});
}
handleDelete = () => {
this.setState({count: this.state.count - 1});
if (this.state.count == 0) {
this.state.counters = {
isHidden: true
}
}
}
render() {
let classes = 'badge m-2 badge-';
classes += this.state.count === 1 ? 'primary' : 'primary';
return (
<div>
<span className={classes}>{this.formatCount()}</span>
<button onClick={() => this.handleIncrement()} className='btn btn-primary m-2'>+</button>
<button onClick={() => this.handleDelete()} className="btn btn-danger m-2">-</button>
</div>
);
}
formatCount() {
const {count} = this.state;
return count === 0 ? 'Reached' : count;
}
}
export default Counter;
counters.jsx
import React, { Component } from 'react';
import Counter from './counter';
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 1 },
{ id: 2, value: 1 },
{ id: 3, value: 1 },
{ id: 4, value: 1 }
]
}
render() {
return (
<div>
{this.state.counters.map(
counter => (
<Counter key={counter.id} value={counter.value} id={counter.id} />
))}
</div>
);
}
}
export default Counters;
So, when delete button is pressed it should go from 1 to 0, and when it reaches zero, counters element should disappear(based on Id).
Generally https://stackoverflow.com/a/54649508/10868273 answer should be the way forward but you in order not to change too much you could try this (i've included only the changes you want to make)
counter.jsx
handleIncrement = () => {
this.setState((state) => ({
count: state.count + 1
}));
}
handleDelete = () => {
this.setState((state) => ({
count: state.count - 1
}),() => {
if (this.state.count <= 0){
this.props.handleCounterRemove(this.props.id);
}
});
}
Counters.jsx
import React, { Component } from 'react';
import Counter from './counter';
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 1 },
{ id: 2, value: 1 },
{ id: 3, value: 1 },
{ id: 4, value: 1 }
]
}
handleCounterRemove = (counterId) =>{
const { counters } = this.state;
this.setState((state)=> ({
counters: state.counters.filter(counter => counter.id !== counterId)
}));
}
render() {
return (
<div>
{this.state.counters.map(
counter => (
<Counter key={counter.id} value={counter.value} id={counter.id} handleCounterRemove={this.handleCounterRemove} />
))}
</div>
);
}
}
export default Counters;
As has been mentioned in the other answers, you do not want to just call setState if the next value depends on the pervious one: you can read more on this here React SetState documentation
Also, notice the introduction of handleCounterRemove in the Counters component it's like the only way a child component can affect it's parent component
As Joe mentioned in the comments, you are mutating the state directly, which is very anti-pattern. Always use setState().
Also, whenever you are setting a new state that depends on a previous state variable, pass in a callback to setState() rather than an object. This can save you a few headaches in certain cases.
handleDelete = () => {
this.setState(prevState => ({
count: prevState.count - 1,
isHidden: prevState.count === 1 // Hide if we are decreasing count from 1 to 0
}));
}
Keep Counter as a stateless component. Store the counter state in the Counters component itself. Move the increment, decrement functions to Counters component. In decrement function, write logic such that when it hits 0, remove the corresponding counter object from this.state.counters.
You can also return null from your render() method in counter.jsx, when this.state.coun is 0
You should redevelop the method handleDelete. Here is the possible version
handleDelete = () => {
let currentCount = this.state.count - 1;
this.setState({
count: currentCount
counters: {
isHidden: currentCount === 0
}
});
}

React JS, how to show state in two pages

I am currently developing a website for counting points, the user can give points to each player. The total of all points are calculated and shown below the counters on the administrator page. Now what I want is that I also want to show the total value in a different page, where players can see how much points their team has scored. But this total value has to be in sync with the total value in the administrator page. How can I do this? I heard about axios, but have no idea how this works. Can someone help me?
My Code:
Counter
import React, { Component } from "react";
import ReactDOM from "react-dom";
export default class Counter extends Component {
render() {
const { onIncrement, onDecrement } = this.props;
return (
<div>
<span>{this.formatCount()}</span>
<button onClick={() => onIncrement(this.props.counter)}>
Add
</button>
<button
onClick={() => onDecrement(this.props.counter)}
disabled={this.props.counter.value === 0 ? "disabled" : ""}
>
Delete
</button>
</div>
);
}
formatCount() {
const { value } = this.props.counter;
return value;
}
}
if (document.getElementById("counter")) {
ReactDOM.render(<Counter />, document.getElementById("counter"));
}
Counters
import React, { Component } from "react";
import ReactDOM from "react-dom";
import Counter from "./counter";
class Counters extends Component {
constructor() {
super();
this.state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
{ id: 5, value: 0 }
],
total: 0
};
}
handleIncrement(counter) {
const total = this.state.total + 1;
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters: counters, total: total });
}
handleDecrement(counter) {
const total = this.state.total - 1;
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value--;
this.setState({ counters: counters, total: total });
}
handleReset() {
const total = 0;
const counters = this.state.counters.map(c => {
c.value = 0;
return c;
});
this.setState({ counters: counters, total: total });
}
render() {
return (
<div>
<button onClick={this.handleReset.bind(this)}>Reset</button>
{this.state.counters.map(counter => (
<Counter
key={counter.id}
onIncrement={this.handleIncrement.bind(this)}
onDecrement={this.handleDecrement.bind(this)}
counter={counter}
/>
))}
<span>{this.state.total}</span>
</div>
);
}
}
export default Counters;
if (document.getElementById("counters")) {
ReactDOM.render(<Counters />, document.getElementById("counters"));
}
Axios is an http client so that wouldn't apply to your problem.
Theres multiple solutions to your problem.
Option #1: Callbacks
Depending on how your admin and non-admin page is structured/nested this option might be easy or difficult to implement.
You could have a parent component that holds all your counter value data and renders either your admin or non-admin component that shows your counters.
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
{ id: 5, value: 0 }
],
total: 0
};
}
...
render() {
return (
<Switch>
<Route exact path="/admin" render={() => <Admin counters={this.state.counters} total={this.state.total} />}/>
<Route exact path="/user" render={() => <Admin counters={this.state.counters} total={this.state.total} />}/>
</Switch>
);
}
}
This will keep in sync because the counters are only being stored in one place, the Parent component.
Option #2: Global state management library
The alternative is a state management library like Redux. This gives you a global state that does not get deleted when a component is unmounted unlike your local this.state. You would put counters into this global state then your admin would perform actions on it, like increment and decrement, and admin and non-admin components would fetch counter values from it similar to local state by calling this.props.reduxStateCounters, for example.
More can be read about Redux here.

Resources