I am working on a simple react app. My task is to update the product price as the quantity updates. I have an array of products in my App.js file.
And also two functions for increment and decrement of the quantity of the product.
import "./App.css";
import Navbar from "./components/navbar";
import ProductList from "./components/ProductList";
import Footer from "./components/Footer";
import React, { useState } from "react";
function App() {
let productList = [
{
price: 99999,
name: "Iphone 10S Max",
quantity: 0,
},
{
price: 999,
name: "Realme 9 Pro",
quantity: 0,
},
];
let [products, setState] = useState(productList);
// console.log(useState(productList));
const incrementQty = (inx) => {
let newProductList = [...products];
newProductList[inx].quantity++;
console.log(newProductList);
setState(newProductList);
};
const decrementQty = (inx) => {
let newProductList = [...products];
newProductList[inx].quantity > 0
? newProductList[inx].quantity--
: (newProductList[inx].quantity = 0);
setState(newProductList);
};
return (
<React.StrictMode>
<Navbar />
<div className="mt-5">
<ProductList
productList={productList}
incrementQty={incrementQty}
decrementQty={decrementQty}
/>
</div>
<Footer />
</React.StrictMode>
);
}
export default App;
I pass the function and the productList array to my ProductList component.
This is the ProductList.js file.
import React from "react";
import Product from "./Product";
export default function ProductList(props) {
return props.productList.map((product, i) => {
return (
<Product
product={product}
key={i}
incrementQty={props.incrementQty}
decrementQty={props.decrementQty}
index={i}
></Product>
);
});
}
and then I pass the function and product index to Product component which will render out my products. This is the Product.js file
import React from "react";
export default function Product(props) {
return (
<div className="container">
<div className="row">
<div className="col-lg-4 col-sm-12 col-md-6">
<h2>
{props.product.name}
<span className="badge bg-success">₹ {props.product.price}</span>
</h2>
</div>
<div className="col-lg-4 col-md-6 col-sm-12">
<div className="btn-group" role="group" aria-label="Basic example">
<button
type="button"
className="btn btn-success"
onClick={() => {
props.incrementQty(props.index);
}}
>
+
</button>
<button type="button" className="btn btn-warning">
{props.product.quantity}
</button>
<button
type="submit"
className="btn btn-danger"
onClick={() => {
props.decrementQty(props.index);
}}
>
-
</button>
</div>
</div>
<div className="col-lg-4 col-sm-12 col-md-6">
<p>{props.product.quantity * props.product.price}</p>
</div>
</div>
</div>
);
}
As you can see, I am using onClick event listener for my increase and decrease buttons. When I click the button, the quantity is updated, but that's not reflected in my DOM.
How can I fix this?
In addition to the other suggestions which should fix your issues (ie: using productList={products}), you're also currently modifying your state directly, which down the line can lead to other UI problems.
When you do:
let newProductList = [...products];
you're creating a shallow copy of the products array, but the objects within that array are still the original references to your state and are not copies. As a result, you need to create a new object within your new array when you want to update it to avoid rerender issues. One possible way is to use:
const newProductList = [...products];
const toUpdate = newProductList[inx];
newProductList[inx] = {...toUpdate, quantity: toUpdate.quantity+1};
setState(newProductList);
The same applies to your decrement logic. Here I've provided a slightly different example where I'm using the state setter function, but you can also use the above approach. I've created a copy of the state in the arguments and then used Math.max() instead of the ternary:
setState(([...products]) => {
const toUpdate = newProductList[inx];
products[inx] = {...toUpdate, quantity: Math.max(0, toUpdate.quantity-1)
return products;
});
Always treat your state as read-only/immutable to avoid unexpected bugs with your UI. Other ways of achieving the above is to use .map() to create a new array and then replace your object when your map reaches the object that matches your inx value.
In you App.js, change
productList={productList}
to
productList={product}
Just noticed the actual problem. You are passing down the default value for the state and not the state's value.
Related
I am unable to trigger the component to update when the viaList state is changed. If anyone could help, please <3
I don't want to append these children if possible.
import React, { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
function BookingForm() {
const [viaList, setViaList] = useState([]);
function addViaLocation() {
const arr = viaList;
arr.push(
<li key={uuidv4()}>
<label className="form-label">
Via Location
</label>
</li>)
setViaList(arr);
}
return (
<>
<button onClick={addViaLocation} className="button-standard primary" type="button">
Add Additional Location
</button>
<ul>{viaList.length > 0 && viaList.map(item => item)}</ul>
</>
);
}
export default BookingForm;
Separating the functionality into a separate component
Triggering the update using props
Regular DOM stuff (appending doesn't fit our use case)
You need to create a new array such that React understands it has changed. If you only push to the old viaList it will still hold that viaList === previousViaList. Instead create a new array, e.g. with the spread operator:
function addViaLocation() {
const newElement = (
<li key={uuidv4()}>
<label className="form-label">
Via Location
</label>
</li>)
setViaList([...viaList, newElement]);
}
Fixed it...
I was doing the following (just longer):
setViaList([...viaList, <li>STUFF</li>])
I had to update the state array using a callback instead:
setViaList(viaList => [...viaList, <li>STUFF</li>])
import React, { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
function BookingForm() {
const [viaList, setViaList] = useState([]);
function addViaLocation() {
const arr = [...viaList];
arr.push(
<li key={uuidv4()}>
<label className="form-label">
Via Location
</label>
</li>)
setViaList(arr);
}
return (
<>
<button onClick={addViaLocation} className="button-standard primary" type="button">
Add Additional Location
</button>
<ul>{viaList.length > 0 && viaList.map(item => item)}</ul>
</>
);
}
export default BookingForm;
You can Create a shallow Copy of the Array by spreading the current state array and then pushing the new data and update state with the shallow copy
I'm currently working on react state updates.
The data that I'm trying to update is a certain key-value pair in an array of objects.
When I click the thumbs-up button, the value in the 'likes' key must increase.
The problem I am facing is that the likes button will increase by 2 from the second click.
How should I handle this problem?
The code I was working on is:
import React, {useState} from 'react';
import './App.css';
function App() {
const [data, setData] = useState([{id: 0, title: '남자 코트 추천', date: '2월 17일', likes: 0}, {id: 1, title: '여자 코트 추천', date:'2월 18일', likes:0}, {id: 2, title: '강남 맛집 추천', date:'2월 19일', likes:0},]);
const handleLikesClick = (event) => {
event.stopPropagation()
setData((prevState)=>{
const updateData = [...prevState]
const id = parseInt(event.target.getAttribute("value"))
updateData[id].likes += 1;
return updateData
})
}
return (
<div className="App">
<div className="black-nav">
<div>개발 Blog</div>
</div>
{data.map(post => (
<div className="list" key={post.id}>
<h3>{post.title} <span onClick={handleLikesClick} value={post.id}>👍</span>{post.likes}</h3>
<p>{post.date}</p>
<hr/>
</div>
))}
</div>
);
}
export default App;
The problem was that I had Strict Mode on in index.js, which makes my component to render twice.
I omitted Strict Mode in index.js and the problem was solved.
ReactDOM.render(<App />,
document.getElementById('root')
);
I am a few days into learning React JS. It's been very interesting and simplifies JavaScript, at least how I view it. My goal at the moment is to create a button that when clicked will create a new card with a random number generated in the card.
I can't figure out how to add the onClick= functionality for it to work the way I'm envisioning it. At the moment I have the code for the button written on my index.js file. index.js is my main file, it is where my App component is located. I see people use a file named App.js, not sure why, but that's another question for later.
Currently my buttons are stylized the following way.
const App = () => {
return (
<body>
<header>
<div className="ui buttons">
<button className="ui button mb-1 mt-1">
<i className="plus icon"></i>
Add Card
</button>
<div className="or mb-1 mt-1"></div>
<button className="ui positive button mb-1 mt-1">
<i className="sort numeric down icon"></i>
Sort All
</button>
</div>
</header>
They are located in a header container at the top of my page. It looks like this following image.
I want to be able to click the Add Card button and then for it to create a new card in my card container. As picture below.
The current card you see in the above image is hard coded at the moment. I created a MainCard.js component that houses the code for the card including the functionality of the randomly generated number and the styling.
import "bootstrap/dist/css/bootstrap.css";
import React from "react";
import { Card, Button } from "react-bootstrap";
let getRandomNumber = function (min, max) {
let getRandom = Math.floor(Math.random() * max + min);
return getRandom;
};
const MainCard = () => {
return (
<Card>
<Button
className="ui mini red basic icon button"
style={{
position: "absolute",
top: "0",
right: "0",
}}
>
<i
className="red x icon"
style={{
position: "relative",
top: "0",
right: "0",
}}
></i>
</Button>
<Card.Body>{getRandomNumber(0, 101)}</Card.Body>
</Card>
);
};
export default MainCard;
Previously I was doing the onClick functionality in my AddCard.js but for some reason the Alert I created was both being generated when the page was being reloaded as well as when the button was clicked. I couldn't figure out the reasoning for this and it has become a wall that I haven't been able to climb over yet. I'm pretty sure I'm missing something simple, but I just can't figure out what that is. Any ideas how I can achieve this?
You need to use useState().
For example.
import { useState } from "react";
export default function App() {
const [{items}, setItems] = useState({ items: [] });
const addItem = () => {
items.push(<div key={items.length}>Hello</div>);
setItems({ items: [...items] });
};
return (
<div>
<button onClick={addItem} />
{items}
</div>
);
}
This creates a persistant storage that you can set values into that remain between component rebuilds. The object in storage has a single key "items" with an array containing the items. It is convenient to put your state in an object with a key that describes the contents. This allows you to tell what state object you are looking at if you need to debug your app.
The addItem() callback will add a new item into the object in state. Importantly, the item has a key attribute that should be unique. This helps react monitor the state of elements in an array.
You can see a demo of the above code here
As stated on Charles Bamford's response, you'll have to use the useState method. The useState method creates a variable and a method to set the value of this variable. Everytime you change a state (using it's setter) React will check the code and re-render everywhere it was used. You will need something like this:
const App = () => {
const [cards, setCards] = useState([]); // instantiate cards as a empty Array
const addCard = () => {
// create a new array with the old values and the new random one
const newCards = [...cards, Math.floor(Math.random() * 100)];
setCards(newCards);
};
const removeCard = (cardIndex) => {
// create a new array without the item that you are removing
const newCards = cards.filter((card, index) => index !== cardIndex);
setCards(newCards);
};
return (
<body>
<header>
<div className="ui buttons">
<button className="ui button mb-1 mt-1" onClick={() => addCard()}>
<i className="plus icon"></i>
Add Card
</button>
<div className="or mb-1 mt-1"></div>
<button
className="ui positive button mb-1 mt-1"
onClick={() => addCard()}
>
<i className="sort numeric down icon"></i>
Sort All
</button>
</div>
</header>
<main>
{cards.map((cardNumber, index) => (
<MainCard number={cardNumber} onRemove={() => removeCard(index)} />
))}
</main>
</body>
);
};
With an array like this [10, 15, 33], the cards.map will do something like this for you:
<main>
<MainCard number={cards[0]} onRemove={() => removeCard(0)} /> // 10
<MainCard number={cards[1]} onRemove={() => removeCard(1)} /> // 15
<MainCard number={cards[2]} onRemove={() => removeCard(2)} /> // 33
</main>
We are passing the "number" and the "onRemove" function from the App component to the MainCard component. Now we just have to get it from our props, like this:
const MainCard = (props) => {
const { onRemove, number } = props
return (
<Card>
<Button
className="ui mini red basic icon button"
style={{
position: "absolute",
top: "0",
right: "0",
}}
onClick={() => onRemove()}
>
<i
className="red x icon"
style={{
position: "relative",
top: "0",
right: "0",
}}
></i>
</Button>
<Card.Body>{number}</Card.Body>
</Card>
);
};
export default MainCard;
I'm working on a flashcard App, my flashcard component is as below:
import React, { useState } from 'react';
import './Flashcard.css'
import {
Button,
} from '#material-ui/core';
function Flashcard({ data }) {
// // Get an initial card
const [currentCard, setCurrentCard ] = useState(data[0]);
// Create state to track whether the card has been flipped yet
const [flipped, setFlipped] = useState(false);
// When the Flip button is clicked, we display the verso
const handleFlipClick = () => {
setFlipped(true);
}
const handleNextClick = () => {
// Change the current active card
setCurrentCard(data[1]);
// Restore the 'flipped' state to indicate that the new active card hasn't been flipped yet
setFlipped(false);
}
// Get data needed from the server
return (
<div className="flashcard__container">
{/* Card content goes here */}
<div className="flashcard__content">
<p>
{ (flipped) ? currentCard.verso : currentCard.recto }
</p>
</div>
<div className="flashcard__actions">
{/* Display the flip button if the card hasn't been flipped yet */}
{(!flipped) ?
<Button
variant="contained"
color="primary"
onClick={handleFlipClick}
>Flip</Button>
:
<>
<Button onClick={handleNextClick} color='primary' variant='contained'>
Hard
</Button>
<Button onClick={handleNextClick} color='primary' variant='contained'>
Slightly difficult
</Button>
<Button onClick={handleNextClick} color='primary' variant='contained'>
Good
</Button>
<Button onClick={handleNextClick} color='primary' variant='contained'>
Easy
</Button>
</>
}
</div>
</div>
)
}
export default Flashcard
'data' is pass from another component, when I console log it in the Flashcard component, here is what I get:
[
0: {id: "PynaJl43Jphl2xI8fWkn", reviewedOn: Array(2), recto: "To go", verso: "行く"}
1: {id: "mSDdg5ATenvjBYojfy8N", verso: "こんにちは", recto: "Hi", reviewedOn: Array(2)
]
However, this line: (flipped) ? currentCard.verso : currentCard.recto gives me the following error message: "Type Error - Cannot read property 'recto' of undefined".
Indeed, when I try to console log 'currentCard', it says it is undefined. Why is that happening? I don't see any mistake in the way I initialized the state. Thank you!
Here is the parent component:
import React, { useState, useEffect } from 'react';
import FlashCard from './Flashcard';
import mockData from './mockData';
import { getParams, useParams } from 'react-router-dom';
import { db } from './firebase';
import Header from './Header';
function Deck() {
const { id } = useParams();
const [cards, setCards] = useState(); // Create a state to store the deck
// When the page is first loaded: Import the deck's card from the database
useEffect(() => {
db.collection(`decks/${id}/cards`)
// Return an array of cards
.onSnapshot((snapshot) => {
setCards(snapshot.docs.map((doc) => {
return {
id: doc.id,
...doc.data()
}
}))
})
}, [])
return (
<div>
<Header />
{cards && <FlashCard data={cards} />}
</div>
)
}
export default Deck
data is likely undefined when this component is first rendered. You can fix this in one of two ways.
1.) Add logic to the parent component so that Flashcard is not rendered until data is defined.
{data && <Flashcard data={data} />}
2.) Add a useEffect that monitors for changes to data and updates state.
React.useEffect(() => {
if(typeof currentCard === 'undefined'){
setCurrentCard(data[0])
}
}, [data])
Maybe you are passing some faulty data into the props. Try my sample, it works:
import React, { useState } from "react";
import "./styles.css";
// import { FlashCard } from "./FlashCard";
import { Button } from "#material-ui/core";
function Flashcard({ data }) {
// // Get an initial card
// // Get an initial card
const [currentCard, setCurrentCard] = useState(data[0]);
// Create state to track whether the card has been flipped yet
const [flipped, setFlipped] = useState(false);
// When the Flip button is clicked, we display the verso
const handleFlipClick = () => {
setFlipped(true);
};
const handleNextClick = () => {
// Change the current active card
setCurrentCard(data[1]);
// Restore the 'flipped' state to indicate that the new active card hasn't been flipped yet
setFlipped(false);
};
// Get data needed from the server
return (
<div className="flashcard__container">
{/* Card content goes here */}
<div className="flashcard__content">
<p>{flipped ? currentCard.verso : currentCard.recto}</p>
</div>
<div className="flashcard__actions">
{/* Display the flip button if the card hasn't been flipped yet */}
{!flipped ? (
<Button variant="contained" color="primary" onClick={handleFlipClick}>
Flip
</Button>
) : (
<>
<Button
onClick={handleNextClick}
color="primary"
variant="contained"
>
Hard
</Button>
<Button
onClick={handleNextClick}
color="primary"
variant="contained"
>
Slightly difficult
</Button>
<Button
onClick={handleNextClick}
color="primary"
variant="contained"
>
Good
</Button>
<Button
onClick={handleNextClick}
color="primary"
variant="contained"
>
Easy
</Button>
</>
)}
</div>
</div>
);
}
export default function App() {
return (
<div className="App">
<Flashcard
data={[
{
id: "PynaJl43Jphl2xI8fWkn",
reviewedOn: Array(2),
recto: "To go",
verso: "行く"
},
{
id: "mSDdg5ATenvjBYojfy8N",
verso: "こんにちは",
recto: "Hi",
reviewedOn: Array(2)
}
]}
></Flashcard>
</div>
);
}
I recreated this in a CodeSandbox and it worked just fine. My guess is you have an error on the props name when passing props to the Flashcard component?
It should be rendered as seen below since in Flashcard you are destructuring data from props:
<Flashcard data = {insertArrayHere} />
I would suggest you using useEffect and adding data as a dependency. Then in useEffect check, if the data exists it should change the state. You can make it this way:
useEffect(() => {
if(data){
setCurrentCard(data[0]);
}
}, [data])
or you can check existing of this prop in the parent element, adn if it exists show it that time. You can implement it in this way:
{ data ? <Flashcard data={data} /> : null }
Some time ago I tried to make my first steps in React. Now I'm trying to create simple ToDo list app. One of my colleagues told me that it will be nice to use PropTypes with shape and arrayOf. I have one component which pass data to another and here is my issue(List.js and ListItem.js). I'm not sure when, where and how to use propTypes with shape and arrayOf. Which approach is right? Define PropTypes in parent component or in child component? Maybe there is a better idea? Please find my code below, it will be nice if you can clarify me how to use PropTypes. I tried to search something in Web, but I still don't get it.
Main App.js file:
import React from "react";
import "./styles/App.scss";
import List from "./components/List/List";
import AppHeader from "./components/AppHeader/AppHeader";
function App() {
return (
<div className="App">
<AppHeader content="TODO List"></AppHeader>
<List></List>
</div>
);
}
export default App;
AppHeader.js file:
import React from "react";
import "./AppHeader.scss";
export default function AppHeader(props) {
return (
<div className="header">
<h1 className="header-headline">{props.content}</h1>
</div>
);
}
List.js file:
import React from "react";
import "./List.scss";
import ListItem from "../ListItem/ListItem";
import _ from "lodash";
const initialList = [
{ id: "a", name: "Water plants", status: "done" },
{ id: "b", name: "Buy something to eat", status: "in-progress" },
{ id: "c", name: "Book flight", status: "in-preparation" }
];
const inputId = _.uniqueId("form-input-");
// function component
const List = () => {
const [value, setValue] = React.useState(""); // Hook
const [list, setList] = React.useState(initialList); // Hook
const handleChange = event => {
setValue(event.target.value);
};
// add element to the list
const handleSubmit = event => {
// prevent to add empty list elements
if (value) {
setList(
list.concat({ id: Date.now(), name: value, status: "in-preparation" })
);
}
// clear input value after added new element to the list
setValue("");
event.preventDefault();
};
// remove current element from the list
const removeListItem = id => {
setList(list.filter(item => item.id !== id));
};
// adding status to current element of the list
const setListItemStatus = (id, status) => {
setList(
list.map(item => (item.id === id ? { ...item, status: status } : item))
);
};
return (
<div className="to-do-list-wrapper">
<form className="to-do-form" onSubmit={handleSubmit}>
<label htmlFor={inputId} className="to-do-form-label">
Type item name:
</label>
<input
type="text"
value={value}
onChange={handleChange}
className="to-do-form-input"
id={inputId}
/>
<button type="submit" className="button to-do-form-button">
Add Item
</button>
</form>
<ul className="to-do-list">
{list.map(item => (
<ListItem
name={item.name}
status={item.status}
key={item.id}
id={item.id}
setListItemStatus={setListItemStatus}
removeListItem={removeListItem}
></ListItem>
))}
</ul>
</div>
);
};
export default List;
And ListItem.js file:
import React from "react";
import PropTypes from "prop-types";
import "./ListItem.scss";
const ListItem = props => {
return (
<li className={"list-item " + props.status} key={props.id}>
<span className="list-item-icon"></span>
<div className="list-item-content-wrapper">
<span className="list-item-text">{props.name}</span>
<div className="list-item-button-wrapper">
<button
type="button"
onClick={() => props.setListItemStatus(props.id, "in-preparation")}
className="button list-item-button"
>
In preparation
</button>
<button
type="button"
onClick={() => props.setListItemStatus(props.id, "in-progress")}
className="button list-item-button"
>
In progress
</button>
<button
type="button"
onClick={() => props.setListItemStatus(props.id, "done")}
className="button list-item-button"
>
Done
</button>
<button
type="button"
onClick={() => props.removeListItem(props.id)}
className="button list-item-button"
>
Remove
</button>
</div>
</div>
</li>
);
};
ListItem.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
status: PropTypes.string,
removeListItem: PropTypes.func,
setListItemStatus: PropTypes.func
};
export default ListItem;
Props are per component.
The purpose of prop types is to let you make some type checking on a component's props.
It seems like you did it correctly in the ListItem component.
Basically, you just list all of the component's props and what type they should be.
Like you did here:
ListItem.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
status: PropTypes.string,
removeListItem: PropTypes.func,
setListItemStatus: PropTypes.func
};
I am not exactly sure where will the propTypes shape and arrayOf be used, but generally, PropTypes are useful when you are trying to render a child component within a parent component, and you want to carry out TypeChecking on the props of the relative child component.
In your scenario, arrayOf and shape can be used in one of the props within your component whereby the prop is an array of a certain type (arrayOf), and an object of a certain type(shape, or exact).