I am developing a simple application in reactS. The main purpose of the app is it will show some cards and on search, cards will be filtered and selective cards will be displayed. I am sharing the code of App.js.
I have a file name 'Robots.js'
import './App.css';
import CardList from './Components/CardList';
import Header from './Components/Header';
import {Robots} from './Components/Robots';
function App() {
const [robots,setRobots] = useState({
robo:Robots,
search:''
});
const onSearchChange = (e) =>{
setRobots({...robots,search:e.target.value});
const filteredRobots = robots.robo.filter(item=>{return item.name.toLowerCase().includes(robots.search.toLowerCase())});
//setRobots({...robots,robo:filteredRobots});
console.log(filteredRobots);
}
return (
<div>
<Header onSearchChange={onSearchChange} />
<CardList Robots={robots.robo}/>
</div>
);
}
export default App;
If I comment
setRobots({...robots,robo:filteredRobots});
this line on console I can show array is reducing its number of items but with that line it just does nothing. I think it makes sense it should do.
I hope I made my point clear.
You can update the state in one go as shown below:
const onSearchChange = (e) => {
const filteredRobots = robots.robo.filter(item => {
return item.name.toLowerCase().includes(robots.search.toLowerCase())
});
console.log(filteredRobots);
setRobots({
robo: filteredRobots,
search: e.target.value
});
}
Since you only have two properties you can just create a new object with those properties and don't really need spread operator.
The setRobots run "asynchronous" - therefore if you have:
const[data, setData] = setState(5);
const update = (newData) => {
setData(1);
console.log(data) // will print 5
}
And only in the second render its will print 1
you can read more in here: https://reactjs.org/docs/state-and-lifecycle.html
In your implementation you have a problem that you don't save the original array of the cards
So first save the original array
// Here will be the full list of cards.
const [cards, setCards] = useState(allCards);
// Here will be the search by text
const [search, setSearch] = useState("");
// Here will be all the filtered cards
// If you don't know whats useMemo is you can look in here: https://reactjs.org/docs/hooks-reference.html#usememo
const filteredCards = useMemo(()=>{
return cards.filter((card) =>
card.item.toLowerCase().contains(search.toLowerCase()));
},[cards, search])
// on search changed callback
const onSearchChange = (e)=>{setSearch(e.target.value || '')}
// then return your component
return (
<div>
<Header onSearchChange={onSearchChange} />
<CardList Robots={filteredCards}/>
</div>
);
Related
I want to add to the array a new object everytime I click at a card, but when I do so it changes the last key-pair to the new one and it doesnt add it. I chose this method of updating the state since I saw it is more popular than the one with the push.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { AiOutlineHeart, AiFillHeart } from "react-icons/ai";
import styles from "./MovieCard.module.css";
const imagePrefixUrl = "http://image.tmdb.org/t/p/w500";
const MovieCard = (props) => {
const [items, setItems] = useState(
JSON.parse(localStorage.getItem("favorites")) || []
);
const [liked, setLiked] = useState(false);
const movie = props?.movie;
const addFavoriteHandler = (movie) => {
setItems((data) => [...data, movie]);
};
useEffect(() => {
localStorage.setItem("favorites", JSON.stringify(items));
}, [items]);
return (
<div className={styles.container}>
{liked ? (
<button className={styles.heartIcon} onClick={() => setLiked(false)}>
<AiFillHeart />
</button>
) : (
<button
className={styles.heartIcon}
onClick={() => addFavoriteHandler(movie)}
>
<AiOutlineHeart />
</button>
)}
<Link to={`/movie/${movie.id}`} title={movie?.title}>
<img src={`${imagePrefixUrl}${movie?.backdrop_path}`} />
<p>{movie?.title}</p>
</Link>
</div>
);
};
export default MovieCard;
I am assuming from the component name MovieCard, that your app would have multiple instances of this component under a parent component (assumed to be MovieCardList).
A solution to your issue would be to lift the state and addFavoriteHandler
const [items, setItems] = useState(
JSON.parse(localStorage.getItem("favorites")) || []
);
to the parent component MovieCardList and pass the handler addFavoriteHandler as a prop to each MovieCard.
This would ensure that you have a single point for updating your localStorage key favorites and it would not be overridden by new update.
The reason for the override issue you are experiencing is that each card has an instance of items and it does not fetch the latest value of favorites from the localStorage before updating it, meaning it would always override the favorites in localStorage as per the current code.
function App(){
const [selectedLang, setSelectedLang] = useState(0);
const [langList, setLangList] = useState([]);
useEffect(()=>{
const list = [];
/* i18n.options.supportedLngs is ['en', 'ko'] */
i18n.options.supportedLngs.map((item, i)=>{
list.push(item);
/* Set selectedLang to i18n's default language.(Default is 'ko')*/
if(item === i18n.language){
setSelectedLang(i);
}
})
setLangList(list);
}, [])
useEffect(()=>{
console.debug("langList :", langList, ",", selectedLang); // <- It print correctly ['en', 'ko'], 1
}, [langList, selectedLang])
return(
<Child defaultIndex={selectedLang} childList={langList}/>
)
}
function Child(props){
const {defaultIndex, childList} = props;
const [selected, setSelected] = useState(0);
useState(()=>{
setSelected(defaultIndex);
},[])
return(
<div>
{childList[selected]} //<- But it print 'en'.
</div>
)
}
The code above is a simplified version of my code.
Currently i18n defaults to 'ko'. I want to display 'ko' in Child by setting selectedLang to 'ko's index in App, and giving the index of 'ko' and the entire language array as props to Child.
However, Child's selected and defaultIndex doesn't seem to change from a state initialized with useState(0).
Can someone help me?
setSelected need to be called after changing defaultIndex in Child component.
And you didn't use proper hook.
useEffect(()=>{
setSelected(defaultIndex);
},[defaultIndex])
You can use react context to achieve this:
First make a Lang provider in app.js
const LangContext = React.createContext('ko');
function App() {
const [lang, setLang] = useState("ko");
return (
<LangContext.Provider value={[lang, setLang]}>
<Toolbar />
</LangContext.Provider>
);
}
Then in any child component you can use :
function Button() {
const [lang, setLang]= useContext(LangContext);
return (
<button onClick={()=>{setLang('en')}}>
{lang}
</button>
);
}
Any change in context will propagate in all components using useContext.
Read more here : https://reactjs.org/docs/hooks-reference.html#usecontext
I wrote a program that takes and displays contacts from an array, and we have an input for searching between contacts, which we type and display the result.
I used if in the search function to check if the searchKeyword changes, remember to do the filter else, it did not change, return contacts and no filter is done
I want to do this control with useEffect and I commented on the part I wrote with useEffect. Please help me to reach the solution of using useEffect. Thank you.
In fact, I want to use useEffect instead of if
I put my code in the link below
https://codesandbox.io/s/simple-child-parent-comp-forked-4qf39?file=/src/App.js:905-913
Issue
In the useEffect hook in your sandbox you aren't actually updating any state.
useEffect(()=>{
const handleFilterContact = () => {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
};
return () => contacts;
},[searchKeyword]);
You are returning a value from the useEffect hook which is interpreted by React to be a hook cleanup function.
See Cleaning up an effect
Solution
Add state to MainContent to hold filtered contacts array. Pass the filtered state to the Contact component. You can use the same handleFilterContact function to compute the filtered state.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const [filtered, setFiltered] = useState(contacts.slice());
const setValueSearch = (e) => setSearchKeyword(e.target.value);
useEffect(() => {
const handleFilterContact = () => {
if (searchKeyword.length >= 1) {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
} else {
return contacts;
}
};
setFiltered(handleFilterContact());
}, [contacts, searchKeyword]);
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts} filter={filtered} />
</div>
);
};
Suggestion
I would recommend against storing a filtered contacts array in state since it is easily derived from the passed contacts prop and the local searchKeyword state. You can filter inline.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const setValueSearch = (e) => setSearchKeyword(e.target.value);
const filterContact = (contact) => {
if (searchKeyword.length >= 1) {
return contact.fullName
.toLowerCase()
.includes(searchKeyword.toLowerCase());
}
return true;
};
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts.filter(filterContact)} />
</div>
);
};
I have made a basic application to practice React, but am confused as to why, when I try to delete a single component from an state array, all items after it get deleted too. Here is my basic code:
App.js:
import React from 'react'
import Parent from './Parent';
import './App.css';
function App() {
return (
<div className="App">
<Parent />
</div>
);
}
export default App;
Parent.js:
import React, { useState } from 'react';
import ListItem from './ListItem';
import './App.css';
function Parent() {
const [itemList, setItemList] = useState([])
const [numbers, setNumbers] = useState([])
const addItem = () => {
const id = Math.ceil(Math.random()*10000)
const newItem = <ListItem
id={id}
name={'Item-' + id}
deleteItem={deleteItem}
/>
const list = [...itemList, newItem]
setItemList(list)
};
const deleteItem = (id) => {
let newItemList = itemList;
newItemList = newItemList.filter(item => {
return item.id !== id
})
setItemList(newItemList);
}
const addNumber = () => {
const newNumbers = [...numbers, numbers.length + 1]
setNumbers(newNumbers)
}
const deleteNum = (e) => {
let newNumbers = numbers
newNumbers = newNumbers.filter(n => n !== +e.target.innerHTML)
setNumbers(newNumbers);
}
return (
<div className="Parent">
List of items:
<div>
{itemList}
</div>
<button onClick={addItem}>
Add item
</button>
<div>
List of numbers:
<div>
{numbers.map(num => (
<div onClick={deleteNum}>{num}</div>
))}
</div>
</div>
<button onClick={addNumber}>
Add number
</button>
</div>
);
};
export default Parent;
ListItem.js:
import React from 'react';
import './App.css';
function ListItem(props) {
const { id, name, deleteItem } = props;
const handleDeleteItem = () => {
deleteItem(id);
}
return (
<div className="ListItem" onClick={handleDeleteItem}>
<div>{name}</div>
</div>
);
};
export default ListItem;
When I add an item by clicking the button, the Parent state updates correctly.
When I click on the item (to delete it), it deletes itself but also every item in the array that appears after it <-- UNWANTED BEHAVOUR. I only want to delete the specific item.
I have tested it with numbers too (not creating a separate component). These delete correctly - only the individual number I click on is deleted.
As far as I can tell, the individual item components are saving a reference as to what the Parent state value was when they are created. This seems like very strange behaviour to me...
How do I delete only an individual item from the itemList state array when they are made up of separate components?
Thanks
EDIT: As per the instruction from Bergi, I fixed the issue by converting the 'itemList' state value to an array of objects to render (and rerender) when the list is changed instead:
const addItem = () => {
const id = Math.ceil(Math.random()*10000);
const newItem = {
id: id,
name: 'Item-' + id,
}
const newList = [...itemList, newItem]
setItemList(newList)
}
...
React.useEffect(() => {
}, [itemList]);
...
<div className="Parent">
List of items:
<div>
{itemList.map(item => {
return (<ListItem
id={item.id}
name={item.name}
deleteItem={deleteItem}
/>);
})}
...
The problem is that your deleteItem function is a closure over the old itemList, back from the moment in which the item was created. Two solutions:
use the callback form of setItemList
don't store react elements in that list, but just plain objects (which you can use as props) and pass the (most recent) deleteItem function only when rendering the ListItems
import React, { useState, useEffect } from "react";
import axios from "axios";
const App = () => {
let [countries, setCountries] = useState([]);
const [newCountry, newStuff] = useState("");
const hook = () => {
//console.log("effect");
axios.get("https://restcountries.eu/rest/v2/all").then((response) => {
console.log("promise fulfilled");
setCountries(response.data);
//console.log(response.data);
});
};
const filter = (event) => {
newStuff(event.target.value);
if (event.target.value === undefined) {
return
} else {
let value = event.target.value;
console.log(value);
countries = countries.filter((country) => country.name.startsWith(value));
setCountries(countries);
console.log(countries);
}
};
useEffect(hook, []);
return (
<div>
<p>find countries</p>
<input value={newCountry} onChange={filter} />
<ul>
{countries.map((country) => (
<li key={country.name.length}>{country.name}</li>
))}
</ul>
</div>
);
};
export default App;
So I have a search bar so that when you enter a few characters it will update the state and show the countries that start with the respective first characters. However, nothing is being shown when I enter input into my search bar. Also, my filter function, when I console.log my countries array which is supposed to have the countries that start with the characters I entered, it's always an empty array.
You need some changes in order to make this work:
Use two states for countries, one for the list you
get in the initial render and another for the current filter
countries.
const [countriesStore, setCountriesStore] = useState([]); // this only change in the first render
const [countries, setCountries] = useState([]); // use this to print the list
I recomed to use any tool to manage the state and create a model for
the countries ther you can make the side effect there and create an
action that update the countries store. I'm using Easy Peasy in
my current project and it goes very well.
Take care of the filter method because startsWith
method is not case-insensitive. You need a regular expression or
turn the current country value to lower case. I recommend to use
includes method to match seconds names like island in the search.
const filterCountries = countriesStore.filter(country => {
return country.name.toLowerCase().includes(value);
});
Remove the if condition in the filter in order to include the
delete action in the search and get the full list again if
everything is removed.
Just in the case, empty the search string state in the first
render
useEffect(() => {
hook();
setSearchString("");
}, []);
Replace the length in the list key. You can use the name and trim to remove space.
<li key={country.name.trim()}>{country.name}</li>
The final code look like this:
export default function App() {
const [countriesStore, setCountriesStore] = useState([]);
const [countries, setCountries] = useState([]);
const [searchString, setSearchString] = useState("");
const hook = () => {
axios.get("https://restcountries.eu/rest/v2/all").then(response => {
console.log("promise fulfilled");
setCountriesStore(response.data);
setCountries(response.data);
});
};
const filter = event => {
setSearchString(event.target.value);
let value = event.target.value;
const filterCountries = countriesStore.filter(country => {
return country.name.toLowerCase().includes(value);
});
setCountries(filterCountries);
};
useEffect(() => {
hook();
setSearchString("");
}, []);
return (
<div>
<p>find countries</p>
<input value={searchString} onChange={filter} />
<ul>
{countries.map(country => (
<li key={country.name.trim()}>{country.name}</li>
))}
</ul>
</div>
);
}
You need to wrap your hook into async useCallback:
const hook = useCallback(async () => {
const {data} = await axios.get("https://restcountries.eu/rest/v2/all");
setCountries(data);
}, []);
you are not able to mutate state countries. Use immutable way to update your state:
const filter = (event) => {
newStuff(event.target.value);
if (event.target.value === undefined) {
return
} else {
let value = event.target.value;
setCountries(countries.filter((country) => country.name.startsWith(value)));
}
};
And useState is asynchronous function. You will not see result immediately. Just try to console.log outside of any function.