So I'm still learning React and I'm trying to use it to remove an item from a "to do list".
Here is the code:
import { Item } from '../../types/item';
import { useState } from 'react';
type Props = {
item: Item
}
export const ListItem = ({ item }: Props) => {
const [isChecked, setIsChecked] = useState(item.done);
return (
<C.Container done={isChecked}>
<input
type="checkbox"
checked={isChecked}
onChange={e => setIsChecked(e.target.checked)}
/>
<button onClick={removeItem}><img src="./remove"/></button>
<label>{item.name}</label>
</C.Container>
);
}
This button should call the function removeItem that will... remove the item haha.
Any suggestions?
I suggest you to use a functional component with useState hook. Create a state with initial value to be an empty array. then create a function which on click with update the value in state, it canbe done using useState hook. Simply enter setTodo(). This will create a new entry in your state. You can use array methods to add the returned value from function which is triggered onclick, too delete an entry, simply use a filter method, you can look more about it on w3school, it provides good examples which are easy to understand by biggeners.
Related
I have created a reproducible exam of my problem, I don't understand why after the setDefaultValue is called and the component is updated (you can see it's updated using the result of my console.log) If now I click on the reset button instead of the new defaultValue I see the old one.
Here is a link to the example showing this problem, I'll also paste the code here
https://codesandbox.io/s/wonderful-tree-wtsgb4?file=/src/App.js
import "./styles.css";
import {useState, useRef} from 'react';
import TextBox from './TextBox';
export default function App() {
const textboxAPI = useRef(null)
const [defaultValue ,setDefaultValue] = useState('First')
return (
<div className="App">
<div style={{fontWeight: 'bold'}}>To reproduce please first click on the default value button then on the reset button</div>
<TextBox getAPI={(api) => textboxAPI.current = api} defaultValue={defaultValue}/>
<button onClick={() => setDefaultValue('second')}>1- Click me to change default value to "second"</button>
<button onClick={() => textboxAPI.current.reset()}>2- Click me to call reset inside Textbox</button>
</div>
);
}
import {useEffect, useState} from 'react';
const TextBox = ({defaultValue, getAPI}) => {
const [value, setValue] = useState(defaultValue || '')
useEffect(() => {
if (getAPI) {
getAPI({
reset: reset,
})
}
}, [])
const reset = () => {
console.log('TextBox Reset DefaultValue', defaultValue)
setValue(defaultValue)
}
console.log('TextBox DefaultValue', defaultValue)
return <div>{value}</div>
}
export default TextBox;
To reproduce the problem:
1- Click on the first button to set a new defaultValue, see the console.log, you can see the defaultValue has changed inside the TextBox Component
2- Click the reset button, it calls the reset function inside TextBox but the default value logged there has the previous value!
Here you save in textboxAPI.current function reset but just one time after first render of TextBox component. Function reset has a defaultValue in a closure and its value is 'First' during first render. So each next time you call textboxAPI.current.reset(), you call the reset function with defaultValue==='First' in its closure.
But you parent component controls child state and React does not recommend to manage your logic like that.
[UPDATED]
That will fix your issue, but I don not recommend to organize a state logic like that:
const TextBox = ({defaultValue, getAPI}) => {
const [value, setValue] = useState(defaultValue || '')
const reset = () => {
console.log('TextBox Reset DefaultValue', defaultValue)
setValue(defaultValue)
}
if (getAPI) {
getAPI({
reset: reset,
})
}
console.log('TextBox DefaultValue', defaultValue)
return <div>{value}</div>
}
export default TextBox;
Based on what I learned from the comments I tried using Hooks but There were too many changes needed especially I had some issues with React.lazy so I tried to look for more solutions until I found that using a combination of forwardRef and useImperativeHandle I can export my reset function without changing my current structure and it works as it should, I thought that I should share my solution for anyone else who might be looking for an alternative solution to Hooks.
I'm having trouble understanding why a list won't update in React. For my website that I'm building, I'm trying to add a 'favorites' button, but when you click the button it updates the state but the changes never re-render in the list. I tried to make a simpler version, but this doesn't work either:
import React, { useState } from 'react';
import './App.css';
function App() {
const [favorites, setFavorites] = useState([]);
function addFavorite(name, id) {
let newFavorites = favorites;
let newFav = {name: name, id: id};
newFavorites.push(newFav);
setFavorites(newFavorites);
}
return (
<div className="App">
<ul>
{favorites.map((val) => {
return(<li key={val.id}><span>{val.name}</span></li>);
})}
</ul>
<button onClick={() => addFavorite("Thing1", 1)}>Thing 1</button>
<button onClick={() => addFavorite("Thing2", 2)}>Thing 2</button>
<button onClick={() => {console.log(favorites)}}>Check</button>
</div>
);
}
export default App;
I can see the state changes in the console when I log them, but the <ul> element never updates. I've looked online but most of the articles I've found have not been very helpful (I feel the example code I wrote looks a lot like this article.
let newFavorites = favorites;
This assigns newFavorites to point to favorites
newFavorites.push(newFav);
Because newFavorites points to favorites, which is an array in state, you can't push anything onto it and have that change render.
What you need to do, is populate a new array newFavorites with the content of favorites.
Try
const newFavorites = [...favorites];
That should work
I would make some changes in your addFavourite function:
function addFavorite(name, id) {
let newFav = {name, id};
setFavorites([…favourites, newFav]);
}
This way, everytime you click favourite, you ensure a new array is being created with spread operator
Its not working because use are mutating the existing state.
The list is updating but it won't render as useState only renders when the parameter passed to it is different from previous one but in your case though you are changing the list items still the reference is not altering.
To make it work you can use spread operator for lists for even Array.concat() returns a new updated array.
function addFavorite(name, id) {
let newFav = {name: name, id: id};
setFavorites(prev=>[...prev, newFav]);
}
For changing array state, you should use:
function addFavorite(name, id) {
let newFav = { name: name, id: id };
setFavorites((favorites) => [...favorites, newFav]);
}
I have two issues, the first one was when I wanted to pass an array of objects from a parent component to a child component, something like this:
function DropdownSome ({options, placeholder, someNum}) {
const [value, setValue] = useState (options)
return (
<Dropdown as = {ButtonGroup}>
<Dropdown.Toggle as = {CustomToggle}>
{placeholder}
</Dropdown.Toggle>
<Dropdown.Menu as = {CustomMenu}>
{value.map ((r, index) =>
<Dropdown.ItemText key = {r.id}>
<Counter
index={index}
someNum={someNum}
...
/>
</Dropdown.ItemText>
)}
</Dropdown.Menu>
</Dropdown>
)
}
export default DropdownSome;
Here I couldn't set an array with useState (hooks) because it didn't finish passing my constant value. Then solve this using the useEffect hook, like this:
useEffect (() => {
setValue (options); // this worked to pass options in value
}, [options])
Then I was able to pass my array to my const value, and I was able to use value within my child component. In this component I need to set a value inside my array (setValue (array[someIndex].qty= someNum)). My second issue now is that the useEffect hook is always updating the value of my array to its initial state and this does not allow me to update my array within my child component (array[someIndex].qty // always set at the initial value). I've only tried running useEffect once like this:
useEffect (() => {
setValue (options); // this doesn't work to pass options in value
}, [])
but doing this, my array again fails to pass to my constant value and I feel like I'm stuck here. I've also tried clearing useEffect so that the setValue only runs once, but useEffect keeps setting my variable to its initial state.
useEffect (() => {
const counterInterval = setInterval (() => {
setValue (options);
});
return () => clearInterval (counterInterval);
}, [options]);
I am new to this and some hook life cycle topics are taking me some time to understand, but I hope someone has gone through something similar who can advise me.
You don't need to use useState or useEffect for your component, as far as I can see.
Just do this:
function DropdownSome ({options, placeholder, someNum}) {
return (
<Dropdown as = {ButtonGroup}>
<Dropdown.Toggle as = {CustomToggle}>
{placeholder}
</Dropdown.Toggle>
<Dropdown.Menu as = {CustomMenu}>
{(options || []).map ((r, index) =>
<Dropdown.ItemText key = {r.id}>
<Counter
index={index}
someNum={someNum}
...
/>
</Dropdown.ItemText>
)}
</Dropdown.Menu>
</Dropdown>
)
}
export default DropdownSome;
The || [] is added to handle the case that options are not set when the component mounts (the issue I assume you are trying to handle with your hooks).
This is not really clear, but guess you want to get array of options from parent and then store it as value to modify only value and options in parent to remain unchanged.
In such case you can try to initiate value with empty array. const [value, setValue] = useState ([]);
and then in useEffect update value only if value.length=0
like this:
useEffect (() => {
if (value.legth===0) setValue(options);
}, [options])
If you need to modify options upon change in value than check abt callbacks.
I have a list that renders some products, these products are divided into some categories. Some products may have more than one category.
I am trying to apply a filter with these categories through checkboxes. When the user checks the checkbox, the list must be updated with the selected category.
I'm still a beginner in Redux and I don't know how to communicate between the components to update the list. I said communication between components because my list of categories is in the Drawer Component, and my list of products is in the Card component.
I put my code into codesandbox because has a lot of files
Here I'm rendering my list of products:
import React, { useState, useMemo, useEffect } from 'react';
import { useSelector } from 'react-redux';
import CardItem from '../CardItem';
import Pagination from '../Pagination';
import Search from '../Search';
import { useStyles } from './styles';
const Card = (props) => {
const { activeFilter } = props;
const classes = useStyles();
const data = useSelector((state) => state.perfume.collections);
const [searchPerfume, setSearchPerfume] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [perfumesPerPage, setPerfumesPerPage] = useState(3);
console.log('activeFilter: ', activeFilter);
const filteredPerfumes = useMemo(() => {
return data.filter((perfume) =>
perfume.name.toLowerCase().includes(searchPerfume.toLowerCase())
);
}, [data, searchPerfume]);
const currentPerfumes = filteredPerfumes.slice(
(currentPage - 1) * perfumesPerPage,
currentPage * perfumesPerPage
);
const pages = Math.ceil(filteredPerfumes.length / perfumesPerPage);
useEffect(() => {
if (currentPage > pages) {
setCurrentPage(1);
}
}, [currentPage, pages]);
const pageNumbers = Array(pages)
.fill(null)
.map((val, index) => index + 1);
const handleClick = (page) => {
setCurrentPage(page);
};
return (
<div>
<Search
data-testid="input-filter-id"
setSearchPerfume={setSearchPerfume}
/>
{currentPerfumes
.filter((perfume) => {
return (
perfume.name.toLowerCase().indexOf(searchPerfume.toLowerCase()) >= 0
);
})
.map((item) => (
<CardItem key={item.id} item={item} />
))}
<Pagination
pageNumbers={pageNumbers}
handleClick={handleClick}
currentPage={currentPage}
/>
</div>
);
};
export default Card;
Here I'm rendering my list of categories:
import Divider from '#material-ui/core/Divider';
import List from '#material-ui/core/List';
import ListItem from '#material-ui/core/ListItem';
import ListItemText from '#material-ui/core/ListItemText';
import Checkbox from '#material-ui/core/Checkbox';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { useStyles } from './styles';
const DrawerComponent = (props) => {
const { activeFilter, setActiveFilter } = props;
const classes = useStyles();
const data = useSelector((state) => state.perfume.collections);
const handleChange = (text) => (event) => {
setActiveFilter((prev) => ({
...prev,
value: event.target.checked,
text,
}));
};
const allCategories = data
.reduce((p, c) => [...p, ...c.categories], [])
.filter((elem, index, self) => index === self.indexOf(elem));
return (
<div className={classes.root}>
<div className={classes.toolbar} />
<Divider />
<List className={classes.list}>
{allCategories.sort().map((text, index) => (
<ListItem className={classes.itemList} button key={text}>
<Checkbox onChange={handleChange(text)} />
<ListItemText primary={text} />
</ListItem>
))}
</List>
</div>
);
};
export default DrawerComponent;
Do you know how I can apply this filter to my list?
Thank you very much in advance.
State Contol
Generally when you need to communicate between components, the solution is Lifting State Up. Meaning that the state should be controlled by a common ancestor.
Instead of having this in your DrawerComponent:
const [activeFilter, setActiveFilter] = React.useState([]);
Move that hook up to your PerfumeStore and pass activeFilter and setActiveFilter as props to DrawerComponent.
You need to add an onChange function to the Checkbox components in DrawerComponent which adds or removes the category by calling setActiveFilter.
So now you need to apply the activeFilter to your list of perfumes, which is determined in Card. You could move all of that filtering up to PerfumeStore, but to keep it simple let's pass activeFilter down as a prop to Card (it just needs to read but not write the filter, so we don't pass down setActiveFilter). Now the Card component has the information that it needs to filter the items based on the selected categories.
Redux Selectors
Everything so far has just had to do with react and the fact that you are using redux hasn't come into play at all. The way that you would incorporate redux principles, if you wanted to, is do define some of the filtering and mapping logic outside of your components as selector functions of state and other arguments. Then instead of calling useSelector to get a huge chunk of state which you process inside the component, you can call useSelector with a selector that gets just the data which you need.
An obvious place to do this is in DrawerComponent, where you are getting the category list. Make a selector function getPerfumeCategories which takes the state and returns the categories. Then in your component, you call const allCategories = useSelector(getPerfumeCategories);
Now all that this component is responsible for is rendering the categories. It is no longer responsible for storing the selections (we've already moved the useState out) or for finding the categories from the state. This is good! You can read up on principles like the Single Responsibility Principle, Separation of Concerns, Logic vs. Presentation components, etc. if you want a deeper understanding of why this is good.
In Card you could use a selector that gets an already-filtered list of perfumes. But in this case a getCurrentPagePerfumes function would take a lot of different arguments so it's kind of messy.
Edit: Filtering
You've asked for help with how to apply the value of activeFilter to filter the perfumes which are shown in your list.
Multiple categories can be selected at once, so activeFilter needs to identify all of the actively selected categories. I first suggested an array of names, but removing items from an array (without mutation) is more complicated than assigning values to objects.
So then I thought about having an object where the keys are the category names and the values are a boolean true/false of whether the category is checked. This makes handleChange really simple because we can update the value for that key to the value of event.target.checked.
const handleChange = (text) => (event) => {
setActiveFilter((prev) => ({
...prev,
[text]: event.target.checked,
}));
};
...prev says "keep everything the same except the key that I am changing".
[text] says "the key I am updating is the variable text, not the literal key 'text'"
event.target.checked is the boolean of whether this category is checked.
We could set an initial state for activeFilter which includes a key for every category and all the values are false (ie. nothing selected). Or we could allow for the object to be incomplete with the assumption that if it key isn't included, then it isn't checked. Let's do that.
So now our activeFilter looks something like: {Floral: true, Floriental: false, Fresh: true} where some are true, some are false, and lots are missing and therefore assumed to be false.
We need to figure out how to filter the displayed perfumes based on the value of activeFilter. Let's start by writing a function that determines whether one perfume is eligible to be shown, and then we can use that as a callback of array.filter() on an array of perfumes. We want a perfume to be included if any of its categories are checked (unless you want it to match all the checked categories?). That looks like:
perfume.categories.some(
category => activeFilter[category] === true
);
Where .some() loops through the categories and returns true if our callback is true for any category.
I added this to your filteredPerfumes memo and added activeFilter as a dependency so that it will re-filter when the checkboxes change.
const filteredPerfumes = useMemo(() => {
return data.filter((perfume) =>
perfume.name.toLowerCase().includes(searchPerfume.toLowerCase())
&& perfume.categories.some(
category => activeFilter[category] === true
)
);
}, [data, searchPerfume, activeFilter]);
That works, except that nothing shows when no categories are checked -- whoops! So we want to add a special case that says "all perfumes pass the category filter if no categories are checked." To do that, we need to know if there are checked categories or not. There's a lot of ways to do that, but here's one:
const hasCategoryFilter = Object.values(activeFilter).includes(true);
We look at all of the values in the activeFilter object and see if it includes any which are true.
Now we need to use this value to only filter based on categories when it's true. I'll pull our previous logic into a function and add an if statement (note: the boolean operator || is shorter to use, but I think the if is more readable).
const matchesCategories = (perfume) => {
if ( hasCategoryFilter ) {
return perfume.categories.some(
category => activeFilter[category] === true
);
} else return true;
}
Sidenote: we have two independent filters, one for search and one for category, so we could call data.filter once and check for both conditions at once or twice and check each condition separately. It does not matter which you do.
The final filter is:
const filteredPerfumes = useMemo(() => {
const hasCategoryFilter = Object.values(activeFilter).includes(true);
const matchesCategories = (perfume) => {
if ( hasCategoryFilter ) {
return perfume.categories.some(
category => activeFilter[category] === true
);
} else return true;
}
return data.filter((perfume) =>
perfume.name.toLowerCase().includes(searchPerfume.toLowerCase())
).filter( matchesCategories );
}, [data, searchPerfume, activeFilter]);
Updated Sandbox
I am trying to do some kind of online shop for myself and I got a problem.
I want to render my shopping cart size in the NavBar component (which is on every page).
I created a Cart Items service where I put all my added items, and it also has functions to addItem, removeItem, getCart, getCartSize.
When I click on Add/Remove on specific product, I would like to do that the value on NavBar with cart size would be changing depending on the cart size (from getCartSize method). I already tried to use useEffect hook, but it does not recognize when the value of cartSize is changed, how can I do that?
This is what I have done already.
navbar.jsx:
//...
//...
import {getTotalCount} from '../../services/myCart';
export default function Navbar() {
// ...
const [count, setCount] = useState();
useEffect(() => {
setCount(getTotalCount());
console.log('counto useeffect');
},[getTotalCount()]);
//},[getTotalCount]); doesn'work also.
//},[count]); doesn'work also.
return (
<>
<header className="navbar">
<Link className="navbar__list-item__home-icon" to="/"><span><FaHome/></span></Link>
<Link className="navbar__list-item" to="/products">Products</Link>
<h2>cart size--> {count}</h2>
<Link className="navbar__list-item__cart-img" to="shopping-cart"><span><FaShoppingCart/></span></Link>
</header>
</>
);
}
myCart.js all functions work fine and I call them when I click add/remove button in on my component in the products page.
var InTheCart = [
];
var totalCount = 0;
export function AddToCart(item) {
// ... logic
totalCount++;
}
export function ContainsInTheCart(id) {
return InTheCart.findIndex(x=> x.item.id == id) != -1 ? true: false;
}
export function RemoveFromCart(id) {
// ... logic
totalCount--;
}
export function getCart() {
return InTheCart;
}
export function getTotalCount() {
return totalCount;
}
React is called react because it re-renders when it reacts to something that changed.
For that to happen, the react components need to know, somehow, that something changed.
In the code example you show, the totalCount (which global access provided by exported function getTotalCount) is independent from react component tree, so that data can change without react knowing it.
So, the question is: how to make your react component aware of those changes?
A few mechanisms exist, here are a few of them:
Use a global state manager like redux, with redux actions and reducer to mutate the state and useSelector in the component that will make them aware of any change, and rerender
React context which will hold the cart state. That context can export both that data, and th function that will mutate the data, so they can be shared between components that have access to that context provider.
Your context could be used in the component that way:
const cartContext = useContext(CartContext)
const { cartState, addToCart, removeFromCart } = cartContext
const { totalCount, inTheCart } = cartState
[...somwhere later...]
<ul><li onClick={() => removeFromCart(index)}>{inTheCart[index].name}</li></ul>
<footer>{`${totalCount} items`}</footer>
I let you build the context object around your existing functions ;)
A simple solution is to lift the state up. Create the state in your layout file from where Header and Cart can access the count as Props. useEffect will be invoked whenever there is a change in the state or props of a component.