Experience: I am a total beginner in React.
What I am trying to learn: Hooks (useState) - but I do not know how to update the state and rerender the view with this. As far as I understood React does not rerender the view if the updated state is somewhat similar to the last one... After googling, I tried to copy the state and update it somehow, but I am missing something, and I do not know what.
What I am trying to do in the project: I have a list of countries I want to filter through when the user selects a region from a dropdown. This is the function that gets fired when the selection happens, along with comments that I hope explain what I am trying to do:
const change = event => {
//copy the `data` state (which has a list of all the countries)
let newData = [...data];
console.log(newData);
//filter through the countries list to get only those with the selected region
let filtered = newData.filter(obj => obj.region === event.target.value);
console.log(filtered);
//change the countries list with the filtered one, and rerender the view
setData([data, newData]);
console.log(data);
};
You can find the file and the code in question HERE (scroll down to get to the change function)
Select a region from the 'Fitler by region dropdown'
See the errors/outputs in the console
You are updating the state to an array of objects and the last item will be the filtered list
Instead, pass in a single array that holds the filtered countries.
Note that your state will be lost the second time you select a different region because you are modifying the entire collection of countries.
setData(data.filter(obj => obj.region === event.target.value))
So what you can we to avoid losing the state?
We can filter the list based on the selected region.
Added comments where i changed the code
export default function CountriesList() {
const [data, setData] = useState([]);
const [distinctRegions, setDistinctRegions] = useState([]);
const [loading, setLoading] = useState(true);
// added state to track the selected region
const [selectedRegion, setSelectedRegion] = useState("");
useEffect(() => {
CountriesAPI().then(res => {
onLoad(res);
setLoading(false);
});
}, []);
const onLoad = dataList => {
setData(...data, dataList);
getRegions(dataList);
};
const getRegions = dataList => {
let regions = [];
dataList.map(dataItem =>
dataItem.region.length ? regions.push(dataItem.region) : ""
);
let regionsFiltered = regions.filter(
(item, index, arr) => arr.indexOf(item) === index
);
setDistinctRegions(...distinctRegions, regionsFiltered);
};
const renderLoading = () => {
return <div>Loading...</div>;
};
// now we only need to update the selected region
const change = event => {
setSelectedRegion(event.target.value);
};
const renderData = (dataList, distinctRegionsItem) => {
if (dataList && dataList.length) {
return (
<div>
<Container>
<Input type="text" placeholder="Search for a country..." />
<Select className="select-region" onChange={change}>
<option value="" hidden>
Filter by region
</option>
// added show all
<option value="">Show All</option>
{distinctRegionsItem.map(item => {
return (
<option key={item} value={item}>
{item}
</option>
);
})}
</Select>
</Container>
<CardList>
// filter the array based on selectedRegion and then render the list.
// if selectedRegion is empty show all
{dataList
.filter(
country => !selectedRegion || country.region === selectedRegion
)
.map(country => (
<CountryCard
population={country.population}
region={country.region}
capital={country.capital}
flag={country.flag}
key={country.alpha3Code}
id={country.alpha3Code}
name={country.name}
/>
))}
</CardList>
</div>
);
} else {
return <div>No items found</div>;
}
};
return loading ? renderLoading() : renderData(data, distinctRegions);
}
Related
I was watching a tutorial on how to make todos, though my main focus was local storage use.
But when he made the delete button then I was a bit confused, the code below shows how he did it but I am not getting it.
Can anyone explain that I tried using the splice method to remove items from the array but I am not able to remove the items from the page?
Can you also suggest what should I do after using splice to return the array on the page?
Below is the code,
import "./styles.css";
import { useState, useEffect } from 'react'
import Todoform from './TodoForm'
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (e) => {
// TODO: items.splice(e-1, 1);
// Is there any other way I can do the below thing .i.e
// to remove todos from page.
// this is from tutorial
setitems((e1)=>{
return e1.filter((er , index)=>{
return index!=e-1;
})
})
}
return (
<>
<div className='display_info'>
<h1>TODO LIST</h1>
<br />
<input onChange={itemevent} value={list} type="text" name="" id="" />
<br />
<button onClick={listofitem} >Add </button>
<ul>
{
items.map((e, index) => {
index++;
return (
<>
<Todoform onSelect={deleteItems} id={index} key={index} index={index} text={e} />
</>
)
})
}
</ul>
</div>
</>
)
}
And this is the TodoForm in this code above,
import React from 'react'
export default function Todoform(props) {
const { text, index } = props;
return (
<>
<div key={index} >
{index}. {text}
<button onClick={() => {
props.onSelect(index)
}} className="delete">remove</button>
</div>
</>
)
}
Here is the codeSandbox link
https://codesandbox.io/s/old-wood-cbnq86?file=/src/TodoForm.jsx:0-317
I think one issue with your code example is that you don't delete the todo entry from localStorage but only from the components state.
You might wanna keep localStorage in sync with the components state by using Reacts useEffect hook (React Docs) and use Array.splice in order to remove certain array elements by their index (Array.splice docs).
// ..
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
/* As this `useEffect` has an empty dependency array (the 2nd parameter), it gets called only once (after first render).
It initially retrieves the data from localStorage and pushes it to the `todos` state. */
useEffect(() => {
const todos = JSON.parse(localStorage.getItem("notes"));
setitems(todos);
}, [])
/* This `useEffect` depends on the `items` state. That means whenever `items` change, this hook gets re-run.
In here, we set sync localStorage to the current `notes` state. */
useEffect(() => {
localStorage.setItem("notes", JSON.stringify(items));
}, [items])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (index) => {
// This removes one (2nd parameter) element(s) from array `items` on index `index`
const newItems = items.splice(index, 1)
setitems(newItems)
}
return (
<>
{/* ... */}
</>
)
}
There are multiple ways to remove an item from a list in JS, your version of splicing the last index is correct too and it is able to remove the last item. What it can't do is update your state.
His code is doing two things at the same time: Removing the last item of the Todo array and then, setting the resulted array in the state which updates the todo list.
So, change your code as
const deleteItems = (e) => {
let newItems = [...items];
newItems.splice(e-1, 1);
setitems(newItems);
}
I have a ul that displays users with a checkbox input. When searching for a user by surname/first name, the previously selected input checkboxes are removed. How to prevent it?
function App() {
let copyList = useRef();
const [contacts, setContacts] = useState([]);
useEffect(() => {
fetch(api)
.then((res) => res.json())
.then((data) => {
copyList.current = data;
setContacts(copyList.current);
})
.catch((err) => console.log(err));
}, []);
contacts.sort((a, b) => (a.last_name > b.last_name ? 1 : -1));
const searchHandler = (value) => {
const contactsList = [...copyList.current].filter((x) => {
if (value.toLowerCase().includes(x.last_name.toLowerCase())) {
return x.last_name.toLowerCase().includes(value.toLowerCase());
} else if (value.toLowerCase().includes(x.first_name.toLowerCase())) {
return x.first_name.toLowerCase().includes(value.toLowerCase());
} else if (value === "") {
return x;
}
});
setContacts(contactsList);
};
return (
<div className="App">
<Header />
<SearchBar onSearch={(value) => searchHandler(value)} />
<ContactsList contacts={contacts} />
</div>
);
}
Input component is in ContactsList component.
function Input(props) {
const [isChecked, setIsChecked] = useState(false);
const [id] = useState(props.id);
const handleChange = () => {
setIsChecked(!isChecked);
console.log(id);
};
return (
<input
type="checkbox"
className={style.contact_input}
checked={isChecked}
onChange={handleChange}
value={id}
/>
);
}
When you filter the contacts and update the contacts state, the list in ContactList will be re-rendered as it is a new array which means you will have a new set of Input components with the default state. In order to persist the previously selected items you will also have to store the array of selected IDs in state. The Input component should receive the isChecked and onChange values from props so you can pass in a condition where you check if the current ID is in the list of selected IDs which means it is checked and when the user clicks on it, you can just simply update that array by adding the ID if it's not currently in the array or removing from it if it is already present (copying the array first since it's a state variable).
I have a form and checkbox's that I create from an array using map. The point of the checkboxes is if checked remove item from array if not checked add it back to array. So every onChange I check if it is checked or not and do that work, but the way it works now is the first checkbox I check it will not remove or add the item, but after that it will work like its supposed to work.
const {query} = useRouter();
let queryCopy = JSON.parse(JSON.stringify(query));
const [stateEditedQuery, setStateEditedQuery] = useState(query);
const tagDeletion = (e, tag) => {
console.log(e, tag);
if(e) {
let index = query.tags.indexOf(tag);
query.tags.splice(index, 1);
} else {
if(query.tags.indexOf(tag) === -1) query.tags.push(tag);
}
setStateEditedQuery(query);
console.log("THIS HRERE", query.tags);
};
<fieldset>
<legend>Delete Tags</legend>
{query.tags?.map((tag, index) => {
return (
<Checkbox
labelText={tag}
id={index}
key={index}
onChange={(e) => tagDeletion(e, tag)}
/>
)
})}
</fieldset>
So this is a relatively common problem including the useState hook, what you should do is update the current state like so:
const [value, setValue] = useState<string[]>([]);
const handleChange = (id: string) => {
setValue(curr =>
curr.includes(id)
? // when checkbox is already checked
curr.filter(x => x !== id)
: // when checkbox is to be checked
[...curr, id]
);
};
if you are not using Typescript, just omit the type definitions. Default state should be handled just by checking if
value.includes(id).
I have two arrays,
const [imagesUrlArray, setURls] = useState([])
const [imagesArray, setImages] = useState([])
using handle change below; imagesUrlArray is used to display the images on the screen, and imagesArray is saved to later update those same images to the database
const handleChange = (e) => {
let selected = e.target.files[0]
var selectedImageSrc = URL.createObjectURL(selected);
addUrl(selectedImageSrc)
addImage(selected)
};
Though I now want to click the X(delete) button and remove the item at index of imagesUrlArray and imagesArray (say if the user no longer wants to use that image)
<div className="img-grid">
{ imagesUrlArray && imagesUrlArray.map((url,index) => {
return ( <div key={index}
className="img-wrap">
{/* <span class="close">×</span> */}
<button onClick={ () => handleRemove(index)} className="close">X</button>
<img src={url} alt="uploaded" />
</div>
)
}
)}
</div>
I have tried splice and slice etc but still cannot find a perfect solution,
here is the handleRemove Function
const handleRemove = (index) => {
const newImagesArray = imagesArray.splice(index);
const newImagesUrlArray = imagesUrlArray.splice(index);
setImages(newImagesArray);
setURls(newImagesUrlArray);
}
You can do something like this:
const handleRemove = (index) => {
setImages(imagesArray.filter((x,i) => i !== index));
setURls(imagesUrlArray.filter((x,i) => i !== index));
}
So, basically the idea is to skip the element at specific index and return the remaining items and set the state.
I'm studying the reaction, and I'm trying to filter by manufacturer the products that I previously request via the API. With this code, products are displayed, and then you can filter by manufacturer. However, when you try to filter them again, i.e. select products from a different manufacturer, nothing happens.
My code:
import React, {useEffect} from 'react'
import Loader from './../Loader/Loader'
import ProductItem from './ProductItem'
function Catalog() {
const [products, setProducts] = React.useState([])
const [brands, setBrands] = React.useState([])
const [brand, setBrand] = React.useState('')
useEffect(() => {
const proxyUrl = 'https://cors-anywhere.herokuapp.com/'
const url = 'https://avtodoka-msk.ru/aimylogic-mission.json'
fetch(proxyUrl + url)
.then(response => response.json())
.then(products => {
setProducts(products)
const brands = []
products.map(product => {
return(
brands.push(...product.brend)
)
})
setBrands([...new Set(brands)])
})
}, [])
function toggleBrand(e) {
setBrand(e.target.value)
setProducts(
products.filter(product => {
if ([...product.brend].includes(e.target.value)) {
return true
}
})
)
}
return (
<div className="container pt-5">
{brands.length ? (
<div className="row">
<div className="col-3">
<select value={brand} onChange={toggleBrand}>
<option value={''}>Выбрать бренд</option>
{brands.map((brend,index) => {
return(
<option value={brend} key={index}>{brend}</option>
)
})}
</select>
</div>
</div>
): null}
{products.length ? (
<div className="row">
{products.map(product => {
return (
<ProductItem
key={product.id}
product={product}
/>
)
})}
</div>
) : <Loader />}
</div>
)
}
export default Catalog
If I understood correctly your state needs to have:
products array - containing your products.
brands array - containing your manufacturers.
brand array - containing an active filter by manufacturer.
If that is correct, then I see a couple of problems with your code:
You're overcomplicating your array methods, leading to wanted behaviors.
Now, the real problem here is that when you filter the first time, you lose access to all the products, and therefore you need to re-fetch them on every filter click.
Giving that, here's your updated code:
import React, {useEffect} from 'react'
import Loader from './../Loader/Loader'
import ProductItem from './ProductItem'
function Catalog() {
const [products, setProducts] = React.useState([])
const [brands, setBrands] = React.useState([])
const [brand, setBrand] = React.useState('')
useEffect(() => {
const proxyUrl = 'https://cors-anywhere.herokuapp.com/'
const url = 'https://avtodoka-msk.ru/aimylogic-mission.json'
fetch(proxyUrl + URL)
.then(response => response.json())
.then(products => {
if (brands.length <= 0)
setBrandsFromProducs(products)
const filteredProducts = products.filter(product => !brand || product.brend.includes(brand))
setProducts(filteredProducts)
})
}, [brand])
function setBrandsFromProducs(products) {
const brands = products.reduce(
(acc, product) => ([...acc, ...product.brend])
, [])
setBrands([...new Set(brands)])
}
function toggleBrand(e) {
const selectedBrand = e.target.value
setBrand(selectedBrand)
}
}
Key aspects:
product.brend is an array. To extract all the brands from this JSON, you need to flatten an array of arrays, therefore you're reducing it into a flat array - hence the usage of a .reduce(). You only need to do this if the brands array is empty. However, if you're not concerned about performance you can remove the if statement to always reset the brandsarray.
Your event is selecting the brand, it shouldn't contain any other side-effect (filtering the products). Those should live inside a proper scope -> useEffect. To trigger this event, you need to 'listen' to changes to the brand filter, therefore it needs to be specified as a dependency of the useEffect.
Your filter was missing the return false outside the if statement. However, it could be simplified because .includes() returns a boolean value and doesn't mutate the original array. The filter I created will first check if you have an active filter and only filter if you do have one.
You need to make sure that e.target.value is the exact string from the product.brend's array.
Small suggestions:
If you control the API, your filters should always be on the backend side to avoid bigger payloads than you need.
If you don't use the brand state, you can safely remove it. Without your JSX I can't be sure.