I am trying to sort JSON object by date and number. Everything is working fine when i console log but the state is not getting updated on the GUI side. What am i missing? I am using the functional components.
Here is the code...
const Posts = () => {
const [dummyData, setDummyData] = useState(Data);
const sortList = (e) => {
if (e.target.value === "date") {
handleSort();
} else if (e.target.value === "upvotes") {
byUpvotes();
}
};
const handleSort = () => {
const sortedData = dummyData.sort((a, b) => {
const c = new Date(a.published);
const d = new Date(b.published);
if (c.getDate() > d.getDate()) {
return c;
} else {
return d;
}
});
setDummyData(sortedData);
console.log(sortedData);
};
const byUpvotes = () => {
const sortByName = dummyData.sort((a, b) => {
return b.upvotes - a.upvotes;
});
setDummyData(sortByName);
console.log(sortByName);
};
return (
<div>
{dummyData.map((post) => (
<PostsItem key={post.id} post={post} />
))}
<div className="row">
<div className="col-s6">
<label>Browser Select</label>
<select className="browser-default" onChange={sortList}>
<option disabled selected>
Choose your option
</option>
<option value="date">Date</option>
<option value="upvotes">Upvotes</option>
</select>
</div>
</div>
</div>
);
};
The sort function does not create a new array, it mutates the old one. So you're rearranging the existing state, and then setting state with the same array. Since it's the same array, react thinks the state hasn't changed and skips rendering.
Instead, you will need to make a copy of the array and then sort that. For example:
const byUpvotes = () => {
const sortByName = [...dummyData];
sortByName.sort((a, b) => {
return b.upvotes - a.upvotes
})
setDummyData(sortByName)
}
Related
I am making Quiz app in React and I got stuck in a problem where Option component gets re-render itself after clicking each option.
Here is the code
App.js
Main app
export default function App() {
const [questions, setQuestions] = useState([])
const [score, setScore] = useState(0)
// Fetching questions
useEffect(() => {
async function fetchQuestions(){
const res = await fetch("https://opentdb.com/api.php?amount=10&category=18&difficulty=medium")
const data = await res.json()
setQuestions(data.results)
}
fetchQuestions()
}, [])
// Checking answer on clicking any option
const checkAnswer = (option, questionIndex) => {
if(option === questions[questionIndex].correct_answer){
setScore(prevScore => prevScore+=5)
console.log("correct")
}
else{
setScore(prevScore => prevScore-=1)
console.log("incorrect")
}
}
// Main Screen
return (
<QuizScreen questions={questions} score={score} checkAnswer={checkAnswer} />
)
}
QuizScreen.js
Component for rendering quiz screen
export default function QuizScreen(props) {
// Setting questions
const question = props.questions.map((ques, index) => {
// storing options
const opt = []
opt.push(ques.correct_answer)
opt.push(ques.incorrect_answers[0])
ques.incorrect_answers[1] && opt.push(ques.incorrect_answers[1]) // if option 3 available
ques.incorrect_answers[2] && opt.push(ques.incorrect_answers[2]) // if option 4 available
// Arranging options in random order
for(let i=0; i<opt.length; i++){
let j = Math.floor(Math.random() * (i+1))
let temp = opt[i]
opt[i] = opt[j]
opt[j] = temp
}
// Setting options
const option = opt.map(opt => <Option key={nanoid()} option={opt} questionIndex={index} checkAnswer={props.checkAnswer} />)
// Rendering Questions
return (
<div className="ques-container" key={nanoid()}>
<p className="ques-title">{ques.question}</p>
{option}
</div>
)
})
// Main Screen
return (
<div>
<p>{props.score}</p>
{question}
</div>
)
}
Option.js
Component for rendering option buttons
export default function Option(props) {
const [selected, setSelected] = useState(false)
const btnStyle = {
backgroundColor: selected ? "#D6DBF5" : "#FFFFFF"
}
return (
<button
className="ques-option"
onClick={() => {
props.checkAnswer(props.option, props.questionIndex)
setSelected(prevState => !prevState)
}}
style={btnStyle}
>
{props.option}
</button>
)
}
I tried to make Option component separately, but it did not work out
Wrap this around a useMemo
const question = useMemo(() => {
return props.questions.map((ques, index) => {
// storing options
const opt = []
opt.push(ques.correct_answer)
opt.push(ques.incorrect_answers[0])
ques.incorrect_answers[1] && opt.push(ques.incorrect_answers[1]) // if option 3 available
ques.incorrect_answers[2] && opt.push(ques.incorrect_answers[2]) // if option 4 available
// Arranging options in random order
for(let i=0; i<opt.length; i++){
let j = Math.floor(Math.random() * (i+1))
let temp = opt[i]
opt[i] = opt[j]
opt[j] = temp
}
// Setting options
const option = opt.map(opt => <Option key={nanoid()} option={opt} questionIndex={index} checkAnswer={props.checkAnswer} />)
// Rendering Questions
return (
<div className="ques-container" key={nanoid()}>
<p className="ques-title">{ques.question}</p>
{option}
</div>
)
})
}, [props.questions])
I'm building an E-commerce app with React and I stumbled on a problem that React doesn't render the UI based on the initial state when first start the page.
Problem description:
I have a sort state which has the initial state of "latest", based on this Sorting functionality - if sort has a value of "latest" - it will sort and return the newest items first.
But on start or when I refresh the page, the default value and state of sort will still be "latest" but the UI just display the oldest items first.
I have to click on other option and then choose the Latest option again for the sort logic to go through. When I refresh the page, the problem is back.
The logic for sorting other values works fine. In the demo below, you can see I log out the sort current state. On start, the sort value is already "latest".
In the API product.js file, I already sorted the items with mongoose by the field createdAt - 1 but seems like it doesn't apply on the UI?
-> What would be the case here that makes React not render the items based on initial state and how can we fix it?
Below is my code:
ProductList.jsx
const ProductList = () => {
const location = useLocation()
const category = location.pathname.split("/")[2]
const [filters, setFilters] = useState({});
const [sort, setSort] = useState("latest");
// For Filters Bar
const handleFilters = (e) => {
const value = e.target.value
// When choosing default value, show all products:
if (value === "") {
return setFilters([])
} else {
setFilters({
...filters,
[e.target.name]: value,
})
}
}
return (
<Container>
<Navbar />
<Announcement />
<Title>Dresses</Title>
<FilterContainer>
<Filter>
<FilterText>Filter Products: </FilterText>
<Select name="color" onChange={handleFilters}>
<Option value="">All Color</Option>
<Option value="white">White</Option>
<Option value="black">Black</Option>
<Option value="brown">Brown</Option>
<Option value="red">Red</Option>
<Option value="blue">Blue</Option>
<Option value="yellow">Yellow</Option>
<Option value="green">Green</Option>
</Select>
<Select name="size" onChange={handleFilters}>
<Option value="">All Size</Option>
<Option>XS</Option>
<Option>S</Option>
<Option>M</Option>
<Option>L</Option>
<Option>XL</Option>
<Option>36</Option>
<Option>37</Option>
<Option>38</Option>
<Option>39</Option>
<Option>40</Option>
<Option>41</Option>
<Option>42</Option>
<Option>43</Option>
</Select>
</Filter>
<Filter>
<FilterText>Sort Products: </FilterText>
<Select onChange={e => setSort(e.target.value)}>
<Option value="latest">Latest</Option>
<Option value="oldest">Oldest</Option>
<Option value="asc">Price ↑ (Low to High)</Option>
<Option value="desc">Price ↓ (High to Low)</Option>
</Select>
</Filter>
</FilterContainer>
<Products category={category} filters={filters} sort={sort} />
<Newsletter />
<Footer />
</Container>
);
}
Products.jsx
const Products = ({ category, filters, sort }) => {
const [products, setProducts] = useState([])
const [filteredProducts, setFilteredProducts] = useState([])
useEffect(() => {
const getProducts = async () => {
try {
const res = await axios.get( category
? `http://localhost:5000/api/products?category=${category}`
: `http://localhost:5000/api/products`
)
setProducts(res.data)
} catch (err) {
console.log(`Fetch all items failed - ${err}`)
}
}
getProducts()
}, [category])
useEffect(() => {
category && setFilteredProducts(
products.filter(item =>
Object.entries(filters).every(([key, value]) =>
item[key].includes(value)
)
)
)
}, [category, filters, products])
// Sorting:
useEffect(() => {
console.log(sort)
if (sort === "latest") {
setFilteredProducts(prev =>
[...prev].sort((a, b) => b.createdAt.localeCompare(a.createdAt))
)
} else if (sort === "asc") {
setFilteredProducts(prev =>
[...prev].sort((a, b) => a.price - b.price)
)
} else if (sort === "desc") {
setFilteredProducts(prev =>
[...prev].sort((a, b) => b.price - a.price)
)
} else {
setFilteredProducts(prev =>
[...prev].sort((a, b) => a.createdAt.localeCompare(b.createdAt))
)
}
}, [sort])
return (
<Container>
<Title>Popular In Store</Title>
<ProductsWrapper>
{filteredProducts.map(item => (
<Product key={item._id} item={item} />
))}
</ProductsWrapper>
</Container>
);
}
API Route - product.js
const router = require('express').Router()
const { verifyTokenAndAdmin } = require('./verifyToken')
const Product = require('../models/Product')
// .... (Other CRUD)
// GET ALL PRODUCTS
router.get("/", async(req, res) => {
const queryNew = req.query.new
const queryCategory = req.query.category
try {
let products = []
if(queryNew) {
products = await Product.find().sort({ createdAt: -1 }).limit(5)
} else if (queryCategory) {
products = await Product.find({
categories: {
$in: [queryCategory],
},
})
} else {
products = await Product.find()
}
res.status(200).json(products)
} catch(err) {
res.status(500).json(`Cannot fetch all products - ${err}`)
}
})
module.exports = router
Demo:
Explain demo: On start, it renders oldest items first. Have to choose another option and then return to the latest option for it to render. But in the console, the initial state of sort is already "latest" but it doesn't match with the useEffect sorting logic.
Update
According #idembele70's answer, I mistyped the initial state of filters to Array.
I have fixed it and also added a name="sort" on the Sort select.
I also replaced value="latest" with defaultValue="latest" for my Sort select bar. -> This makes the Latest option stop working so I don't think it can be used in this case?
The result is still the same, the UI doesn't render the logic of the Sort bar to display the latest items first.
Code
const ProductList = () => {
const location = useLocation()
const category = location.pathname.split("/")[2]
const [filters, setFilters] = useState({});
const [sort, setSort] = useState("latest");
const handleFilters = (e) => {
const value = e.target.value
// When choosing default value, show all products:
if (value === "") {
setFilters({}) // Changed from array to object
} else {
setFilters({
...filters,
[e.target.name]: value,
})
}
}
...
<Filter>
<FilterText>Sort Products: </FilterText>
<Select name="sort" onChange={e => setSort(e.target.value)} >
<Option defaultValue="latest">Latest</Option>
<Option value="oldest">Oldest</Option>
<Option value="asc">Price ↑ (Low to High)</Option>
<Option value="desc">Price ↓ (High to Low)</Option>
</Select>
</Filter>
You shouldn't put the changed products to the state, as it makes it extra complex to keep it updated and you need to deal with various useEffect cases. Instead it's better to define sorting and filtering functions and apply them at the render time. This way you'll ensure the render result is always up-to-date with the data:
const filterProducts = (products) => {
if (!category) {
return products;
}
return products.filter(item =>
Object.entries(filters).every(([key, value]) =>
item[key].includes(value),
),
);
};
const sortProducts = (products) => {
switch (sort) {
case "asc":
return [...products].sort((a, b) => a.price - b.price);
case "desc":
return [...products].sort((a, b) => b.price - a.price);
case "latest":
default:
return [...products].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
}
};
return (
<Container>
<Title>Popular In Store</Title>
<ProductsWrapper>
{sortProducts(filterProducts(products)).map(item => (
<Product key={item._id} item={item} />
))}
</ProductsWrapper>
</Container>
);
If you put log inside your useEffect hooks, I would assume the execution order is:
[sort]->[category]->[category, filters, products]
The useEffect of [category] has an ajax call and it will definitely take effect after [sort], when it taking effect, it will run your query which is not using req.query.new, so it will just run this branch(i guess you do have category)
else if (queryCategory) {
products = await Product.find({
categories: {
$in: [queryCategory],
},
})
}
, it should default return a ascending list just match 'oldest' option.
So in general, your sort effect is not working at all on initial load because it will always get overwritten by other effects.
So either you make all the query with default descending condition to match the default 'latest' option, or you could trigger sort after any query was performed, option 2 looks better but you need to consider what's your expected behavior of sort when changing other elements(filter, category).
i faced a problem with this code from lama, so first i see a error from you define useState with a initialState as object and in your handleFilters you use an array
const [filters, setFilters] = useState({}); // you use Object there
const handleFilters = (e) => {
const value = e.target.value
products:
if (value === "") {
return setFilters([]) //. you should use object there not array.
} else {
setFilters({
...filters,
[e.target.name]: value,
})
}}
So in React when you use onChange it's recommended to use a value or defaultValue in your html elems
to solve this problem take a look to this CodeSandebox link: https://codesandbox.io/s/determined-hopper-gp9ym9?file=/src/App.js
Let me know if it has solved your problem.
I'm creating a simple app that queries an api that returns a list of books when you search by title. I am adding the option to sort the search results.
When a user searches for a title, eg 'harry potter', the app stores an array of books in state and renders the results.
When a user then selects the option to sort (eg by title), my array of books in state is correctly being sorted and updated, but the old unsorted books are the ones shown.
Notably, if I then select to sort by date, this time I am seeing sorted books being displayed by it's the books sorted by title not date!
And also, at all times, the value of my books array is correctly sorted, but the books being displayed are not (it's essentially the previous state of books which is being rendered).
This is observable if you keep switching between title and sort, the app will update, but the results will be sorted by title when you try to sort by date and vice versa.
It's almost as if when I select a sort option, the dom is rerendering, and then I'm changing the value of books in state, but that isn't then again causing a re-rendering of the dom.
Any ideas what might be going wrong?
sandbox
The code:
export default function App() {
const [input, setInput] = useState("");
const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(false);
const [sort, setSort] = useState("");
const API_URL = `https://openlibrary.org/search.json?title=`;
const sortFn = (books, sortType) => {
setLoading(true);
if (sortType === "title") {
console.log("sorting by title!");
const sortedBooks = books.sort((a, b) => a.title.localeCompare(b.title));
setBooks(sortedBooks);
} else if (sortType === "publishDate") {
console.log("sorting by date, most recent");
const sortedBooks = books.sort((a, b) => {
const dateA = new Date(a.publishDate);
const dateB = new Date(b.publishDate);
return dateB - dateA;
});
console.log("sorted books:", sortedBooks);
setBooks(sortedBooks);
}
setLoading(false);
};
const getBooks = async (queryStr) => {
setLoading(true);
try {
const {
data: { docs }
} = await axios.get(`${API_URL}${queryStr}`);
// console.log(docs);
const slice = docs.slice(0, 10);
const results = slice.map((item) => ({
title: item.title,
author: item.author_name[0],
isbn: item.isbn[0].trim(),
publishDate: item.publish_date[0]
}));
if (sort) {
sortFn(results, sort);
} else {
setBooks(results);
}
setLoading(false);
} catch (err) {
console.log(err);
}
};
useEffect(() => {
if (books && sort) sortFn(books, sort);
}, [sort]);
const changeHandler = (e) => {
setInput(e.target.value);
};
const selectChange = (e) => {
setSort(e.target.value);
};
const submitHandler = (e) => {
e.preventDefault();
if (input) {
//must replace whitespace within string with '+' symbol
const query = input.trim().replace(" ", "+");
getBooks(query);
}
setInput("");
};
console.log("books:", books);
const tiles = books.map((book) => (
<Book
key={book.isbn}
title={book.title}
author={book.author}
publishDate={book.publishDate}
imgURL={`https://covers.openlibrary.org/b/isbn/${book.isbn}-M.jpg`}
/>
));
return (
<div className="App">
<form onSubmit={submitHandler}>
<input type="text" value={input} onChange={changeHandler} />
<button type="submit">Submit</button>
</form>
<select onChange={selectChange}>
<option value="">Sort by</option>
<option value="title">Title(A-Z)</option>
<option value="publishDate">Publish Date</option>
</select>
{loading && <div>Loading</div>}
{!loading && books ? tiles : null}
</div>
);
}
Below, i am rendering <App/> component with children as <Input/> component array. I added few inputs using "add new" button. I am able to add input text components. But, when i am typing value in text, it is not displaying. i am not able to modify object in state array since index is showing as "-1" in setData function. Due to this, value is not showing when we type in text box. Please let me know why state is [] when i am accessing in setData function.
function Input(props)
{
return (
<div>
<label htmlFor='variable'>Name</label>
<input id='variable'
type='text'
value={props.value}
onChange={(e) => props.setData(props.id, e.target.value)} />
</div>
)
}
function App()
{
let [state, setState] = React.useState([])
let [inputs, setInputs] = React.useState([])
let setData = ((id, value) =>
{
console.log(state); // prints []
let index = state.findIndex(ele => ele.key === id);
console.log(index); // prints -1
if (!(index === -1))
{
setState(state =>
{
state[idx]["value"] = value;
})
}
})
let handleAdd = () =>
{
let idx = `${new Date().getTime()}`
let tempState = {
"key": idx,
"value": "",
}
setState(state => [...state, tempState])
let input = <Input key={tempState.key}
value={tempState.value}
id={tempState.key}
setData={setData} />
setInputs(inputs => [...inputs, input])
}
return (
<div>
<button onClick={handleAdd}>add new</button>
<div>
{inputs}
</div>
</div>
)
}
When you create an Input component inside handleAdd, it creates a closure and as a result setData gets the state that existed when the component was created, missing the newly added state.
In general, creating components and saving them to state is not a good approach. Instead it's better to only save the data onto state and render the components based on it.
Here's one way to do this, note how much simpler the component and its logic are.
function App() {
let [state, setState] = React.useState([]);
let setData = (id, value) => {
const newState = state.map((st) => {
if (st.key === id) {
st.value = value;
}
return st;
});
setState(newState);
};
const addInput = () => {
const idx = `${new Date().getTime()}`;
setState([...state, { key: idx, value: '' }]);
};
return (
<div>
<button onClick={addInput}>add new</button>
<div>
{state.map((st) => (
<Input value={st.value} key={st.key} setData={setData} id={st.key} />
))}
</div>
</div>
);
}
I'm doing this fullstack course to learn about web dev: https://fullstackopen.com/en/part2/getting_data_from_server
And I have a problem with section 2.13*.
I am able to display a list of the countries after filtering with the button. Pressing the button returns the correct values from the countries arrays as seen with the console.log(country), but it doesn't to the screen.
My guess is that I can't return a div item within another item, but I am pretty sure that works in normal cases, so the fact that I'm returning the item to a different return statement might be the issue?
How can I fix this? I know my code is messy and a refactor might make things simpler, but it is currently beyond me right now since I find it easier to refactor working code.
In the DisplayCountries component, I've tried apply a map to countries that fit the filter input and prints it into a div item. Now when I add a button beside it, it displays correctly, but pressing it does not yield what I expect.
Is the correct approach here to use a useState with the button, so that each button click will rerender the screen? How would I go about doing this if so?
After pressing the button, the detailed information of the country should display such as in 2.12* from the linked website.
import { useState, useEffect } from 'react'
import axios from 'axios'
//feed array of countries
const printLanguages = (languages) => {
// console.log('map', languages.map(language => language.name))
return languages.map(language => <li key={language.name}>{language.name}</li>)
}
const displayCountryView = (country) => {
console.log(country)
return (
<div>
<h1>{country.name}</h1>
<p>capital {country.capital}</p>
<p>population {country.population}</p>
<h2>languages</h2>
<ul>
{printLanguages(country.languages)}
</ul>
<img src={country.flag} height="100" width="100"></img>
</div>
)
}
const DisplayCountries = ({ countries, searchValue }) => {
const displayFilter = filteredCountries(countries, searchValue)
// console.log('current search', searchValue)
if (displayFilter.length >= 10) {
return <p>Too many matches, specify another filter</p>
} else if (isFiltered(searchValue)) {
if (displayFilter.length > 1 && displayFilter.length < 10) {
console.log('new level')
return displayFilter.map(country => <div key={country.name}>{country.name}{showButton(country)}</div>)
} else if (displayFilter.length === 1) {
// console.log('suh')
// return displayFilter.map(country => <p key={country.name}>{country.name}</p>)
const country = displayFilter
return displayCountryView(country[0])
// console.log(country)
// console.log('country.name', country[0])
// console.log(country[0].languages)
// console.log(printLanguages(country[0].languages))
// return (
// <div>
// <h1>{country[0].name}</h1>
// <p>capital {country[0].capital}</p>
// <p>population {country[0].population}</p>
// <h2>languages</h2>
// <ul>
// {printLanguages(country[0].languages)}
// </ul>
// <img src={country[0].flag} height="100" width="100"></img>
// </div>
// )
}
} else {
return <p>empty</p>
}
}
const showButton = (country) => {
return <button type="button" onClick={() => displayCountryView(country)}>show</button>
}
const filteredCountries = (countries, searchValue) => {
const showCountries = (!isFiltered(searchValue))
? [{ name: "hi" }]
: countries.filter(country => country.name.toLowerCase().includes(searchValue.toLowerCase()))
// const countryMap = countries.map(country => country.name.toLowerCase())
// console.log(countryMap)
// return countryMap
return showCountries
}
function isFiltered(value) {
if (value === '') {
return false
} else {
return true
}
}
const Filter = ({ search, onChange }) => {
return (
<form >
<div>
find countries <input value={search} onChange={onChange} />
</div>
</form>
)
}
const App = () => {
const [countries, setCountries] = useState([])
const [search, setNewSearch] = useState('')
const [showCountry, setShowCountry] = useState('false')
useEffect(() => {
// console.log('effect')
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
// console.log('promise fulfilled')
setCountries(response.data)
})
}, [])
// const countryNames = countries.map(country => country.name)
// console.log('name', countryNames)
const handleSearchChange = (event) => {
setNewSearch(event.target.value)
}
// const fil = countries.filter(country => country.name==='Afg')
// console.log(countries[0])
// console.log('filtered:',fil)
// console.log(countries[0])
// console.log('render', countries.length, 'persons')
return (
<div>
<Filter search={search} onChange={handleSearchChange} />
<form>
<div>
<DisplayCountries countries={countries} searchValue={search} />
</div>
</form>
</div>
)
}
export default App;