I am trying to delete an item (const removeItem) from a list using an onClick event in React.
The state is managed with hooks. I know my way of deleting the item is not the right way yet (i'm putting the name to null), but this is not the issue.
After i set a user to null (i update the users object), i expect a render to happen (useEffect) but it does not. If i switch components and go back to this one, it works, but when clicking the X button, nothing happens in the view.
component Home:
import React, { Suspense, useState, useEffect } from "react";
const Home = props => {
const [users, setUsers] = useState(props.users);
console.log(users);
useEffect(() => {}, [users]);
const addItem = e => {
users.push(e);
console.log(e);
e.preventDefault();
};
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null;
}
});
console.log(users);
setUsers(users);
};
return (
<div className="Home">
<form className="form" id="addUserForm">
<input
type="text"
className="input"
id="addUser"
placeholder="Add user"
/>
<button className="button" onClick={addItem}>
Add Item
</button>
</form>
<ul>
{users.map(item => {
return (
<>
<li key={item.id}>{item.name + " " + item.count}</li>
<button className="button" onClick={() => removeItem(item.id)}>
X
</button>
</>
);
})}
</ul>
</div>
);
};
export default Home;
How i get my users object:
import React from "react";
const LotsOfUsers =[...Array(100).keys()].map((item, key) => item = {
name : `User ${key}`,
id: key,
count: 0
})
export default LotsOfUsers;
main App:
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useRouteMatch,
useParams
} from "react-router-dom";
import Home from "./Home"
import CTX from './store'
import LotsOfUsers from "./LotsOfUsers";
export default function App() {
return (
<CTX.Provider value={{}}>
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/topics">
<Topics />
</Route>
<Route path="/">
<Home users={LotsOfUsers} text="hello world"/>
</Route>
</Switch>
</div>
</Router>
</CTX.Provider>
);
}
function About() {
return <h2>About</h2>;
}
function Topics() {
let match = useRouteMatch();
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>
Props v. State
</Link>
</li>
</ul>
{/* The Topics page has its own <Switch> with more routes
that build on the /topics URL path. You can think of the
2nd <Route> here as an "index" page for all topics, or
the page that is shown when no topic is selected */}
<Switch>
<Route path={`${match.path}/:topicId`}>
<Topic />
</Route>
<Route path={match.path}>
<h3>Please select a topic.</h3>
</Route>
</Switch>
</div>
);
}
function Topic() {
let { topicId } = useParams();
return <h3>Requested topic ID: {topicId}</h3>;
}
Looking into your code, I've noticed 2 times that you change users without the use of setUsers.
const addItem = e => {
users.push(e); // <--- here
console.log(e);
e.preventDefault();
};
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null; // <--- here
}
});
console.log(users);
setUsers(users);
};
In that way, you are updating your users, without letting react know about it. On both cases you have to update users with setUsers and not mutating the array directly.
const addItem = e => {
setUsers(users.concat(e)); // sidenote: if e is your event, then you might be looking for e.target.value here
console.log(e);
e.preventDefault();
};
const removeItem = item => {
setUsers(users.filter(user => user.id !== item));
};
Both .concat() and .filter() use your users array, and return a new array based on the changes you want to apply, and then is used by setUsers to update your users.
So, in extend what I'm actualy doing on removeItem is:
const removeItem = item => {
const newUsers = users.filter(user => user.id !== item); // users remains untouched
setUsers(newUsers);
};
Also, you don't need useEffect hook for this scenario to work properly.
I hope this solves your problem.
You are committing THE Fundamental sin of the react universe. "Mutation"
of state.
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null; // <--- HERE
}
});
console.log(users);
setUsers(users);
};
Check this codesandbox for a working demo of this issue.
https://codesandbox.io/s/jovial-panini-unql4
The react reconciler checks to see whether the 2 objects are equal and since you have just mutated and set the value it registers as the same object and there will be no state change triggered. Hence the view will not be re-rendered and useEffect will not be triggered.
Related
does anyone have experience with React routers?Is there a way to create a list of routes and hold it in a usestate? When i try to do the [... prevCountryRoutes] i get the error that prevCountryRoutes is not iterable
const [countriesWithRouteList,setCountriesWithRouteList]=React.useState(JSON.parse(localStorage.getItem("countriesWithRouteList")) || [])
const [countryRoutes,setCountryRoutes]=React.useState()
function addCountryRoute(co){
if(countriesWithRouteList.filter(el => el == co) == null){
console.log('already route exists')
}else{
console.log('country page being added')
setCountryRoutes((prevCountryRoutes)=>{
const newRoute = <Route
key={nanoid()}
path={`/countrypage/${co.replace(/ /g, '%20')}`}
element={
<CountryPage
country={co}
holidays={holidays}
handleDeleteClick={handleDeleteClick}
handleFormSubmit={handleFormSubmit}
/>
}
/>
return(
[...prevCountryRoutes, newRoute]
)
})
}
setCountriesWithRouteList(prevList => [...prevList, co])
}
The error you are asking about is cause by not declaring an initial countryRoutes state that is iterable. It's undefined.
Anyway, it's a React anti-pattern to store React components and JSX in state. Just store the data and render the derived JSX from state.
I suggest the following refactor:
const [countries, setCountries] = React.useState(JSON.parse(localStorage.getItem("countries")) || []);
function addCountryRoute(co){
if (countries.some(el => el.co === co)){
console.log('already route exists')
} else {
console.log('country page being added')
setCountries((countries) => countries.concat({
id: nanoid(),
co,
}));
}
}
...
{countries.map(country => (
<Route
key={country.id}
path={`/countrypage/${co.replace(/ /g, '%20')}`}
element={
<CountryPage
country={co}
holidays={holidays}
handleDeleteClick={handleDeleteClick}
handleFormSubmit={handleFormSubmit}
/>
}
/>
))}
And instead of mapping a bunch of routes that differ only in the country path segment, render just a single route where the country code is a route path parameter and the CountryPage component uses the useParams hook to get the code.
Example:
<Route
path="/countrypage/:country"
element={
<CountryPage
holidays={holidays}
handleDeleteClick={handleDeleteClick}
handleFormSubmit={handleFormSubmit}
/>
}
/>
CountryPage
const { country } = useParams();
Initialize countryRoutes with an array, so the first time can be iterable.
const [countryRoutes,setCountryRoutes] = React.useState([])
I'm new to React. I'm trying to add additional functionality of deleting the record from the list by setting the value.
here is my App.js
import React, { useState } from "react";
import data from "./data";
import List from "./List";
function App() {
const [movies, setMovie] = useState(data);
return (
<main>
<section className='container'>
<h3>{movies.length} Movies to Watch</h3>
<List movies={movies} setMovie />
<button onClick={() => setMovie([])}>clear all</button>
</section>
</main>
);
}
export default App;
In List.js, Im trying to delete the record when clicking on Watched button. Can I call setMovie inside the List component? is it a correct way?
List.js
import React from "react";
const List = ({ movies }, setMovie) => {
return (
<>
{movies.map((movie) => {
const { id, name, year, image } = movie;
return (
<article key={id} className='person'>
<img src={image} alt={name} />
<div>
<h4>{name}</h4>
<button
className='btn'
onClick={(id) =>
setMovie(movies.filter((movie) => movie.id !== id))
}
>
watched
</button>
<p>{year}</p>
</div>
</article>
);
})}
</>
);
};
export default List;
You have two mistakes in your code. First:
<List movies={movies} setMovie />
This shorthand assigns a value of true to setMovie. To assign the setMovie function to it, you must instead do:
<List movies={movies} setMovie={setMovie} />
And secondly this:
const List = ({ movies }, setMovie) => {
Should be this:
const List = ({ movies, setMovie }) => {
try:
<List movies={movies} setMovie={setMovie} />
this way the funcition will appear in the List component as a prop.
The way you were doing, it will just appear as true
I've created a Breadcrumb component but I'm struggling to add routing to it.
So far, the component is able to load a custom number of nodes based on how many elements we send to an array and it logs into the console the name of the node when clicked.
What it is needed is to make this breadcrumb change the url when a node is clicked.
It should contain something like this:
<Switch>
<Route path="/component0" component={Component0} />
<Route path="/component1" component={Component1} />
...
</Switch>
Is it possible to make it for a custom number of nodes?
This is the code so far:
Parent component:
import React, { useState } from 'react';
import Breadcrumbs from './Breadcrumbs';
export interface BreadcrumbProps {}
export function Breadcrumb(props: BreadcrumbProps) {
const [crumbs, setCrumbs] = useState(['Home', 'Category', 'Sub Category']);
const selected = (crumb: any) => {
console.log(crumb);
};
return (
<div>
<Breadcrumbs crumbs={crumbs} selected={selected} />
</div>
);
}
export default Breadcrumb;
Child component:
function Breadcrumbs(props: any) {
function isLast(index: number) {
return index === props.crumbs.length - 1;
}
return (
<nav className=''>
<ol className=''>
{props.crumbs.map((crumb, ci) => {
const disabled = isLast(ci) ? 'disabled' : '';
return (
<li key={ci} className=''>
<button className={`btn btn-link ${disabled}`} onClick={() => props.selected(crumb)}>
{crumb}
</button>
</li>
);
})}
</ol>
</nav>
);
}
export default Breadcrumbs;
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>
I'm trying to learn how to lift the state from <Child/> to <Parent/> and have the parent control user interactions done the child, (which receives state down as a prop) i.e show color and text.
I was able to lift the state up. However, when I switch back and forth between routes the <Parent/> component is not re-mounting and its state remains exactly how it was previously set by setState({})
const cars = [
{ name: "Ferrari", cost: "$9.000", color: "red", id: 1 },
{ name: "Porsche", cost: "$8.000", color: "black", id: 2 },
***
];
class Dealership extends Component {
state = {
cars,
isShow: {},
correctIndex: Math.floor(Math.random() * (cars.length - 1))
};
handleShuffle = () => {
this.setState({
cars: [...this.state.cars.sort(() => Math.random() - 0.5)],
isShow: {}
});
};
handleShow = car => {
const { isShow } = this.state;
console.log("isShow=", isShow);
this.setState(currentState => ({
isShow: { ...currentState.isShow, [car]: true }
}));
};
render() {
return (
<>
<Navigation />
<Routes
state={this.state}
shuffle={this.handleShuffle}
handleShow={this.handleShow}
// isShow={this.state.isShow}
/>
</>
);
}
}
export default withRouter(Dealership);
As mentioned above, the child <Car/>is receiving state down as props so that its user interaction can be controlled by one source of truth the parent <Dealership />
export default class Car extends Component {
render() {
const { cars, shuffle, isShow, handleShow, correctIndex } = this.props;
const correctCar = cars[correctIndex];
const car = cars.map(car => (
<CarList
// {...this.state}
isShow={isShow[car.name]}
key={car.id}
car={car.name}
guess={car.cost}
isCorrect={correctCar.cost === car.cost}
handleShow={handleShow}
/>
));
return (
<>
<Question key={correctCar.id} guess={correctCar.cost} />
<button
onClick={() => {
shuffle();
}}
>
go again
</button>
<ul className="car-list">{car}</ul>
</>
);
}
}
The <CarList/> is abstracted here:
// CarList.js
export const CarList = ({ isShow, isCorrect, car, handleShow, guess }) => {
function getColor() {
if (isShow) {
const showColor = isCorrect ? "green" : "red";
return showColor;
}
return "";
}
return (
<li onClick={() => handleShow(car)} className={getColor()}>
{car}
<span className={isShow ? "show" : "hide"}>{guess}</span>
</li>
);
};
Oddly (to me), when I switch to a route that holds its own local state i.e <Bike/>, everything works as expected (the state is back to original)
import React, { useState } from "react";
export const Bike = () => {
const [color, setColor] = useState(false);
function ChangeColor() {
setColor(true);
}
return (
<p onClick={ChangeColor}>
Click on the <span className={color ? "red" : " "}>Bike</span>
</p>
);
};
This is how I have my Routes setup:
// Navigation.JS
export const Navigation = () => (
<nav>
<ul>
<li>
<Link to="/">home</Link>
</li>
<li>
<Link to="/car-cost">car</Link>
</li>
<li>
<Link to="/bike">bike</Link>
</li>
</ul>
</nav>
);
// Routes.js
export const Routes = ({ state, shuffle, handleShow, isShow }) => (
<Switch>
<Route
path="/car-cost"
render={() => (
<Car
{...state}
shuffle={shuffle}
handleShow={handleShow}
// isShow={isShow}
/>
)}
/>
<Route path="/bike" render={() => <Bike />} />
<Route path="/" component={Home} />
</Switch>
);
I then wrapped my main app with <BrowserRouter /> as you see in totality plus the current misbehavior happening on this code sandbox
How can I switch between routes having <Car/> behave such as <Bike/>? i.e return to its original state. Also, am I lifting and controlling state correctly here?
Here the state are being saved in parent component. When the route changes then only child components are being remounted. So the state of parent component remains there throughout that routing.
You can keep the state in child component, which would reset the state after every unmount. However if you want to lift the state up and still reset the state, then you would have to do that in parent component.
A better way would be to monitor the route change in the parent component. If the route has changed then parent component should reset its state. In componentDidUpdate method of parent component, you can track the route change and reset the state like this
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
console.log('Route change! Reset the state');
this.setState({ isShow: {}})
}
}