I've seen that this issue has been asked many times before but none of the answers make sense to me. After trying about every solution i could find, decided i'd just ask it myself.
Below is my code, the main issue being that in the DOM filteredData
only changes on second click. Note, projs
is the prop containing data fetched that gets filtered out and displayed
const langs = ["react", "next js", "material-ui", "tailwind css", "firebase"];
const [projectData, setProjectData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const [category, setCategory] = useState("");
useEffect(() => {
setProjectData(projs);
}, []);
const handleCategory = (res) => {
setCategory(res.data);
const filter = projectData.filter((data) => data.category === category);
setFilteredData(filter);
};
button:
{langs.map((data, index) => (
<button
key={index}
class="px-2 sm:px-6 py-2 ring-2 font-semibold ring-portfBtnLight rounded-md transform hover:scale-110 transition duration-500"
onClick={() => handleCategory({ data })}
>
{data}
</button>
))}
I'm pretty lost at the moment, any help would be greatly appreciated
You can't use the newly set value of a state in the same render cycle. To continue calling both setState
calls in the same handler you need to use the category from res.data
to generate the new filtered array and then set both.
const handleCategory = (res) => {
const filter = projectData.filter((data) => data.category === res.data);
setCategory(res.data);
setFilteredData(filter);
};
const { useState, useEffect } = React;
function App({ projs }) {
const langs = ["react", "next js", "material-ui", "tailwind css", "firebase"];
const [projectData, setProjectData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const [category, setCategory] = useState("");
useEffect(() => {
setProjectData(projs);
}, [projs]); // need to watch for change as props won't update this automatically. Setting state from props is somewhat of an anti-pattern
const handleCategory = (res) => {
const filter = projectData.filter((data) => data.category === res.data);
setCategory(res.data);
setFilteredData(filter);
};
return (
<div className="App">
{langs.map((data) => (
<button
key={data} // avoid using index as key as this will lead to erros in updating.
class={(data === category ? 'selected ' : '') + "px-2 sm:px-6 py-2 ring-2 font-semibold ring-portfBtnLight rounded-md transform hover:scale-110 transition duration-500"}
onClick={() => handleCategory({ data })}
>
{data}
</button>
))}
<div>
<h3>{category}</h3>
{filteredData.length
? filteredData.map(p => (
<div>
<p>{p.value} – {p.category}</p>
</div>
))
: <p>No projects match</p>}
</div>
</div>
);
}
const projectData = [{ id: 1, value: 'Project 1', category: 'react' }, { id: 2, value: 'Project 2', category: 'next js' }, { id: 3, value: 'Project 3', category: 'react' }, { id: 4, value: 'Project 4', category: 'material-ui' }]
ReactDOM.render(
<App projs={projectData} />,
document.getElementById("root")
);
.selected {
background-color: tomato;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Alternatively, update filteredData
in a useEffect
that watches for changes to either category
or projectData
useEffect(() => {
setFilteredData(projectData.filter((data) => data.category === category));
}, [category, projectData]);
const handleCategory = (res) => {
setCategory(res.data);
};
const { useState, useEffect } = React;
function App({ projs }) {
const langs = ["react", "next js", "material-ui", "tailwind css", "firebase"];
const [projectData, setProjectData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const [category, setCategory] = useState("");
useEffect(() => {
setProjectData(projs);
}, [projs]); // need to watch for change as props won't update this automatically. Setting state from props is somewhat of an anti-pattern
useEffect(() => {
setFilteredData(projectData.filter((data) => data.category === category));
}, [category, projectData]);
const handleCategory = (res) => {
setCategory(res.data);
};
return (
<div className="App">
{langs.map((data) => (
<button
key={data}
class={(data === category ? 'selected ' : '') + "px-2 sm:px-6 py-2 ring-2 font-semibold ring-portfBtnLight rounded-md transform hover:scale-110 transition duration-500"}
onClick={() => handleCategory({ data })}
>
{data}
</button>
))}
<div>
<h3>{category}</h3>
{filteredData.length
? filteredData.map(p => (
<div>
<p>{p.value} – {p.category}</p>
</div>
))
: <p>No projects match</p>}
</div>
</div>
);
}
const projectData = [{ id: 1, value: 'Project 1', category: 'react' }, { id: 2, value: 'Project 2', category: 'next js' }, { id: 3, value: 'Project 3', category: 'react' }, { id: 4, value: 'Project 4', category: 'material-ui' }]
ReactDOM.render(
<App projs={projectData} />,
document.getElementById("root")
);
.selected {
background-color: tomato;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
As noted in the snippets –
Setting state from props is a bit of anti-pattern, but if you must you should add the prop to the dependency array so that state will be updated should the prop change. (prop changes don't force remounting).
Also, avoid using index
as key, especially when mapping arrays whose order/membership will change as React will not update as expected.