Add item to end of array, then call function - React - javascript

I wasn't sure how to title this correctly. I have an array of items, where I am mapping through them and creating a button for each item. when each button (which represents a category) is clicked, it loads the posts in that category. I am adding an extra item (which will also be a button) to the end of the array that will be a "view all" button, but it will call a different function. So far this component is like:
const Posts = ({ state }) => {
const [categories, setCategories] = useState([]);
const [categoryId, setCategoryId] = useState();
const [page, setPage] = useState(1);
const [posts, setPosts] = useState([]);
const [allPosts, setAllPosts] = useState([]);
useEffect(() => {
fetch(state.source.api + "/wp/v2/categories")
.then(response => response.json())
.then(data => {
setCategories(data);
})
}, []);
console.log(categories);
useEffect(() => {
if (categoryId) {
fetch(state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5")
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}
}, [categoryId]);
useEffect(() => {
if (!categoryId) {
return;
}
let url = state.source.api + "/wp/v2/posts?categories=" + categoryId + "&per_page=5";
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setPosts([...posts, ...data]);
});
}, [categoryId, page]);
useEffect(() => {
let url = state.source.api + "/wp/v2/posts?per_page=5";
if (page > 1) {
url += `&page=${page}`;
}
fetch(url)
.then((response) => response.json())
.then((data) => {
setAllPosts([...allPosts, ...data]);
});
}, [page]);
const allCategories = categories.map((category, i) => (category))
allCategories.push("View All");
console.log(allCategories);
return (
<>
{allCategories.length > 0 ? (
allCategories.map((category, i) => {
return (
<>
<button className="btn" key={i} onClick={() => {
setPage(1);
setPosts([]);
setCategoryId(category.id);
}}>{category.name}</button>
{(category === "View All") && (<button>View all</button>)}
</>
)
})
) : (
<p>Loading...</p>
)
}
<div>
{posts.length === 0 ? (
<>
{allPosts.map((generalPost, i) => {
return (
<li key={i}>{generalPost.title.rendered}</li>
)
})}
<button onClick={() => { setPage(page + 1); }}>Load more</button>
</>
) : (
<>
<ol>
{posts.map((post, i) => {
// console.log(post.id);
return (
<li key={i}>{post.title.rendered}</li>
)
})}
</ol>
<button onClick={() => { setPage(page + 1); }}>Load more</button>
</>
)}
</div>
</>
)
}
I was able to get the "view all" button to be added to the end, but there seems to be an extra empty button before the "view all" button. I am not sure how that is getting in there. It's displaying like:
[Books] [Movies] [Songs] [ ] [View all]
Is there something wrong with the way I am adding the "view all" button to the array here?

In your original code, you are always rendering a <button class="btn">...</button> + conditional check to render <button>View all</button>:
allCategories.map((category, i) => {
return (
<>
<button className="btn" key={i} onClick={() => {
setPage(1);
setPosts([]);
setCategoryId(category.id);
}}>{category.name}</button>
{(category === "View All") && (<button>View all</button>)}
</>
)
})
Therefore, when category === "View All" is true, it also renders a <button class="btn"> element with empty content because in that case, category.name is undefined.
What you need to do is to make a if-else statement or ternary expression to render only one of them:
allCategories.map((category, i) => {
return (
{(category === "View All") ? (
<button>View all</button>
) : (
<button className="btn" key={i} onClick={() => {
setPage(1);
setPosts([]);
setCategoryId(category.id);
}}>{category.name}</button>
)
)
})

Related

Why does this function reduce to one rather than decrease one?

I have a button that trigger decreaseQuantity function and I have a state,checkoutList comes from context API. When I click that button, its directly decrease the quantity to 1, and what I want is only decrease by one. Why it's not decreasing one by one?
const { checkoutList, setCheckoutList } = useContext(AppContext);
const [uniqueCheckoutList, setUniqueCheckoutList] = useState([]);
const decreaseQuantity = (item) => {
let updatedList = [...uniqueCheckoutList];
let productIndex = updatedList.findIndex((p) => p.id === item.id);
updatedList[productIndex].quantity -= 1;
if (updatedList[productIndex].quantity === 0) {
updatedList = updatedList.filter((p) => p.id !== item.id);
}
setUniqueCheckoutList(updatedList);
setCheckoutList(updatedList);
localStorage.setItem("checkoutList", JSON.stringify(updatedList));
};
Edit: I have a useEffect hook that updates uniqueCheckoutList if the product has the same id. Add quantity property to it so it will just display once. This way I can show the quantity of that product next to the buttons in a span.
useEffect(() => {
let updatedList = [];
checkoutList.forEach((item) => {
let productIndex = updatedList.findIndex((p) => p.id === item.id);
if (productIndex === -1) {
updatedList.push({ ...item, quantity: 1 });
} else {
updatedList[productIndex].quantity += 1;
}
});
setUniqueCheckoutList(updatedList);
}, [checkoutList]);
Here I call the decreaseQuantity function.
<div className="checkout">
{uniqueCheckoutList.length === 0 ? (
<div>THERE IS NO ITEM IN YOUR BAG!</div>
) : (
uniqueCheckoutList.map((item) => {
return (
<div className="container" key={item.id}>
<div>
<img src={item.images[0]} alt={item.title} />
</div>
<div className="texts">
<h1>{item.title} </h1>
<p>{item.description} </p>
<button onClick={() => increaseQuantity(item)}>+</button>
<span>{item.quantity}</span>
<button onClick={() => decreaseQuantity(item)}>-</button>
</div>
</div>
);
})
)}
</div>

How to differentiates buttons and behaviour in a function

I have a list of items; Each item has a "Details" button beside it.
When the "Details" button it is pressed, I would like to show under the list the details of that element.
So far so good. I managed to do it. Even if it doesn't seem the best way. Now the problem is:
When I press, for the first time, a button, it shows the details of that item. But when I press again, regardless of the button, it close it. This is because I don't understand how to differentiate them. So for closing the "Details" I can just hit any button, and I don't want it.
My desired behavior would be (pseudo code):
if details_not_showing:
show_details(id==button_pressed)
else:
if details_showing == details_from_button_pressed
close_details()
else
show_details(id==button_pressed)
Hoping this make some sense, I leave you with my terrible code under this.
Imports
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
Function
function MonthArticles() {
const { user_id, year, month } = useParams();
const url =
"http://127.0.0.1:8000/api/" + user_id + "/posts/" + year + "/" + month;
const url_retrieve_author = "http://127.0.0.1:8000/api/retrieve-author";
const formdata = new FormData();
formdata.append("id", user_id);
const requestOptions = {
method: "POST",
body: formdata,
};
const [article, setArticle] = useState([]);
useEffect(() => {
fetch(url, {
method: "GET",
})
.then((res) => res.json())
.then((data) => {
setArticle(data);
});
}, []);
const [author, setAuthor] = useState([]);
useEffect(() => {
fetch(url_retrieve_author, requestOptions)
.then((res) => res.json())
.then((data) => {
setAuthor(data);
});
}, []);
const [details, setDetails] = useState(false);
const [articleId, setArticleId] = useState();
return (
<div>
<h2>
Articles writte in {year}, {month} - By{" "}
{author.map((author) => (
<div key={author.id}>{author.last_name}</div>
))}
</h2>
<h4>List of articles below:</h4>
<ul>
{article.map((article) => (
<div key={article.id}>
<li key={article.id}>
{article.title}{" "}
<button
id={article.id}
type='button'
onClick={() => [
setDetails((currentDetails) => !currentDetails),
setArticleId(article.id),
]}
>
Details
</button>
</li>
</div>
))}
</ul>
{details ? (
<div>
<h3>Showing details</h3>
{article
.filter((a) => a.id === articleId)
.map((filteredArticle) => (
<div>
<h4>Post created in: {filteredArticle.date_created}</h4>
<p>{filteredArticle.text}</p>
</div>
))}
</div>
) : null}
</div>
);
}
Thanks in advance
The main issue in your code is that details is toggled from true to false on any button click.
The solution with minimal changes would check the current articleId before toggling the details value.
onClick={() => [
setDetails((currentDetails) => !currentDetails),
setArticleId(article.id),
]}
Should be changed into:
onClick={() => {
// `details` is set to `true` if a new article is clicked.
// When pressing the button of the same article multiple
// times, the value is toggled.
setDetails(article.id !== articleId || !details);
setArticleId(article.id);
}}
There is also a solution that only uses a single state, but requires more code to change.
// `false` if no details, otherwise an article id
const [detailsId, setDetailId] = useState(false);
// Lookup the article of which details must be shown.
// Performance can be improved by using the useMemo hook.
const details = article.find(article => article.id === detailsId);
This essentially uses detailsId as a boolean. It contains false if no details must be shown. But instead of true we use the article id, to store both the fact that details must be shown, and the id of the article that must be shown.
details looks up the article based on the detailsId. This variable (not a state) helps simplify the view later on.
Your onClick handler then becomes:
onClick={() => {
// Set `detailId` to `false` if is the same as the current article id,
// otherwise set the current article id.
setDetailId(article.id !== detailId && article.id);
}}
Finally you need to update the view:
{details && (
<div>
<h3>Showing details</h3>
<div>
<h4>Post created in: {details.date_created}</h4>
<p>{details.text}</p>
</div>
</div>
)}
Just check on click if the id doesn't change then return null to hide the details, if not set the new article id to show the details.
function MonthArticles() {
const { user_id, year, month } = useParams();
const url =
"http://127.0.0.1:8000/api/" + user_id + "/posts/" + year + "/" + month;
const url_retrieve_author = "http://127.0.0.1:8000/api/retrieve-author";
const formdata = new FormData();
formdata.append("id", user_id);
const requestOptions = {
method: "POST",
body: formdata,
};
const [article, setArticle] = useState([]);
useEffect(() => {
fetch(url, {
method: "GET",
})
.then((res) => res.json())
.then((data) => {
setArticle(data);
});
}, []);
const [author, setAuthor] = useState([]);
useEffect(() => {
fetch(url_retrieve_author, requestOptions)
.then((res) => res.json())
.then((data) => {
setAuthor(data);
});
}, []);
const [details, setDetails] = useState(false);
const [articleId, setArticleId] = useState();
return (
<div>
<h2>
Articles writte in {year}, {month} - By{" "}
{author.map((author) => (
<div key={author.id}>{author.last_name}</div>
))}
</h2>
<h4>List of articles below:</h4>
<ul>
{article.map((article) => (
<div key={article.id}>
<li key={article.id}>
{article.title}{" "}
<button
id={article.id}
type='button'
onClick={() => [
setDetails((currentDetails) => !currentDetails),
setArticleId(prev => {
if (prev === articleId) return null
return article.id
}),
]}
>
Details
</button>
</li>
</div>
))}
</ul>
{details ? (
<div>
<h3>Showing details</h3>
{article
.filter((a) => a.id === articleId)
.map((filteredArticle) => (
<div>
<h4>Post created in: {filteredArticle.date_created}</h4>
<p>{filteredArticle.text}</p>
</div>
))}
</div>
) : null}
</div>
);
}
Currently, in your buttons onClick functions you do this:
onClick={() => [
setDetails((currentDetails) => !currentDetails),
setArticleId(article.id),
]}
The first line toggles whether or not the details secions is visible. All buttons currently toggle the details visibility. What you want is to only do this when it is the button corresponding to the currently displayed details.
onClick={() => {
//If no details are visible, show them
if(!details) setDetails(true);
//If details are visible, and this is the corresponding button, hide them
else if(article.id == articleId) setDetails(false);
setArticleId(article.id);
}}

Get an Error that .filter is not a function

I have tried to create an autocomplete suggestion box from an Thailand's province database URL.
This is my source code. I export this to App.js in src directory
import React, { useEffect, useState, useRef } from "react";
const Test = () => {
const [display, setDisplay] = useState(false);
const [singleProvince, setSingleProvince] = useState([]);
const [singleProvinceData, setSingleProvinceData] = useState([]);
const [search, setSearch] = useState("");
const wrapperRef = useRef(null);
const province_dataBase_url = 'https://raw.githubusercontent.com/earthchie/jquery.Thailand.js/master/jquery.Thailand.js/database/raw_database/raw_database.json'
useEffect(() => {
const promises = new Array(20).fill(fetch(province_dataBase_url)
.then((res) => {
return res.json().then((data) => {
const createSingleProvince = data.filter( (each) => {
if (false == (singleProvince.includes(each.province))) {
setSingleProvince(singleProvince.push(each.province))
setSingleProvinceData(singleProvinceData.push(each))
}
})
return data;
}).catch((err) => {
console.log(err);
})
}))
}, [])
useEffect(() => {
window.addEventListener("mousedown", handleClickOutside);
return () => {
window.removeEventListener("mousedown", handleClickOutside);
};
});
const handleClickOutside = event => {
const { current: wrap } = wrapperRef;
if (wrap && !wrap.contains(event.target)) {
setDisplay(false);
}
};
const updateProvince = inputProvince => {
setSearch(inputProvince);
setDisplay(false);
};
return (
<div ref={wrapperRef} className="flex-container flex-column pos-rel">
<input
id="auto"
onClick={() => setDisplay(!display)}
placeholder="Type to search"
value={search}
onChange={event => setSearch(event.target.value)}
/>
{display && (
<div className="autoContainer">
{ singleProvinceData
.filter( ({province}) => province.indexOf(search.toLowerCase()) > -1)
.map( (each,i) => {
return (
<div
onClick={() => updateProvince(each.province)}
className="singleProvinceData"
key={i}
tabIndex="0"
>
<span>{each.province}</span>
</div>
)
})}
</div>
)}
</div>
);
}
export default Test
When click on an input box, the console says "TypeError: singleProvinceData.filter is not a function"
enter image description here
I cannot find out what's wrong with my code
The issue is with the "singleProvinceData" state is not set correctly.
you cannot push data directly into the state.
useEffect(() => {
const promises = new Array(20).fill(fetch(province_dataBase_url)
.then((res) => {
return res.json().then((data) => {
const shallowSingleProvinceList = [];
const shallowSingleProvinceDataList = [];
const createSingleProvince = data.filter( (each) => {
if (false == (singleProvince.includes(each.province))) {
shallowSingleProvinceList.push(each.province)
shallowSingleProvinceDataList.push(each)
}
})
setSingleProvince(shallowSingleProvinceList)
setSingleProvinceData(shallowSingleProvinceDataList)
return data;
}).catch((err) => {
console.log(err);
})
}))
}, [])
You can show the data conditionally
{display && (
<div className="autoContainer">
{ singleProvinceData && singleProvinceData
.filter( ({province}) => province.indexOf(search.toLowerCase()) > -1)
.map( (each,i) => {
return (
<div
onClick={() => updateProvince(each.province)}
className="singleProvinceData"
key={i}
tabIndex="0"
>
<span>{each.province}</span>
</div>
)
})}
</div>
)}

Loop in React JSX

Instead of using fileList.map(), I want to put it in for loop and setTimeout(500) for every single loop. How can I do it ?
const SortableList = SortableContainer(({ fileList }) => (
<div>
{
fileList.map((file, index) => (
<SortableItem key={`item-${index}`} index={index} value={file} />
))
}
</div>
))
A very simple way is to create a new array which should contain the items which are currently visible.
Then do a simple setTimeout which will call a recursive function every 500ms that will add one element to the visible array.
const SortableList = SortableContainer(({ fileList }) => {
const [visibleItems, setVisibleItems] = useState([]);
useEffect(() => {
function displayNext(next) {
setVisibleItems(prevItems => [...prevItems, fileList[next]]);
if (++next < fileList.length) {
setTimeout(() => {
displayNext(next);
}, 500);
}
}
displayNext(0);
}, []);
return (
<div>
{
visibleItems.map((file, index) => (
<SortableItem key={`item-${index}`} index={index} value={file} />
))
}
</div>
)});
Let me know if works!
You can do something like this:
import React, {useState, useEffect} from 'react';
const ITEMS_TO_ADD_IN_EACH_TIMEOUT = 50
const SortableList = SortableContainer(({ fileList }) => {
const [visibleItems, setVisibleItems] = useState([])
useEffect(() => {
const timer = setTimeout(() => {
setVisibleItems([fileList.slice(0, visibleItems.length + ITEMS_TO_ADD_IN_EACH_TIMEOUT)])
}, 500);
return () => clearTimeout(timer);
})
return (
<div>
{
visibleItems.map((file, index) => (
<SortableItem key={`item-${index}`} index={index} value={file} />
))
}
</div>
))
}

React - combine functions

How could I combine those two functions (handleSelectAll and handleSelectNone)? Toggle (on/off) wouldn't work here as in most cases some options would be 'checked' so you won't know whether to toggle it ALL or NONE so there need to be 2 separate buttons (at least that's what I think). What I was thinking was that the function can be shared
const handleSelectAll = () => {
setCategories(oldCats => oldCats.map(category => {
return {
...category,
selected: true
}
}))
}
const handleSelectNone = () => {
setCategories(oldCats => oldCats.map(category => {
return {
...category,
selected: false
}
}))
}
and then the buttons in a component:
const Categories = (props) => {
return(
<div className="categories">
<h2>Categories</h2>
<form className="check-form">
{props.categories.map(category => (
<Category key={category.id} category={category} {...props} />
))}
</form>
<button onClick={props.handleSelectAll}>
Select All
</button>
<button onClick={props.handleSelectNone}>
Select None
</button>
</div>
);
};
Would there be a way of just defining one function for both buttons?
I usually do like this, keep things simple and legible, nothing too fancy:
const handleSelect = (selected = false) => {
setCategories((oldCats) =>
oldCats.map((category) => {
return {
...category,
selected,
}
})
)
}
const handleSelectAll = () => {
return handleSelect(true)
}
const handleSelectNone = () => {
return handleSelect()
}
(the render part continues as is)
Doing like so avoids you creating that extra template function passing an argument and creating a new function on every render
Yes. You can wrap the call for props.handleSelectAll and props.handleSelectNone with a function and pass the new value as argument:
const Categories = (props) => {
return(
<div className="categories">
<h2>Categories</h2>
<form className="check-form">
{props.categories.map(category => (
<Category key={category.id} category={category} {...props} />
))}
</form>
<button onClick={()=>props.handleSelect(true)}>
Select All
</button>
<button onClick={()=>props.handleSelect(false)}>
Select None
</button>
</div>
);
};
[ ()=>props.handleSelect(true) is an arrow function that calls handleSelect on click ]
And the new function will be:
const handleSelect= (newValue) => {
setCategories(oldCats => oldCats.map(category => {
return {
...category,
selected: newValue
}
}))
}
You can make it one function using a parameter, such as
const handleChangeAll = (selected) => {
setCategories(oldCats => oldCats.map(category => {
return {
...category,
selected: selected
}
}))
}
Then you can call this function with a parameter in each button like this:
const Categories = (props) => {
return(
<div className="categories">
<h2>Categories</h2>
<form className="check-form">
{props.categories.map(category => (
<Category key={category.id} category={category} {...props} />
))}
</form>
<button onClick={() => props.handleChangeAll(true)}>
Select All
</button>
<button onClick={() => props.handleChangeAll(false)}>
Select None
</button>
</div>
);
};
selected: !category.selected
const handleSelectNone = () => {
setCategories(oldCats => oldCats.map(category => {
return {
...category,
selected: !category.selected
}
}))
}

Categories

Resources