So I tried to modify the tagList using setTagList(). It changed the content of the page (it adds tags in the <Filter /> component) but I cannot implement that each tag in <Filter /> is unique.
import Filter from "./components/Filter";
import Card from "./components/Card";
import data from "./assets/data.json";
import { useState, useEffect } from "react"
export default function App() {
const [cards, setCards] = useState([]);
const [tagList, setTagList] = useState([]);
useEffect(() => {
generateCards();
}, []);
function addTag(newTag) {
// console.log(tagList) always outputs []
if (!tagList.includes(newTag)) {
setTagList(oldTagList => [...oldTagList, newTag])
// console.log(tagList) here always outputs []
// But if I change "const [tagList, setTagList] = useState([]);" to "let [tagList, setTagList] = useState([]);" and add "tagList = [...tagList, newTag]" here, it works
}
}
function deleteTag(tag) {
setTagList((oldTags) => oldTags.filter((oldTag) => oldTag !== tag));
}
function clearTags() {
setTagList([]);
}
function generateCards() {
setCards(
data.map((cardData) => (
<Card
key={cardData.id}
logo={cardData.logo}
company={cardData.company}
isNew={cardData.new}
isFeatured={cardData.featured}
position={cardData.position}
postedAt={cardData.postedAt}
contract={cardData.contract}
location={cardData.location}
tags={[
cardData.role,
cardData.level,
...cardData.languages,
...cardData.tools,
]}
addTag={addTag}
filter={filter}
/>
))
);
}
// console.log(tagList) works normally
return (
<div className="bg-bg-color h-screen">
<div className="relative bg-wave-pattern w-full h-28 text-bg-color bg-primary">
{tagList.length !== 0 && (
<Filter tagList={tagList} deleteTag={deleteTag} clearTags={clearTags} />
)}
</div>
<div className="2xl:px-96 xl:px-64 lg:px-32 md:px-16 sm:px-4 py-16 space-y-8">{cards}</div>
</div>
);
}
I tried to console.log(tagList) in the addTag() function but it always returns the default value. But when console.log(tagList) outside the function, it works like normal. But when I changed the const keyword to let when setting the state and add "tagList = [...tagList, newTag]" right after setTagList(...), it works. Is React broken?
The generateCards
function is only called from the useEffect
above, which has no dependencies, meaning it's only called when the component is mounted.
This means that the addTag
callback prop from the Card
component always references the same addTag
function that was created on component mount, so its closure only sees the initial tagList
value ([]
).
One quick fix might be to add tagList
as dependency to the useEffect
that calls generateCards
, so that all cards are re-rendered every time tagList
changes, making the newly rendered cards reference the most recent addTag
function that also has the most recent tagList
value in its context:
const data = ['Tag 1', 'Tag 2', 'Tag 3', 'Tag 4']
function App() {
const [cards, setCards] = React.useState([]);
const [tagList, setTagList] = React.useState([]);
React.useEffect(() => {
generateCards();
}, [tagList]); // <= ONLY CHANGED THIS LINE
function addTag(e) {
const newTag = e.currentTarget.textContent;
if (!tagList.includes(newTag)) {
setTagList(oldTagList => [...oldTagList, newTag])
}
}
function deleteTag(e) {
const tag = e.currentTarget.textContent;
setTagList((oldTags) => oldTags.filter((oldTag) => oldTag !== tag));
}
function clearTags() {
setTagList([]);
}
function generateCards() {
setCards(
data.map((tag) => {
const isSelected = tagList.includes(tag);
return (
<button
key={tag}
onClick={ isSelected ? deleteTag : addTag }
className={ `card${ isSelected ? ' selected' : '' }` }>
{ tag }
</button>
)
})
);
}
return (
<div>
{cards}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body,
button {
font-family: monospace;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.card {
display: block;
background: white;
padding: 8px 16px;
border: 2px solid black;
border-radius: 3px;
}
.card + .card {
margin-top: 8px;
}
.card.selected {
border-color: blue;
}
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
However, I would advise you to use useMemo
rather than storing JSX in the state, as well as updating your addTag
function to check if a tag is included in the currently selected ones that come from the previous state, rather than using the tagList
from the function's context:
const data = ['Tag 1', 'Tag 2', 'Tag 3', 'Tag 4']
function App() {
const [tagList, setTagList] = React.useState([]);
function addTag(e) {
const tag = e.currentTarget.textContent;
// CHANGED THIS:
setTagList((oldTags) => oldTags.includes(tag) ? oldTags : [...oldTags, tag])
}
function deleteTag(e) {
const tag = e.currentTarget.textContent;
setTagList((oldTags) => oldTags.filter((oldTag) => oldTag !== tag));
}
function clearTags() {
setTagList([]);
}
// CHANGED THIS:
const memoizedCards = React.useMemo(() => {
return data.map((tag) => {
const isSelected = tagList.includes(tag);
return (
<button
key={tag}
onClick={ isSelected ? deleteTag : addTag }
className={ `card${ isSelected ? ' selected' : '' }` }>
{ tag }
</button>
)
})
}, [tagList])
return (
<div>
{memoizedCards}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body,
button {
font-family: monospace;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.card {
display: block;
background: white;
padding: 8px 16px;
border: 2px solid black;
border-radius: 3px;
}
.card + .card {
margin-top: 8px;
}
.card.selected {
border-color: blue;
}
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>