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);
}}
Related
I have 2 components, the Favorites component, makes a request to the api and maps the data to Card.
I also have a BtnFav button, which receives an individual item, and renders a full or empty heart according to a boolean.
Clicking on the BtnFav render removes a certain item from the favorites database.
What I need is that in the Favorites component, when I click on the BtnFavs component, the useEffect of Favorites is triggered again to bring the updated favorites.
How can i solve this? I have partially solved it with a global context(favoritesUser), but is there any other neater alternative?
The data flow for now would be something like this:
Favorites component fetches all the complete data and passes it to the Card component, the Card component passes individual data to the BtnFavs component.
Favorites Component:
const fetchWines = async () => {
try {
const vinos = await axios.get(`/api/favoritos/${id}`);
const arrVinos = vinos.data.map((vino) => {
return vino.product;
});
setVinosFavs(arrVinos);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
fetchWines();
}, [favoritesUser]);
return (
<div>
<h1>Mis favoritos</h1>
<Card listWines={vinosFavs} />
</div>
);
BtnFavs:
const handleClickFav = (e) => {
if (!boton) {
axios.post("/api/favoritos/add", { userId, productId }).then((data) => {
setBoton(true);
return;
});
}
axios.put("/api/favoritos/delete ", { userId, productId }).then((data) => {
setBoton(false);
setFavoritesUser(data);
});
};
What I need is that in the Favorites component, when I click on the BtnFavs component, the useEffect of Favorites is triggered again to bring the updated favorites.
How can i solve this? I have partially solved it with a global context(favoritesUser), but is there any other neater alternative?
The pattern you want is called a callback function, just like the onClick of a button. You pass a function to your components that get executed given a condition. If you want fetchWines to be called again, then just pass the function in as a prop.
Favorites Component:
<Card listWines={vinosFavs} refresh={fetchWines} />
Card Component
<BtnFavs onDelete={refresh} ... />
BtnFavs Component
onDelete();
You can name it whatever you want, but generally callbacks will be named like on<condition>.
If you really wanted useEffect to be triggered then you would pass a setState function that set one of the dependencies, but I don't see a point in this case.
I will share code, because this problem its normal for me, i really want to learn and improve that.
const Favorites = () => {
const { favoritesUser } = useFavoritesContext();
const user = useSelector((state) => state.user);
const id = user.id;
const [vinosFavs, setVinosFavs] = useState([]);
const fetchWines = async () => {
try {
const vinos = await axios.get(`/api/favoritos/${id}`);
const arrVinos = vinos.data.map((vino) => {
return vino.product;
});
setVinosFavs(arrVinos);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
fetchWines();
}, [favoritesUser]);
return (
<div>
<h1>My favorits</h1>
<Grid listVinos={vinosFavs} />
</div>
);
};
export default Favorites
Grid
export default function Grid({ listVinos }) {
return (
<div>
<ul className={styles.layoutDeVinos}>
{listVinos?.map((element) => {
return <WineCard key={element.id} vino={element} />;
})}
</ul>
</div>
);
}
Card
export default function WineCard({ vino }) {
return (
<>
<div>
<Link to={`/products/${vino.id}`}>
<li>
<div className={styles.card}>
<div
className={styles.img1}
style={{
backgroundImage: `url(${vino.images})`,
}}
></div>
<div className={styles.text}>{vino.descripcion}</div>
<div className={styles.catagory}>
{vino.nombre}
<i className="fas fa-film"></i>
</div>
<div className={styles.views}>
{vino.bodega}
<i className="far fa-eye"></i>{" "}
</div>
</div>
</li>
</Link>
<div className="botonesUsuario">
<BtnFavs vino={vino} />
</div>
</div>
</>
);
}
BTN FAVS
export default function BtnFavs({ vino }) {
const { setFavoritesUser } = useFavoritesContext();
const [boton, setBoton] = useState(false);
const user = useSelector((state) => state.user);
const userId = user.id;
const productId = vino.id;
useEffect(() => {
axios
.post("/api/favoritos/verify", { userId, productId })
.then((bool) => setBoton(bool.data));
}, []);
const handleClickFav = (e) => {
if (!boton) {
axios.post("/api/favoritos/add", { userId, productId }).then((data) => {
setBoton(true);
return;
});
}
axios.put("/api/favoritos/delete ", { userId, productId }).then((data) => {
setBoton(false);
setFavoritesUser(data);
});
};
return (
<>
{!user.id ? (
<div></div>
) : boton ? (
<span
class="favIcons material-symbols-rounded"
onClick={handleClickFav}
>
favorite
</span>
) : (
<span className="material-symbols-rounded" onClick={handleClickFav}>
favorite
</span>
)}
</>
);
}
i am new to react and i am trying to mark one poem from a db.json file as read but clicking one affects all of the articles. how do i select only one at a time.
import React , {useState, useEffect} from "react";
function Poem() {
const [poemShow, setPoemShow] = useState([])
const PoemData = "http://localhost:8004/poems"
// to load the Poem
useEffect(() => {
fetch(PoemData)
.then((res) => res.json())
.then((data) => {setPoemShow(data)})
// .then((data) => console.log(data))
}, []);
const [unread, setUnread] = useState(false)
function markRead(event) {
console.log({unread, setUnread})
setUnread(!unread)
// console.log( `${item.id} marked`)
}
return (
<div>
{poemShow.map((item) => (
<div key={item.id}>
<h3>{item.title}</h3>
<p>{item.content}</p>
<strong>{item.author}</strong><br/>
{/* <button onClick={markRead}>{unread ? "Mark as unread" : "Mark as read" }</button> */}
<button onClick={(event) => markRead(event.target.id)}>{unread ? "Mark as unread" : "Mark as read" }</button>
<hr/>
</div>
))}
</div>
);
}
export default Poem;
You could store an object instead, that maps an itemId to a boolean (true or false), meaning if the item with the given id is read or not:
const [readState, setReadState] = useState({})
Have an event handler like this (it's a function creating a function, that will be used as the event handler)
const toggleReadState = (itemId) => () => setReadState({ ...redState, [itemId]: !redState[itemId] })
And then render the list like this:
poemShow.map((item) => (
<div key={item.id}>
{/* ... */}
<button onClick={toggleReadState(item.id)}>{redState[item.id] ? 'Mark as unread' : 'Mark as read'}</button>
</div>
))
When you are clicking on button, you are instantly changing all undread values from true to false and vice versa:
setUnread(!unread)
This might do the trick, because you need to filter out entry selected entry.
setUnread(unread.filter(el => el.id === event))
I have two React components: ul list and each li item, which is a flash-card. In each item there are two strings: searching word and its translation (api.source, api.target) and also checkbox. After clicking on the checkbox and later on the button (CardList.js) I would like to get my data and send it to my backend. Can anybody please tell me how to get these two strings and pass them into addFlashCards function? Right now I have hardcoded it as dataSet
const CardsList = ({ fetchedApi }) => {
const addFlashCards = () => {
const dataSet = [
{
apiSource: "sampleDataCheckbox",
apiTarget: "sampleDataCheckbox2",
},
{
apiSource: "sampleDataCheckbox",
apiTarget: "sampleDataCheckbox",
},
];
const url = "/api/saveToDB";
axios
.post(url, { data: dataSet, user: "SOME USER" })
.then((res) => {
const fetchedData = res.data;
console.log(fetchedData);
})
.catch((err) => console.error(err));
};
return (
<div className="cards-list">
<button onClick={addFlashCards}>Add</button>
<ul className="cards-list__list">
{fetchedApi.map((api) => {
return <CardItem api={api} />;
})}
</ul>
</div>
);
};
export default CardsList;
const CardItem = ({ api }) => {
const [isChosenCard, setIsChosenCard] = useState(false);
const clickedCheckbox = () => {
setIsChosenCard(!isChosenCard);
};
return (
<>
<li className="card-item">
<input
className="fiszkiBox"
type="checkbox"
onClick={clickedCheckbox}
/>
<div className="flip-card">
<div className="flip-card-inner">
<div className="flip-card-front">
<p id="api-source">{api.source}</p>
</div>
<div className="flip-card-back">
<p className="api-target">{api.target}</p>
</div>
</div>
</div>
</li>
</>
);
};
export default CardItem;
Many thanks!
So as summary you have a list which you render and you want to select items which would be sent to backend. In this case you can add a empty array to your CardList by using useState hook which will contain the selected items.
const CardsList = ({ fetchedApi }) => {
const [selectedItems, setSelectedItems] = useState([]);
const addFlashCards = () => {
// Here you can do some kind of check with mapping the selectedItems Array, as you need
const url = "/api/saveToDB";
axios
.post(url, { data: selectedItems, user: "SOME USER" })
.then((res) => {
const fetchedData = res.data;
console.log(fetchedData);
})
.catch((err) => console.error(err));
};
function selectItem(item){
// So below, we do it to make component render by creating new array instead of mutating the current array. Actually try to play with code, you can even remove these and mutate current and set.
let selectedItemsArray = [...selectedItems];
// Here below, we check if selectedItem already exists in selectedItemsArray, if it exists we would like to remove it right? if it does not exist, we just add it to array by pushing it.
let itemCheck = selectedItemsArray.find((arrayItem) => arrayItem.source === item.source);
if (itemCheck) {
// Here if selected item was in array, we just remove it by filtering the array
selectedItemsArray = selectedItemsArray.filter((arrayItem) => arrayItem.source !== item.source)
}
else{
// Here selected item was not in our array so we just push it
selectedItemsArray.push(item)
}
setSelectedItems(selectedItemsArray);
}
return (
<div className="cards-list">
<button onClick={addFlashCards}>Add</button>
<ul className="cards-list__list">
{fetchedApi.map((api, index) => {
return <CardItem api={api} onItemSelected={item=> selectItem(item)} />;
})}
</ul>
</div>
);
};
const CardItem = ({ api, onItemSelected }) => {
return (
<>
<li className="card-item">
<input
className="fiszkiBox"
type="checkbox"
onClick={() => onItemSelected({
source: api.source,
target: api.target
})}
/>
<div className="flip-card">
<div className="flip-card-inner">
<div className="flip-card-front">
<p id="api-source">{api.source}</p>
</div>
<div className="flip-card-back">
<p className="api-target">{api.target}</p>
</div>
</div>
</div>
</li>
</>
);
};
These codes should work, hope you got the logic also. This approach would be logical in your case.
i have a backend api build on nodejs. in the code the api return some categories array.
i ran .map() function on the array to display the array in the JSX.
after that i added a checkBox to each object inside the array.
so what im trying to do is if the checkbox is true its will added another h1 Element (JSX).
Only to the object i clicked on the checkbox.
i tryied to add "status" props and make it false or true and then catch it with onClick e.target.status?
"YES" : "NO"
also, i tried to added checkBox useState and make it true or false . and its work. but not as i want
its display Yes or No to the all objects and not only to the on i clicked on.
const Category = ({ history }) => {
const dispatch = useDispatch()
const user = useSelector((state) => state.waiter)
const selectedCategory = useSelector((state) => state.selectedTable)
const [currectCategory, setCurrectCategory] = useState([])
const [categoryName, setCategoryName] = useState("")
const [categoryIMG, setCategoryIMG] = useState("not found")
const [checkBox, setCheckBox] = useState("false")
useEffect(() => {
if (!user.name) {
history.push('/login')
} else {
(async () => {
const res = await fetch('http://localhost:1000/categories/' + selectedCategory)
const data = await res.json()
setCurrectCategory(data.CurrectCountry.subcategories.map(sc => sc.name))
setCategoryName(data.CurrectCountry.name)
setCategoryIMG(data.CurrectCountry.img)
})()
}
}, [user])
const goBack = () => {
dispatch({
type: 'ALL_CATEGORIES'
})
history.push('/login')
}
const handleCheck = (e) => {
setCheckBox(e.target.checked.toString())
console.log(e.target.checked)
}
return (
<>
<Button className="LogButton" color="secondary" onClick={goBack}>back</Button>
<div className="SingleCategory">
<h1>{categoryName}</h1>
<ListGroup>
{currectCategory.map(category => {
return (
<Row className="Col-padd" key={category}>
<div>
<InputGroup className="mb-3">
<b className="ItemName"> {category} </b>
<img src={categoryIMG} height="100" width="100" ></img>
<FormCheck id={category} className="Checkbox" onChange={handleCheck}></FormCheck>
{checkBox == "true" ? <b>yes</b> : <b>No</b>}
</InputGroup>
</div>
</Row>
)
})}
</ListGroup>
</div>
</>
)
}
Thanks for help !!
You are only creating a single value for the checkbox. If you want to show for all the checkbox, if you have to track the value for each checkbox shown below,
const [checkBox, setCheckBox] = useState({}); // checkBoxName: value
const handleCheck = (e) => {
setCheckBox((prev) => {...prev, [e.target.name]: e.target.value};
}
{!!checkBox['name'] === true ? <b>yes</b> : <b>No</b>}
//change the attribute according to your implementation.
Your problem is that you're just creating a single value for the checkbox and not separating the individual checkboxes. You could solve this in many different ways, but you would be well served by extracting the code for your checkbox to a separate component.
const Checkbox = ({ category, categoryIMG }) => {
const [isChecked, setIsChecked] = useState(false);
const handleCheck = () => {
setIsChecked((prevState) => !prevState);
};
return (
<Row className="Col-padd" key={category}>
<div>
<InputGroup className="mb-3">
<b className="ItemName"> {category} </b>
<img src={categoryIMG} height="100" width="100"></img>
<FormCheck id={category} className="Checkbox" onChange={handleCheck}></FormCheck>
{isChecked == 'true' ? <b>yes</b> : <b>No</b>}
</InputGroup>
</div>
</Row>
);
};
With a separate checkbox component like above you could instantiate it like this in the map:
<ListGroup>
{currectCategory.map((category) => (
<Checkbox category={category} categoryIMG={categoryIMG} />
))}
</ListGroup>
I know there have been similar questions, but I have a weird issue.
This is what I'm doing
import React, {useState} from 'react';
import './App.css';
import {Table, Button, InputGroup, FormControl} from 'react-bootstrap';
function App() {
const [pons, setPons] = useState();
const [translations, setTranslations] = useState([]);
const [isInEditMode, setIsInEditMode] = useState(false);
const [inputValue, setInputValue] = useState('samochod');
const [errors, setErrors] = useState([]);
const [translationsToSave, setTranslationsToSave] = useState([]);
const changeIsInEditMode = () => setIsInEditMode(!isInEditMode);
const handleEditButtonClick = (id) => console.log('Edit', id);
const handleDeleteButtonClick = (id) => console.log('Delete', id);
const handleInputChange = (e) => setInputValue(e.target.value);
const handleFetchOnButtonClick = async () => {
const resp = await fetch(`http://localhost:8080/pons/findTranslation/${inputValue}`).then(r => r.json()).catch(e => console.log(e));
if (resp.ok === true) {
setTranslations(resp.resp[0].hits);
setErrors([]);
} else {
setErrors(resp.errors ? resp.errors : ['Something went wrong. check the input']);
}
};
const handleSaveTranslations = async () => {
const resp = await fetch('localhost:8080/pons/', {method: 'POST', body: {content: translationsToSave}});
if (resp.ok === true) {
setInputValue('');
setTranslations(null);
}
};
return (
<div className="App">
{errors.length > 0 ? errors.map(e => <div key={e}>{e}</div>) : null}
<InputGroup className="mb-3">
<FormControl
value={inputValue}
onChange={handleInputChange}
placeholder={inputValue}
/>
</InputGroup>
<div className="mb-3">
<Button onClick={handleFetchOnButtonClick} disabled={inputValue === '' || errors.length > 0}>Translate</Button>
<Button onClick={changeIsInEditMode}>
{isInEditMode ? 'Exit edit mode' : 'Enter edit mode'}
</Button>
<Button disabled={translationsToSave.length === 0} onClick={handleSaveTranslations}>Save translations</Button>
</div>
<Table striped bordered hover>
<thead>
<tr>
<th>Original</th>
<th>Translation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{translations ? translations.map(pon => pon.roms.map(rom => rom.arabs.map(arab => arab.translations.map(translation => {
const {source, target} = translation;
return (
<tr>
<td><span dangerouslySetInnerHTML={{__html: source}}/></td>
<td><span dangerouslySetInnerHTML={{__html: target}}/></td>
<td>
{
!translationsToSave.includes(target) ?
<Button onClick={() => {
const tmp = translationsToSave;
tmp.push(target);
setTranslationsToSave(tmp);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
const tmp = translationsToSave;
tmp.splice(tmp.findIndex(elem => elem === target));
setTranslationsToSave(tmp);
}}>
Remove translation
</Button>
}
</td>
</tr>
)
})))) : (
<div>No translations</div>
)}
</tbody>
</Table>
</div>
);
}
export default App;
So it's a basic app, it right now just adds and removes from an array wit setTranslationsToSave. After I click the Add translation button the view stays the same. But it refreshes when I click Enter edit mode. Same with Remove translation. I need to click Enter/Exit edit mode.
Hitting Translate also reloads the view. So the Add/Remove translation buttons are the only ones which do not refresh the page. Why? What am I missing?
The issue is that you are mutating the satte in Add/Remove translation button, so when react check before re-rendering if the state updater was called with the same state it feels that nothing has changed as it does a reference check and ehnce doesn't trigger re-render
Also while updating current state based on previous state use functional callback approach for state updater.
Update your state like below
<Button onClick={() => {
setTranslationsToSave(prev => [...prev, target]);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
setTranslationsToSave((prev) => {
const index = prev.findIndex(elem => elem === target)); return [...prev.slice(0, index), ...prev.slice(index + 1)]
});
}}>
Remove translation
</Button>
In your Add translation click handler, you're mutating the state:
<Button onClick={() => {
// tmp is just a reference to state
const tmp = translationsToSave;
// You are mutating state, this will be lost
tmp.push(target);
setTranslationsToSave(tmp);
}}>
You should duplicate the state and add the new element:
<Button onClick={() => {
setTranslationsToSave([...translationsToSave, target]);
}}>