New to react and I am removing the local state in my counter component and will be relying on the props to receive the data that it needs. I believe this is called a controlled component. After I got rid of the state and changed every where I was using this.state to this.props, I am no longer able to see the box that displays the value when I click my increment button. I will post all the code down below.
/* Counter Component*/
import React, { Component } from "react";
class Counter extends Component {
renderTags() {
return (
<ul>
{this.state.tags.length === 0 && <p> There are no tags </p>}
{this.state.tags.map(tag => (
<li key={tag}> {tag} </li>
))}
</ul>
);
}
// You can do styles this way or do it inline
// styles = {
// fontSize: 50,
// fontWeight: "bold"
// };
render() {
return (
<div>
<span style={{ fontSize: 20 }} className={this.getBadgeClasses()}>
{this.formatCount()}
</span>
<button
onClick={() => this.props.onIncrement(this.props.counter)}
className="btn btn-secondary btn-sm"
>
Increment
</button>
<button
onClick={() => this.props.onDelete(this.props.counter.id)}
className="btn btn-danger btn-sm m-2"
>
Delete
</button>
{/* {this.renderTags()}
<p>{this.state.tags.length === 0 && "Please create a new tag"}</p> */}
</div>
);
}
getBadgeClasses() {
let classes = "badge m-2 badge-";
classes += this.props.counter.value === 0 ? "warning" : "primary";
return classes;
}
formatCount() {
const { count } = this.props.counter;
return count === 0 ? "Zero" : count;
}
}
export default Counter;
/* Counters Component */
import React, { Component } from "react";
import Counter from "./counter";
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 5 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
handleIncrement = counter => {
console.log(counter);
};
handleReset = () => {
const counters = this.state.counters.map(c => {
c.value = 0;
return c;
});
this.setState({ counters });
};
handleDelete = counterID => {
const counters = this.state.counters.filter(c => c.id !==
counterID);
this.setState({ counters });
};
render() {
return (
<React.Fragment>
<button onClick={this.handleReset} className="btn btn-dark btn-sm m-2">
Reset
</button>
{this.state.counters.map(counter => (
<Counter
key={counter.id}
onDelete={this.handleDelete}
counter={counter}
onIncrement={this.handleIncrement}
/>
))}
</React.Fragment>
);
}
}
export default Counters;
You can't see the values since you are using a wrong key for your counter.
formatCount() {
const { count } = this.props.counter;
return count === 0 ? "Zero" : count;
}
There isn't any key named count in your counter. It is value. So, you should use it or you need to destruct it like this:
const { value: count } = this.props.counter
But, using the same name is more consistent I think. Also, your Counter component would be a stateless one since you don't need any state or lifecycle method there.
One extra change would be done to the handler methods like onClick for onIncrement. If you use an arrow function, that function will be recreated in every render. You can use an extra handler method. Here is the complete working example (simplified for a clear view).
class Counters extends React.Component {
state = {
counters: [
{ id: 1, value: 5 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 }
]
};
handleIncrement = counter => {
const { counters } = this.state;
const newCounters = counters.map( el => {
if( el.id !== counter.id ) { return el; }
return { ...counter, value: counter.value + 1 }
} )
this.setState({ counters: newCounters});
};
handleReset = () => {
const counters = this.state.counters.map(c => {
c.value = 0;
return c;
});
this.setState({ counters });
};
handleDelete = counter => {
const { id: counterID } = counter;
const counters = this.state.counters.filter(c => c.id !== counterID);
this.setState({ counters });
};
render() {
return (
<div>
<button onClick={this.handleReset} className="btn btn-dark btn-sm m-2">
Reset
</button>
{this.state.counters.map(counter => (
<Counter
key={counter.id}
onDelete={this.handleDelete}
counter={counter}
onIncrement={this.handleIncrement}
/>
))}
</div>
);
}
}
const Counter = props => {
const { counter, onIncrement, onDelete} = props;
function formatCount(){
const { value } = counter;
return value === 0 ? "Zero" : value;
}
function handleIncrement(){
onIncrement( counter );
}
function handleDelete(){
onDelete( counter );
}
return (
<div>
<span>
{formatCount()}
</span>
<button
onClick={handleIncrement}
>
Increment
</button>
<button
onClick={handleDelete}
>
Delete
</button>
</div>
);
}
ReactDOM.render(<Counters />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Related
My code below shows my current component design. This is a counter component which is responsible for incrementing a counter for the respective array item and also for adding the clicked item to the cart. I am trying to figure out if there is some way in which I can assign each array item within the items array to its own state count value. Currently, the screen shows four array items, with each one having a button next to it and also a count. When clicking the increment button for any particular item, the state count for all buttons is updated and rendered, which is not what I want. I have tried to assign each button it's own state count in several ways, but haven't been able to figure out the right way. I would like to somehow bind a state count value to each button so that each one has it's individual state count.I would really appreciate if someone can provide some tips or insight as I dont know of a way to isolate the state count for each button and make it unique so that when one value's button is clicked, only the state count for that particular button (located next to the increment button) is updated and not the others.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
cart: [],
};
}
handleIncrement = (e) => {
this.setState({
count: this.state.count + 1,
cart: [...this.state.cart, e.target.value],
});
};
render() {
const listItems = this.props.items.map((item) => (
<li key={item.id}>
{item.value}
<button onClick={this.handleIncrement}>+</button>
{this.state.count}
</li>
));
return (
<div>
{listItems}
</div>
);
}
}
What I did here is I remove the constructor, update Counter component props, update the event on how to update your cart in Example component, adjusted the Counter component, for the Cart component, I added componentDidMount and shouldComponentUpdate make sure that the component will re-render only when props listArray is changing. Here's the code.
class Example extends React.Component {
state = {
cart: [],
items: [
{ id: 1, value: "L1" },
{ id: 2, value: "L2" },
{ id: 3, value: "L3" },
{ id: 4, value: "L4" }
]
}
render() {
const { cart } = this.state
return (
<div>
<h1>List</h1>
{ items.map(
({ id, ...rest }) => (
<Counter
key={ id }
{ ...rest }
cart={ cart }
onAddToCard={ this.handleAddCart }
/>
)
) }
</div>
)
}
handleAddCart = (item) => {
this.setState(({ items }) => ([ ...items, item ]))
}
}
class Counter extends React.Component {
state = {
count: 0
}
handleIncrement = () => {
this.setState(({ count }) => ({ count: count++ }))
}
render() {
const { count } = this.state
const { cart, value } = this.props
return (
<div>
{ value }
<span>
<button onClick={ this.handleIncrement }>+</button>
{ count }
</span>
<Cart listArray={ cart } />
</div>
)
}
}
class Cart extends React.Component {
state = {
cart: []
}
addTo = () => (
<div>List: </div>
)
componentDidMount() {
const { cart } = this.props
this.setState({ cart })
}
shouldComponentUpdate({ listArray }) {
return listArray.length !== this.state.cart.length
}
render() {
return (
<div>
<ListFunctions addClick={ this.addTo } />
</div>
)
}
}
const ListFunctions = ({ addClick }) => (
<div>
<button onClick={ addClick }>Add To List</button>
</div>
)
If you want to add to the list of items without rendering the button, you can add a custom property to mark that it is a custom addition:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [
{ id: 1, value: "L1" },
{ id: 2, value: "L2" },
{ id: 3, value: "L3" },
{ id: 4, value: "L4" },
]
}
}
addToItems = items => {
this.setState({
items,
});
}
render() {
var cartArray = [];
return (
<div>
<h1>List</h1>
{this.state.items.map((item) =>
<Counter
key={item.id}
value={item.value}
id={item.id}
custom={item.custom}
cart={cartArray}
addToItems={this.addToItems}
items={this.state.items}
/>
)}
</div>
);
}
}
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleIncrement = () => {
this.setState({
count: this.state.count + 1,
});
this.props.cart.push(this.props.value);
};
addTo = () => {
const { items } = this.props;
let lastId = items.length;
lastId++;
this.props.addToItems([
...items,
{
id: lastId,
value: `L${lastId}`,
custom: true,
}]);
};
render() {
return (
<div>
{this.props.value}
{
!this.props.custom &&
(
<span>
<button onClick={this.handleIncrement}>+ </button>
{this.state.count}
</span>
)
}
<Cart addTo={this.addTo} />
</div>
);
}
}
class Cart extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<ListFunctions
addClick={this.props.addTo}
/>
</div>
);
return null;
}
}
const ListFunctions = ({ addClick}) => (
<div>
<button onClick={addClick}>Add To List</button>
</div>
);
// Render it
ReactDOM.render(
<Example />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I am using MERN stack and Redux. I have created a function to update a property within a database. I have tested the api on Postman and it works. When i try and run it it seems to clash with another function and i get the error 'TypeError: this.props.subjects.map is not a function' which works prior to the trueVote function being called. Anyone any idea what i am missing here?
Print outs show the action and reducer are being hit but not the api, even though that works on Postman. The function is being called from within the voteHandler
EDIT: The console.log message within the api doesn't print but shows in the terminal window on VS when i refresh my page after the error the function has done what it should and the relevant data has been updated. Is it a case of managing the error? If so how would i do this so it doesn't crash the app?
api
// put req for a true vote
subjectRouter.put("subject/true/:_id/:currTrue", (req, res) => {
console.log("True api hitting");
Subject.findOneAndUpdate(
{ _id: req.params._id },
{
true: Number(req.params.currTrue) + 1,
},
{
new: true,
useFindAndModify: false,
}
)
.then((subject) => res.json(subject))
.catch((err) => console.log(err));
});
action
// true vote
export const trueVote = (_id, currTrue) => (dispatch) => {
console.log("trueVote hitting");
fetch(`/api/subjects/subject/true/${_id}/${currTrue}`, {
method: "PUT",
})
.then((res) => res.json())
.then((subject) =>
dispatch({
type: TRUE_VOTE,
subjects: subject,
})
);
};
reducer
case TRUE_VOTE:
console.log("true reducer hitting");
return {
...state,
items: action.subjects,
};
component
import React, { Component } from "react";
import PropTypes from "prop-types";
import GoogleSearch from "./GoogleSearch";
import { connect } from "react-redux";
import { fetchLatestSubjects } from "../../actions/subject";
import { fetchTopicSubjects } from "../../actions/subject";
import { fetchTopicComments } from "../../actions/comment";
import { fetchComments } from "../../actions/comment";
import { rateSubject } from "../../actions/subject";
import { fetchUsers } from "../../actions/authActions";
import { rateUser } from "../../actions/authActions";
import { rateComment } from "../../actions/comment";
import { trueVote } from "../../actions/subject";
import { falseVote } from "../../actions/subject";
class Subject extends Component {
// on loading the subjects and comments
// are fetched from the database
componentDidMount() {
this.props.fetchLatestSubjects();
this.props.fetchComments();
this.props.fetchUsers();
}
constructor(props) {
super(props);
this.state = {
// set inital state for subjects
// description, summary and comments all invisible
viewDesription: -1,
viewSummary: -1,
comments: [],
topic: "subjects",
};
}
componentWillReceiveProps(nextProps) {
// new subject and comments are added to the top
// of the arrays
if (nextProps.newPost) {
this.props.subjects.unshift(nextProps.newPost);
}
if (nextProps.newPost) {
this.props.comments.unshift(nextProps.newPost);
}
}
clickHandler = (id) => {
// when a subject title is clicked pass in its id
const { viewDescription } = this.state;
this.setState({ comments: [] });
var temp = [];
// get the details of the author of the subject and save to state
const subject = this.props.subjects.find((subject) => subject._id === id);
const user = this.props.users.find((user) => user._id === subject.author);
// save comments for subject to temp array
var i;
for (i = 0; i < this.props.comments.length; i++) {
if (this.props.comments[i].subject === id) {
temp.unshift(this.props.comments[i]);
}
}
console.log(temp);
// for each comment add a property with the authors name
temp.forEach((comment) => {
var commentAuthor = this.props.users.find(
(user) => user._id === comment.author
);
comment.authName = commentAuthor.name;
});
// save the subject id to local storage
// this is done incase a new comment is added
// then the subject associated with it can be retrieved
// and added as a property of that comment
localStorage.setItem("passedSubject", id);
localStorage.setItem("passedTopic", subject.topic);
// add all changes to the state
this.setState({
viewDescription: viewDescription === id ? -1 : id,
comments: temp,
subAuthor: user.name,
authRating: user.rating,
authNoOfVotes: user.noOfVotes,
});
};
// hovering on and off subjects toggles the visibility of the summary
hoverHandler = (id) => {
this.setState({ viewSummary: id });
};
hoverOffHandler = () => {
this.setState({ viewSummary: -1 });
};
rateHandler = (id, rate, item) => {
if (item === "subject") {
// this function rates the subject and the author
const subject = this.props.subjects.find((subject) => subject._id === id);
const author = this.props.users.find(
(user) => user._id === subject.author
);
// call the rateSubject and rateUser functions
this.props.rateSubject(id, rate, subject.noOfVotes, subject.rating);
this.props.rateUser(author._id, rate, author.noOfVotes, author.rating);
console.log(author.name);
alert("Thank you for rating this subject.");
} else if (item === "comment") {
const comment = this.props.comments.find((comment) => comment._id === id);
const author = this.props.users.find(
(user) => user._id === comment.author
);
// call the rateComment and rateUser functions
this.props.rateComment(id, rate, comment.noOfVotes, comment.rating);
this.props.rateUser(author._id, rate, author.noOfVotes, author.rating);
console.log(author.name);
alert("Thank you for rating this comment.");
}
};
voteHandler = (id, currVote, vote) => {
if (vote == "True") {
console.log(id, currVote, vote);
this.props.trueVote(id, currVote);
} else if (vote == "False") {
console.log(id, currVote, vote);
this.props.falseVote(id, currVote);
}
};
render() {
const subjectItems = this.props.subjects.map((subject) => {
// if the state equals the id set to visible if not set to invisible
var view = this.state.viewDescription === subject._id ? "" : "none";
var hover = this.state.viewSummary === subject._id ? "" : "none";
var comments = this.state.comments;
var subjectAuthor = this.state.subAuthor;
var authRating = this.state.authRating;
var authNoOfVotes = this.state.authNoOfVotes;
var className = "";
if (subject.category === "Education") {
className = "Education";
} else if (subject.category === "Environment") {
className = "Environment";
} else if (subject.category === "Politics") {
className = "Politics";
} else if (subject.category === "Health") {
className = "Health";
} else if (subject.category === "Other") {
className = "Other";
}
return (
<div key={subject._id}>
<div
className={className}
onMouseEnter={() => this.hoverHandler(subject._id)}
onMouseLeave={() => this.hoverOffHandler()}
>
<p className="title" onClick={() => this.clickHandler(subject._id)}>
{subject.title}
</p>
<p className="vote" style={{ textAlign: "Right" }}>
True:{" "}
{((100 / (subject.true + subject.false)) * subject.true).toFixed(
1
)}
% {" False: "}
{((100 / (subject.true + subject.false)) * subject.false).toFixed(
1
)}
%
</p>
<p className="summary" style={{ display: hover }}>
{subject.summary}
</p>
</div>
<div className="subjectBody " style={{ display: view }}>
<div className="leftSubjectBody">
<div className="subjectAuthor">
<p className="author">
Subject created by: {subjectAuthor} -{" "}
{(authRating / authNoOfVotes).toFixed(1)}/5 Star user
{/* <br /> {subject.date} */}
</p>
</div>
<div className="subjectDescription">
<p className="description">{subject.description}</p>
</div>
<div className="subjectLinks">Links: {subject.links}</div>
</div>
<div className="rightSubjectBody">
<div className="rate">
<p> Rate this subject:</p>
<br />
<button
onClick={() => this.rateHandler(subject._id, 1, "subject")}
>
1
</button>
<button
onClick={() => this.rateHandler(subject._id, 2, "subject")}
>
2
</button>
<button
onClick={() => this.rateHandler(subject._id, 3, "subject")}
>
3
</button>
<button
onClick={() => this.rateHandler(subject._id, 4, "subject")}
>
4
</button>
<button
onClick={() => this.rateHandler(subject._id, 5, "subject")}
>
5
</button>
<p>
Rating: {(subject.rating / subject.noOfVotes).toFixed(1)}/5
</p>
</div>
<div className="voting">
<p>
Do you think this subject question is true or false based on
the evidence provided and your own reseach in the area? <br />
</p>
<p>Please vote and leave comments.</p>
<br />
<div
className="voteButton"
onClick={() =>
this.voteHandler(subject._id, subject.true, "True")
}
>
TRUE
</div>
<div
className="voteButton"
onClick={() =>
this.voteHandler(subject._id, subject.false, "False")
}
>
FALSE
</div>
</div>
</div>
<div className="subjectComments">
<p style={{ fontWeight: "bold" }}>Comments:</p>
{comments.map((comment, i) => {
return (
<div key={i} className="singleComment">
<p>
{comment.title}
<br />
{comment.comment}
<br />
Comment by : {comment.authName} - This user has a rating
of {(comment.rating / comment.noOfVotes).toFixed(1)}/5
STARS
</p>
<div className="rate">
Rate this comment:
<button
onClick={() =>
this.rateHandler(comment._id, 1, "comment")
}
>
1
</button>
<button
onClick={() =>
this.rateHandler(comment._id, 2, "comment")
}
>
2
</button>
<button
onClick={() =>
this.rateHandler(comment._id, 3, "comment")
}
>
3
</button>
<button
onClick={() =>
this.rateHandler(comment._id, 4, "comment")
}
>
4
</button>
<button
onClick={() =>
this.rateHandler(comment._id, 5, "comment")
}
>
5
</button>
<p>
Rating:{" "}
{(comment.rating / comment.noOfVotes).toFixed(1)}/5
</p>
</div>
</div>
);
})}
<br />
<a href="/addcomment">
<div className="buttonAddComment">ADD COMMENT</div>
</a>
</div>
</div>
</div>
);
});
return (
<div id="Subject">
<GoogleSearch />
{subjectItems}
</div>
);
}
}
Subject.propTypes = {
fetchLatestSubjects: PropTypes.func.isRequired,
fetchTopicSubjects: PropTypes.func.isRequired,
fetchTopicComments: PropTypes.func.isRequired,
fetchComments: PropTypes.func.isRequired,
fetchUsers: PropTypes.func.isRequired,
rateSubject: PropTypes.func.isRequired,
rateComment: PropTypes.func.isRequired,
rateUser: PropTypes.func.isRequired,
trueVote: PropTypes.func.isRequired,
falseVote: PropTypes.func.isRequired,
subjects: PropTypes.array.isRequired,
comments: PropTypes.array.isRequired,
users: PropTypes.array.isRequired,
newPost: PropTypes.object,
};
const mapStateToProps = (state) => ({
subjects: state.subjects.items,
newSubject: state.subjects.item,
comments: state.comments.items,
users: state.auth.users,
newComment: state.comments.item,
});
// export default Subject;
export default connect(mapStateToProps, {
fetchLatestSubjects,
fetchTopicSubjects,
fetchTopicComments,
fetchComments,
fetchUsers,
rateSubject, // rate subject
rateUser,
rateComment,
trueVote,
falseVote,
})(Subject, Comment);
Because you are returning a single subject as part of your reducer action, you presumably want to exchange the existing subject with the updated subject, so you would need to update your reducer to be:
case TRUE_VOTE:
const index = state.items.subjects.findIndex( subject => action.subjects.id === subject.id );
return {
items: [...state.items.slice(0, index), action.subjects, ...state.items.slice( index + 1 )] };
};
To make it a bit more clear, you could of course also change your action to indicate it's only a single subject you are returning
// true vote
export const trueVote = (_id, currTrue) => (dispatch) => {
console.log("trueVote hitting");
fetch(`/api/subjects/subject/true/${_id}/${currTrue}`, {
method: "PUT",
})
.then((res) => res.json())
.then((subject) =>
dispatch({
type: TRUE_VOTE,
subject
})
);
};
Which would then be more clear inside your reducer that you are only expecting 1 subject
It's my first day learning react and I'm stuck with an issue (I'm following Mosh's tutorial):
import React, { Component } from "react";
class Counter extends Component {
state = {
value: this.props.value,
};
handleIncrement = () => {
console.log("Click!");
this.setState({ value: this.state.value + 1 });
};
handleDecrement = () => {
console.log("Click!");
if (this.state.value !== 0) {
this.setState({ value: this.state.value - 1 });
}
};
render() {
return (
<div className="row align-items-center">
<div className="col">
<span className={this.getBadgeClasses()}>{this.formatCount()}</span>
</div>
<div className="col">
<button
onClick={() => this.handleIncrement({ id: 1 })}
className="btn btn-dark"
>
+
</button>
<button
onClick={() => this.handleDecrement({ id: 1 })}
className={this.isLessThanZero()}
>
-
</button>
</div>
<div className="col">
<button
onClick={() => this.props.onDelete(this.props.id)}
className="btn btn-danger m-2"
>
Delete
</button>
</div>
</div>
);
}
isLessThanZero() {
let classes = "btn btn-dark ";
classes += this.state.value === 0 ? "disabled" : "";
return classes;
}
getBadgeClasses() {
let classes = "badge m-2 badge-";
classes += this.state.value === 0 ? "warning" : "primary";
return classes;
}
formatCount() {
let { value } = this.state;
return value === 0 ? <h1>Zero</h1> : value;
}
}
export default Counter;
This is a counter component that just responds to the buttons. I'm including those in another component:
import React, { Component } from "react";
import Counter from "./counter";
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
],
};
handleDelete = (counterId) => {
console.log("Event detected! Delete", counterId);
};
render() {
return (
<div className="cont">
{this.state.counters.map((counter) => (
<Counter
value={counter.value}
key={counter.id}
onDelete={this.handleDelete}
></Counter>
))}
</div>
);
}
}
export default Counters;
In the handleDelete function, when called, I'm getting undefined for the counterId. When I check in the ReactComponents Chrome extention, I see that there isn't any ID:
Why is this happening?
The problem is you are not passing the counter for this.handleDelete. You need to explicitly pass it.
<Counter
value={counter.value}
key={counter.id}
onDelete={() => this.handleDelete(counter.id)}
/>
In the above snippet, I am passing a new function to the Counter component, the function just calls this.handleDelete with the counter.id of the corresponding component.
I have 3 counter buttons but I want a separate button that will onClick increment all the counters by 1. What is the best way to implement it and to have the state change all of the counters at once? I tried adding a countAll and combining all the counts but the syntax seemed off and I am not sure how to do it.
import React, { Component } from 'react';
import Button from './components/Button';
class App extends Component {
constructor(props) {
super(props);
this.state = { counter1: 0, counter2: 0, counter3: 0 };
}
incrementCount1() {
this.setState(prevState => ({ counter1: prevState.counter1 + 1 }));
}
incrementCount2() {
this.setState(prevState => ({ counter2: prevState.counter2 + 1 }));
}
incrementCount3() {
this.setState(prevState => ({ counter3: prevState.counter3 + 1 }));
}
decrementCount1() {
this.setState(prevState => ({ counter1: prevState.counter1 - 1 }));
}
decrementCount2() {
this.setState(prevState => ({ counter2: prevState.counter2 - 1 }));
}
decrementCount3() {
this.setState(prevState => ({ counter3: prevState.counter3 - 1 }));
}
render() {
let { counter1, counter2, counter3 } = this.state
return (
<div className="App">
<h2>Count: { counter1 }</h2>
<Button title = { "+" } task = { () => this.incrementCount1(counter1) } />
<Button title = { "-" } task = { () => this.decrementCount1(counter1) } />
<h2>Count: { counter2 }</h2>
<Button title = { "+" } task = { () => this.incrementCount2(counter2) } />
<Button title = { "-" } task = { () => this.decrementCount2(counter2) } />
<h2>Count: { counter3 }</h2>
<Button title = { "+" } task = { () => this.incrementCount3(counter3) } />
<Button title = { "-" } task = { () => this.decrementCount3(counter3) } />
</div>
);
}
}
export default App;
Sample using bracket notation and public class fields syntax
countOperation = (field, diff) => () => {
this.setState(prevState => ({ [field]: prevState[field] + diff }));
};
<button title={"+"} onClick={this.countOperation("counter1", 1)} />
<button title={"-"} onClick={this.countOperation("counter1", -1)} />
Addition
If you like, you can make one step further to package the set of buttons to a common HOC which can return id on certain callback.
In this way, you won't need to bind the index/key for each of your elements multiple times if there are multiple callbacks.
countOperation = diff => (e, id) => {
this.setState(prevState => ({ [id]: prevState[id] + diff }));
};
<CustomButton
id="counter1"
title={"+"}
onClick={this.countOperation(1)}
/>
class CustomButton extends React.Component {
render() {
const { id, title, onClick } = this.props;
return <button title={title} onClick={e => onClick(e, id)} />;
}
}
I really like #keikai's solution for code reduction/DRY-principal, but if you didn't want to change your existing state shape, AND if your existing state is only counters, then this would do the trick by operating over the state as an object.
Takes the state object, converts to array of entries, and reduces them back to an object that represents the next state with all counters incremented by the incrementBy amount.
incrementAll(incrementBy = 0) {
this.setState(prevState =>
Object.entries(prevState).reduce((counters, [counterKey, count]) => {
counters[counterKey] = count + incrementBy;
return counters;
}, {})
);
}
Usage
<Button title = { "+ all" } task = { () => this.incrementAll(1) } />
<Button title = { "- all" } task = { () => this.incrementAll(-1) } />
I am new to ReactJS. I am trying to build an online shopping type of feature wherein we can increment the quantity and decrement it. Or directly remove an item using 'delete' button. I am trying to print the id of the button which is clicked, using console.log("delete clicked",counterId); in "counters.jsx". But some how it gives me undefined each time. Here is my code.
counters.jsx
import React,{Component} from 'react';
import Counter from './counter';
class Counters extends Component {
state = {
counters:[
{id:1,value:4},
{id:2,value:0},
{id:3,value:3},
{id:4,value:0}
]
};
handleDelete=(counterId)=>{
console.log("delete clicked",counterId);
}
render(){
return (
<div>
{this.state.counters.map(counter=>
<Counter key={counter.id} onDelete={this.handleDelete} value={counter.value}/>)}
</div>
);
}
}
export default Counters;
counter.jsx
import React, { Component} from 'react';
class Counter extends Component{
state={
value: this.props.value
};
handleIncrement=()=>{
this.setState({value:this.state.value+1});
}
handleDecrement=()=>{
this.setState({value:this.state.value-1});
}
render(){
return (
<React.Fragment>
<p>
<span className={this.getBadgeClasses()}>{this.formatCount()}</span>
<button onClick={this.handleIncrement} className="btn btn-secondary btn-sm m-1">Increment</button>
<button onClick={this.handleDecrement} className="btn btn-warning btn-sm m-1">Decrement</button>
<button onClick={()=>this.props.onDelete(this.props.id)} className="btn btn-danger btn-sm m-1">Delete</button>
</p>
</React.Fragment>
);
}
getBadgeClasses(){
let classes="badge m-2 badge-";
classes+=this.state.value===0?"warning":"primary";
return classes;
}
formatCount(){
const {value} = this.state;
return value===0 ? 'Zero': value;
}
}
export default Counter;
Any help will be much appreciated!
Read this once.
https://reactjs.org/docs/lists-and-keys.html#keys-must-only-be-unique-among-siblings
With the example above, the Post component can read props.id, but not props.key.
Therefore, you have to add props.id={counter.id}!
counters.jsx
before
render(){
return (
<div>
{this.state.counters.map(counter=>
<Counter key={counter.id} onDelete={this.handleDelete} value={counter.value}/>)}
</div>
);
}
after
render(){
return (
<div>
{this.state.counters.map((counter, index)=>
<Counter key={index} id={counter.id} onDelete={this.handleDelete} value={counter.value}/>)}
</div>
);
}
You are not passing counter as a prop to your Counter component, you are just passing counter.value. Instead of doing this pass the counter itself:
<Counter key={counter.id} onDelete={this.handleDelete} counter={counter} /> )}
Then in Counter component:
state={
value: this.props.counter.value,
};
Also, if you separate your handleDelete function here and use its reference, it is not recreated in every render. Like:
handleDelete = () => this.props.onDelete( this.props.counter.id );
and
<button onClick={this.handleDelete} className="btn btn-danger btn-sm m-1">Delete</button>
But, your logic is somehow weird. You have a state in parent component, then you also keep another state in the child and do the increments, decrements there. Your parent's state does not change. Is that what you really want? So, you want multiple counters and keep their state separately like that?
Here is the full code:
class Counters extends React.Component {
state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 3 },
{ id: 4, value: 0 },
],
};
handleDelete=( counterId ) => {
console.log( "delete clicked", counterId );
}
render() {
return (
<div>
{this.state.counters.map( counter =>
<Counter key={counter.id} onDelete={this.handleDelete} counter={counter} /> )}
</div>
);
}
}
class Counter extends React.Component {
state={
value: this.props.counter.value,
};
handleIncrement=() => {
this.setState( { value: this.state.value + 1 } );
}
handleDecrement=() => {
this.setState( { value: this.state.value - 1 } );
}
handleDelete = () => this.props.onDelete( this.props.counter.id );
render() {
return (
<div>
<p>
<span className={this.getBadgeClasses()}>{this.formatCount()}</span>
<button onClick={this.handleIncrement} className="btn btn-secondary btn-sm m-1">Increment</button>
<button onClick={this.handleDecrement} className="btn btn-warning btn-sm m-1">Decrement</button>
<button onClick={this.handleDelete} className="btn btn-danger btn-sm m-1">Delete</button>
</p>
</div>
);
}
getBadgeClasses() {
let classes = "badge m-2 badge-";
classes += this.state.value === 0 ? "warning" : "primary";
return classes;
}
formatCount() {
const { value } = this.state;
return value === 0 ? "Zero" : value;
}
}
ReactDOM.render(
<Counters />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
In case of you want to see here is the alternative approach:
class Counters extends React.Component {
state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 3 },
{ id: 4, value: 0 },
],
};
handleDelete = ( counter ) => {
const newCounters = this.state.counters.filter( el => el.id !== counter.id );
this.setState( { counters: newCounters } );
}
handleCounter = ( counter, direction ) => {
const newCounters = this.state.counters.map( ( el ) => {
if ( el.id !== counter.id ) { return el; }
return direction === "up" ? { ...counter, value: counter.value + 1 }
: { ...counter, value: counter.value - 1 };
} );
this.setState( { counters: newCounters } );
}
render() {
return (
<div>
{this.state.counters.map( counter =>
( <Counter
key={counter.id}
onDelete={this.handleDelete}
counter={counter}
handleCounter={this.handleCounter}
/> ) )}
</div>
);
}
}
const Counter = ( props ) => {
const { counter, handleCounter, onDelete } = props;
function handleIncrement() {
handleCounter( counter, "up" );
}
function handleDecrement() {
handleCounter( counter );
}
function handleDelete() { onDelete( counter ); }
function getBadgeClasses() {
let classes = "badge m-2 badge-";
classes += counter.value === 0 ? "warning" : "primary";
return classes;
}
function formatCount() {
const { value } = counter;
return value === 0 ? "Zero" : value;
}
return (
<div>
<p>
<span className={getBadgeClasses()}>{formatCount()}</span>
<button onClick={handleIncrement} className="btn btn-secondary btn-sm m-1">Increment</button>
<button onClick={handleDecrement} className="btn btn-warning btn-sm m-1">Decrement</button>
<button onClick={handleDelete} className="btn btn-danger btn-sm m-1">Delete</button>
</p>
</div>
);
};
ReactDOM.render(
<Counters />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>