getting the most recent value in a react component - javascript

so I am trying to create add to faviourate button with icon.
so far I could make a logic if a user clicked the empty heart icon that it turns to be full heart icon and I was able to locate the item it was clicked on.
So far so good, my issue starts when products object recieves only the most recent item that is picked and loses the other items that are previously picked.
so for example If I want to click on 3 items to add them to faviourate, I see that the console.log(favProduct) only preserves the most recent item which is in my case number 3 and loses number 1 and 2.
My Question is How to get all Items I clicked on and not only the most recent one.
Edit This is where I get the product from, check the code below.
import React , { useState, useEffect } from 'react'
import { Row , Col } from 'react-bootstrap'
import Product from '../components/Product'
import axios from 'axios'
const HomeScreen = () =>{
const [products , setProducts] = useState([])
useEffect(()=>{
let componentMounted = true
const fetchProducts = async () =>{
const {data} = await axios.get('http://172.30.246.130:5000/api/products')
if( componentMounted){
setProducts(data)
}
}
fetchProducts()
return () =>{
componentMounted = false
}
},[])
console.log('products' , products)
return(
<>
<h2 className='my-3'>Latest Products</h2>
<Row>
{
products.map((product)=>(
<Col key={product._id} sm={12} md={6} lg={4} xl={3}>
<Product product={product} rating = {product.rating} reviews={product.numReviews}/>
</Col>
))
}
</Row>
</>
)
}
export default HomeScreen
import React, { useState } from 'react'
const Fav = ({products}) => {
let [checked , setChecked] = useState(false)
let[favProduct ,setFavProduct] = useState([])
const toggle = ()=>{
(!checked) ? setChecked(true) : setChecked(false)
setFavProduct([...favProduct,products]) // problem is here
}
console.log(favProduct)
return (
<>
<span onClick={toggle}>
{
<i className={(checked) ? "fas fa-heart" : "far fa-heart"}></i>
}
</span>
</>
)
}
export default Fav

At the moment each of your Fav components looks like they're trying to manage the state for all of the favourites which is not a good idea.
The general idea is to make most UI components as dumb as possible (ie. just return the bare minimum given the props they're given). They can control their own state but usually you want to a parent to control the state by lifting state up, and have them pass down a handler that the dumb component can call when their listener is triggered.
In this example Fav accepts an id, a favoured, and a handleClick listener, and then just returns some JSX.
The parent component does all the state management.
const { useEffect, useState } = React;
// Accept some props
// Render the class based on the `favoured` prop
function Fav({ id, favoured, handleClick }) {
return (
<div className="icon">
<i
data-id={id}
className={favoured ? 'fa-solid fa-heart' : 'fa-regular fa-heart'}
onClick={handleClick}
> {id}</i>
</div>
);
}
function Example() {
// The parent component manages the state
const [ favourites, setFavourites ] = useState([]);
// When a favourite icon is clicked, get its id
// and if it's in the state, remove it, otherwise add it
function handleClick(e) {
const { id } = e.target.dataset;
const found = favourites.includes(id);
if (found) {
setFavourites(favourites.filter(fav => fav !== id));
} else {
setFavourites([...favourites, id]);
}
}
// In this example I'm using a loop to generate the
// the favourites, checking if the state includes the id
// (I have to coerce it to a string because that's what
// the data attributes return, and `i` will always be a number
// Pass down the handler that the favourite button
// will use to update the state
function getFavs() {
const favs = [];
for (let i = 0; i < 5; i++) {
const isFavoured = favourites.includes(i.toString());
const fav = (
<Fav
id={i}
favoured={isFavoured}
handleClick={handleClick}
/>
);
favs.push(fav);
}
return favs;
}
return (
<div>
{getFavs()}
</div>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta2/css/all.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Related

How to make element scrolled to bottom in React.js?

In my app I want to make my element always scrolled to bottom after getting new logs.
For some reason my logsRef.current.scrollTop has value of zero all the time. My logs do show on screen and in console. I am not sure why is this not working, I've tried to use different approaches using useLyaoutEffect() but nothing made logsRef.current.scrollTop value change, it stayed zero all the time.
//my Logs.jsx component
import { useEffect, useRef } from "react";
import Container from "./UI/Container";
import styles from "./Logs.module.css";
const Logs = ({ logs }) => {
const logsRef = useRef(null);
useEffect(() => {
logsRef.current.scrollTop = logsRef.current.scrollHeight;
console.log(logs);
console.log(logsRef.current.scrollTop);
}, [logs]);
return (
<Container className={`${styles.logs} ${styles.container}`}>
<div ref={logsRef}>
{" "}
{logs.map((log, index) => (
<p key={index}>{log}</p>
))}
</div>
</Container>
);
};
export default Logs;
Also, I do render my Logs.jsx in BattlePhase.jsx component where I do my attack logic on click and I save logs using useState() hook.
//parts where i do save my logs in BattlePhase.jsx
const [logs, setLogs] = useState([]);
const attackHandler = () => {
//logs where pokemon on left attacked pokemon on right
setLogs((prevLogs) => [
...prevLogs,
`${pokemonDataOne.name} attacked ${
pokemonDataTwo.name
} for ${attack.toFixed(2)} dmg`,
`${pokemonDataTwo.name} died`,
])
}
...
<Attack className={isActiveArrow}>
<Button onClick={attackHandler}>Attack!</Button>
</Attack>
Slight long shot but it's possible that the ref is attached to the wrong element. Are you sure the element with the CSS property that makes it scrollable (overflow) isn't on <Container>?
//my Logs.jsx component
import { useLayoutEffect, useRef } from "react";
import Container from "./UI/Container";
import styles from "./Logs.module.css";
const Logs = ({ logs }) => {
const logsRef = useRef(null);
useLayoutEffect(() => {
logsRef.current.scrollTop = logsRef.current.scrollHeight;
console.log(logs);
console.log(logsRef.current.scrollTop);
}, [logs]);
return (
<Container className={`${styles.logs} ${styles.container}`} ref={logsRef}>
<div>
{" "}
{logs.map((log, index) => (
<p key={index}>{log}</p>
))}
</div>
</Container>
);
};
export default Logs;
Also to confirm, you do need useLayoutEffect here.

React - toggle text and class in an HTML element?

I am trying to create a system where I can easily click a given sentence on the page and have it toggle to a different sentence with a different color upon click. I am new to react native and trying to figure out the best way to handle it. So far I have been able to get a toggle working but having trouble figuring out how to change the class as everything is getting handled within a single div.
const ButtonExample = () => {
const [status, setStatus] = useState(false);
return (
<div className="textline" onClick={() => setStatus(!status)}>
{`${status ? 'state 1' : 'state 2'}`}
</div>
);
};
How can I make state 1 and state 2 into separate return statements that return separate texts + classes but toggle back and forth?
you can just create a component for it, create a state to track of toggle state and receive style of text as prop
in React code sandbox : https://codesandbox.io/s/confident-rain-e4zyd?file=/src/App.js
import React, { useState } from "react";
import "./styles.css";
export default function ToggleText({ text1, text2, className1, className2 }) {
const [state, toggle] = useState(true);
const className = `initial-style ${state ? className1 : className2}`;
return (
<div className={className} onClick={() => toggle(!state)}>
{state ? text1 : text2}
</div>
);
}
in React-Native codesandbox : https://codesandbox.io/s/eloquent-cerf-k3eb0?file=/src/ToggleText.js:0-465
import React, { useState } from "react";
import { Text, View } from "react-native";
import styles from "./style";
export default function ToggleText({ text1, text2, style1, style2 }) {
const [state, toggle] = useState(true);
return (
<View style={styles.container}>
<Text
style={[styles.initialTextStyle, state ? style1 : style2]}
onPress={() => toggle(!state)}
>
{state ? text1 : text2}
</Text>
</View>
);
}
This should be something you're looking for:
import React from "react"
const Sentence = ({ className, displayValue, setStatus }) => {
return (
<div
className={className}
onClick={() => setStatus((prevState) => !prevState)}
>
{displayValue}
</div>
);
};
const ButtonExample = () => {
const [status, setStatus] = React.useState(false);
return status ? (
<Sentence
className="textLine"
displayValue="state 1"
setStatus={setStatus}
/>
) : (
<Sentence
className="textLineTwo"
displayValue="state 2"
setStatus={setStatus}
/>
);
};
You have a Sentence component that takes in three props. One for a different className, one for a different value to be displayed and each will need access to the function that will be changing the status state. Each setter from a hook also has access to a function call, where you can get the previous (current) state value, so you don't need to pass in the current state value.
Sandbox

React - setState in child through props

I'm relatively new to React and am trying to set the state of an object inside the child component(SportTile) for the onclick event. I'm comfortable updating the state of a single variable by using individual useState() hooks but, that is a cumbersome task when you have a lot of variables. So I used an object named : isClicked which stores a boolean value for various sports on whether they are selected by the user or not.
The functionality that I'm looking for is, whenever a SportTile gets clicked on, its isClicked["sportname"] value is set to true and the rest are set to false. Only one sport can be true at once.
Upon console logging the isClicked object in the onclick() function, I got the desired values but they weren't updated in the Parent component's(Booking) h1 tag
import React from 'react';
import SportTile from '../SportTile';
import './booking.css';
import { useState } from 'react';
import SportsSoccerRoundedIcon from '#mui/icons-material/SportsSoccerRounded';
import SportsBasketballRoundedIcon from '#mui/icons-material/SportsBasketballRounded';
import SportsVolleyballIcon from '#mui/icons-material/SportsVolleyball';
import SportsTennisIcon from '#mui/icons-material/SportsTennis';
const Booking = () => {
const [isClicked, setIsClicked] = useState({
Football: false,
Basketball: false,
Volleyball: false,
Tennis: false,
});
return (
<div className='booking'>
<div className='booking__body'>
<div className='booking__left'>
<h1>SELECT SPORT</h1>
<SportTile
sportname={'Football'}
icon={<SportsSoccerRoundedIcon />}
clicked={setIsClicked}
isClicked={isClicked}
// test={setTestclicked}
// istestClicked={istestClicked}
/>
<SportTile
sportname={'Basketball'}
icon={<SportsBasketballRoundedIcon />}
clicked={setIsClicked}
isClicked={isClicked}
/>
<SportTile
sportname={'Volleyball'}
icon={<SportsVolleyballIcon />}
clicked={setIsClicked}
isClicked={isClicked}
/>
<SportTile
sportname={'Tennis'}
icon={<SportsTennisIcon />}
clicked={setIsClicked}
isClicked={isClicked}
/>
<h1>Football : {isClicked.Football.toString()} </h1>
<h1>Basketball : {isClicked.Basketball.toString()} </h1>
<h1>Volleyball : {isClicked.Volleyball.toString()} </h1>
<h1>Tennis : {isClicked.Tennis.toString()} </h1>
</div>
<div className='booking__right'>Right Side</div>
</div>
</div>
);
};
export default Booking;
import { Button } from '#mui/material';
import React from 'react';
import './sportTile.css';
const SportTile = ({
sportname,
icon,
clicked,
isClicked,
}) => {
function onclick() {
Object.keys(isClicked).map((key) => {
if (key == sportname) {
isClicked[key] = true;
} else {
isClicked[key] = false;
}
});
clicked(isClicked);
console.log(isClicked);
}
return (
<Button className='sportname__button' onClick={onclick}>
<div className='sportname__buttonColumn'>
{icon}
{sportname}
</div>
</Button>
);
};
export default SportTile;
Maybe I'm missing the obvious but would appreciate anyone who could point me in the right direction
You should never pass the original setState method.
create a new method in the Booking component:
const setIsClickedWrapper = (sportKey) = {
setIsClicked((isClicked) => Object.keys(isClicked).map((key) => sportKey === key)
}
and in the SportTile component just call:
const onclick = () => { setIsClickedWrapper(sportname) }
but I think it will be better if isClicked will be just a string of the current clicked sport key and then it's enough:
const setIsClickedWrapper = (sportKey) = {
setIsClicked(sportKey)
}

How to call child component's method from a parent component in React

I have a Grid with 3*3 squares.
When a click on a square , we change the background color to green.
So, I tried to put the all the states in the parent GridContainer.
state = {
gridCells: []
};
This will hold the indices that are clicked.
GridContainer nests Grid and Grid nests Square.
render() {
return (
<div>
<Grid action={this.handler} />
<button>Reset Clicks</button>
</div>
);
}
Here is my current implementation.
Now how do I clear the background cells when I reset clicks and make the background back to white again?
function Square(props) {
const liClickHandler = event => {
event.target.classList.add("highlight");
props.clickAction();
};
return <li onClick={e => liClickHandler(e)} />;
}
function Grid(props) {
const gridHandler = index => {
props.action(index);
};
return (
<ul>
{Array.from(new Array(9)).map((item, index) => (
<Square key={index} clickAction={() => gridHandler(index)} />
))}
</ul>
);
}
class GridContainer extends React.Component {
state = {
gridCells: []
};
handler = index => {
let temp = [...this.state.gridCells];
temp.push(index + 1);
this.setState({
gridCells: temp
});
};
render() {
return (
<div>
<Grid action={this.handler} />
<button>Reset Clicks</button>
</div>
);
}
}
So when I click a Sqaure , it calls a method clickAction that calls handler
that updates the state and we have an array which indices were clicked in order.
How do I implement Reset clicks that updates the background of those Sqaures back to white ? How do I let know my child know.
Am I maintaining the state wrong?
Sandbox link : https://codesandbox.io/s/3-x-3-grids-s0b43?file=/src/index.js:640-1563
I'd advise to rethink the way how your components are structured.
Each component should be independent unit with it's own logic and state (if needed of course). I'm saying if needed for state, cause ideally components should be stateless.
There are several problems with Square class:
It adds class via event.target, which is not react way to go. React works with virtual DOM and has it's own set of methods to interact with html. Working with DOM directly - will bite your later, starting from writing tests for your code in the future.
It does not contain incoming information whether it should be highlighted or not
Both these problems result in fact that you cannot reset presentation of your squares easily.
I've updated your sample: https://codesandbox.io/s/3-x-3-grids-uflhr?file=/src/index.js
It's still not ideal, but you can notice that gridCells is passed from top via props. And then each square gets own props param. This allows state to come through the flow and let squares rerender with updated class.
In react you should think the "react" way:
pass the necessary state down through the props
pass down the callbacks so that children can update the parent state
Here is corrected version of the demo:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function Square(props) {
return (
<li onClick={props.onClick} className={props.active ? "highlight" : ""} />
);
}
function Grid(props) {
let squares = [];
for (let i = 0; i < 9; i++) {
squares.push(
<Square
key={i}
onClick={() => props.onCellClick(i)}
active={props.cells[i]}
/>
);
}
return <ul>{squares}</ul>;
}
class GridContainer extends React.Component {
state = {
gridCells: []
};
onCellClick = index => {
this.setState(prevState => {
const newCells = [...prevState.gridCells];
newCells[index] = true;
return {
gridCells: newCells
};
});
};
render() {
return (
<div>
<Grid cells={this.state.gridCells} onCellClick={this.onCellClick} />
<button
onClick={() => {
let that = this; //we could bind the callback aswell
that.setState(() => ({ gridCells: [] }));
}}
>
Reset Clicks
</button>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<GridContainer />, rootElement);

How can I avoid updating all instances of my function component with the useState hook in React.js?

TL;DR I am making a reusable Button function component. My useState() hook for the button label is updating every Button instance. How can I prevent this?
I am very new to React and building a Book Finder app in order to learn. So far my app has a BookList and a ReadingList. Each BookDetail in either list has a Button to add/remove that book from the ReadingList. The add/remove function works (phew), but using useState to update the Button's label updates every instance of the Button component, and not just the one that was clicked.
Buttons on books in the BookList start with label 'Add to Reading List', but if I click any of them, all of them update to 'Remove from Reading List'.
I've tried moving the logic around into the Button component or either List component but I just end up breaking the function.
App.js
function App() {
const books = useState([])
const [booksToRead, setBooksToRead] = useState([])
const [addRemove, setAddRemove] = useState(true)
const [label, setLabel] = useState('Add to Reading List')
function handleAddBook(book) {
const newID = book.title_id
if( (typeof booksToRead.find(x => x.title_id === newID)) == 'undefined' ) {
setBooksToRead([...booksToRead, book])
}
}
function handleRemoveBook(book) {
console.log(book)
const array = booksToRead
const index = array.indexOf(book)
const newArray = [...array.slice(0, index), ...array.slice(index +1)]
setBooksToRead(newArray)
}
function addOrRemove(book) {
if( addRemove ) {
handleAddBook(book)
setLabel('Remove from Reading List')
setAddRemove(false)
} else {
handleRemoveBook(book)
setLabel('Add to Reading List')
setAddRemove(true)
}
}
return (
<main>
<BookList books={books} handleAddBook={handleAddBook} addOrRemove={addOrRemove} label={label} />
<ReadingList booksToRead={booksToRead} handleRemoveBook={handleRemoveBook} />
</main>
);
}
export default App;
BookList.js
function BookList ({ book, label, handleAddBook, addOrRemove }) {
return (
<div className="booklist">
{BookData.map((book, index) => {
const onAddBook = () => addOrRemove(book)
return (
<div key={index} className="card">
<BookDetail key={book.title_id} book={book} />
<Button key={index + 'btn'} label={label} onClick={onAddBook} />
</div>
)
})}
</div>
)
}
export default BookList
And finally, Button.js
export default function Button({ styleClass, label, onClick }) {
return (
<button className='btn' onClick={(event) => onClick(event)}>
{label}
</button>
)
}
Unstyled example in codesandbox: https://codesandbox.io/s/cool-rgb-fksrp
Can you make these changes and let me know if there any progress:
<Button label={label} onClick={() => addOrRemove(book)} />
<button className='btn' onClick={onClick}>
It looks like that in button you are passing event instead of book as function parameter
As it is right now, you are declaring a single label and using that same one on all your book entries. This is why they all display the same label. You would need to keep track of the label of each book, for instance by keeping the label as a field in the book object.
For example:
const books = useState([{ label: 'Add to reading list', addRemove: true }])
And then:
function addOrRemove(book) {
if( book.addRemove ) {
handleAddBook(book)
book.label = 'Remove from Reading List'
book.addOrRemove = false
} else {
handleRemoveBook(book)
book.label = 'Add to Reading List'
book.addOrRemove = true
}
}
This way, each book has it's own label.

Categories