React js, javascript not filtering properly - javascript

i am trying to make a filter feature for a website i am working on, i am using an html range slider. The problem is that the values update just if they are going down, for example if i set the slider to $500, only the products that cost $500 or less will appear, if i set the value lower, it's going to work how is supposed to work, but if i try to set the value bigger, the items will not filter, for example, the value is set to $500, if set the value to $600 only the items that are $500 or less will render, but not the $600 ones.
here is my code:
const Shop = () => {
const [sliderValue, setValue] = useState(0);
const [filterItems, setApplyFilter] = useState(false);
const [newData, setData] = useState(data);
const checkChange = () => {
if (sliderValue > 3) {
setApplyFilter(true);
} else {
setApplyFilter(false);
}
console.log(applyFilter);
};
const applyFilter = () => {
if (filterItems === true) {
const filteredData = newData.filter((item) => item.price <= sliderValue);
console.log(filteredData);
setData(filteredData);
} else {
setData(data);
}
};
useEffect(() => {
checkChange();
applyFilter();
}, [sliderValue]);
const handleChange = (value) => {
setValue(value);
};
return (
<div className="slider-container">
<input
type="range"
min={0}
max={1000}
value={sliderValue}
onChange={(e) => handleChange(e.target.value)}
className="slider"
/>
</div>
);
}

The problem: you are changing the data with setData(), so every time you move your scrollbar this deletes some data. If you want to keep a constant information that is available to all your application, consider using useRef(). This creates a persistent object for the full lifetime of the component.
import { useRef } from 'react'
const Shop = () => {
const dataArr = useRef(data)
...
const applyFilter = () => {
if (filterItems === true) {
// Access with "current" attribute
const filteredData = dataArr.current
.filter((item) => item.price <= sliderValue);
setData(filteredData);
}
}
}
Working example

I think it's something to do with this two lines:
const filteredData = newData.filter((item) => item.price <= sliderValue);
setData(filteredData);
Once you have filtered your data once, the value of newData in your state will be only the already filtered data.
Let's say we start with prices: newData=[100, 200, 300, 400]
We filter it for the first time down to 200, so now newData=[100, 200]
Next we filter up to 300, but newData only has [100, 200]
So just change those two lines for:
const filteredData = data.filter((item) => item.price <= sliderValue);
setData(filteredData);
This is asuming you have a variable data declared or imported somewhere with the comple data set.

You don't need state for data array since it can be determined on every render based on some other state.
const Shop = ({ inputData }) => {
const [sliderValue, setValue] = useState(0);
// This flag is deterministic based on sliderValue, so determine it here
const filterItems = sliderValue > 3;
// The items that will make it past the filter are deterministic, based on your filterItems flag
// so no state is necessary
const renderItems = filterItems ? inputData.filter(i => i.price <= sliderValue) : inputData;
const handleChange = (value) => {
setValue(value);
};
return ...
};

Related

React JS Filters being overwritten by setState data in useEffect

I have a table where I'm setting data inside a useEffect from an api. My filter logic iterates through the "rows" variable which is being set inside this useEffect. However, every-time the user searches via an input which has an onChange event the useEffect setRows I believe is setting the data over and over again.
What would be a better way to set the data so it doesn't conflict with my filtering logic?
//State
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
//useEffect to setData
useEffect(() => {
//setDocuments from claimStore when component mounts
setDocuments(claimsStore.getIncomingDocuments());
//setRows from documents when component mounts
setRows(
documents.map((document) =>
createData(
document.documentAuthor ?? '',
document.documentMetadataId.toLocaleString(),
document.documentMetadataId.toLocaleString(),
document.documentName ?? '',
document.documentSource ?? '',
document.documentType,
document.featureId ?? '',
document.mimeType,
document.uploadDateTime,
),
),
);
}, [claimsStore, documents]);
//Filter logic that updates rows as user input values captured
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
//Filter documents based on user input
const updatedList = rows.filter((document) => {
return (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
);
});
//Trigger render with updated values
setRows(updatedList);
};
Use of filterBySearch:
<TableCell align={'center'} className={classes.tableCell}>
<input
value={filters.featureId}
onChange={(e) => filterBySearch(e)}
name="featureId"
className={classes.inputCell}
/>
</TableCell>
This is one of the things useMemo is good for: Have an array of filtered rows, that you update as necessary when rows or filters changes:
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
// ...
const filteredRows = useMemo(
() => rows.filter((document) => (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
)),
[rows, filters]
);
Then display filteredRows, not rows.
With that change, filterBySearch just sets the filter, it doesn't actually do the filtering:
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
};
useMemo will only call your callback when either rows or filters changes; otherwise, it'll just return the previous filtered array.
Here's a simplified demo — it shows words filtered by whatever you type in the filter, and randomly adds a word once every couple of seconds (this demonstrates that the filtering is repeated when the filter changes or when the rows change):
const { useState, useEffect, useRef, useMemo } = React;
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
return (
<div>
<div>
Filter: <input type="text" value={filter} onChange={filterChange} />
</div>
Rows - showing {filteredRows.length} of {rows.length} total:
<div>
{filteredRows.map((row) => (
<div key={row.id}>{row.value}</div>
))}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
React's documentation says that useMemo is just for performance enhancement, it isn't a semantic guarantee (basically, React may call your callback even when nothing has actually changed). If you want a semantic guarantee, you can do it with a ref. You can even wrap that up into a hook that provides the semantic guarantee — I call it useHardMemo:
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
Live Example:
const { useState, useEffect, useRef, createElement } = React;
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useHardMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
// I'm using `createElement` because I had to turn off SO's hopelessly outdated Babel because
// I wanted to be able to use optional chaining and such; so I couldn't use JSX.
// return (
// <div>
// <div>
// Filter: <input type="text" value={filter} onChange={filterChange} />
// </div>
// Rows - showing {filteredRows.length} of {rows.length} total:
// <div>
// {filteredRows.map((row) => (
// <div key={row.id}>{row.value}</div>
// ))}
// </div>
// </div>
// );
return createElement(
"div",
null,
createElement(
"div",
null,
"Filter: ",
createElement("input", { type: "text", value: filter, onChange: filterChange })
),
`Rows - showing ${filteredRows.length} of ${rows.length} total:`,
createElement(
"div",
null,
filteredRows.map((row) => createElement("div", { key: row.id }, row.value))
)
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(createElement(Example));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

having n states in react, assuming that n won't be received in props

How could I have n states in a React component
Assuming that the component won't receive this n value in any props, is something that it will get from a database
Using useState will create the state, setState for each pair, but I need n pairs
Rafael
JavaScript arrays doesn't have a fixed length.
You can do something like
const [arr, setArr] = useState([]);
And when you receive n values from database just set it to the array using setArr(values)
Now arr will be an array containing n elements retrieved from database. You can then iterate over it and render them as you wish.
As T J pointed out. You can use an array in state.
Or, another option is to map n Components for each item, therefore instantiating n states.
const Example = (props) => {
const [data, setData] = useState();
useEffect(() => {
// ...fetch data
// setData(data);
});
if (data === undefined) {
return null;
}
return data.map((data) => <Item data={data} />);
};
const Item = (props) => {
const [state, setState] = useState(props.data);
return <>Example</>;
};
Or if n is literally just a number, a count. Then you could do something like this.
const Example = (props) => {
const [count, setCount] = useState();
useEffect(() => {
// ...fetch count
// setCount(count);
});
if (count === undefined) {
return null;
}
const items = [];
for (var i = 1; i <= count; i++) {
items.push(<Item />);
}
return items;
};
const Item = (props) => {
const [state, setState] = useState();
return <>Example</>;
};

How to get sum of array from two different input in two different components? React

I am new to React and I am building a budget calculator. I am taking the amount from one input and adding it to another input so I can come up with the balance. I have tried reduce and concat and they are coming up to sum but the value is wrong. I don't know what I'm doing wrong. Can anyone point me in the right direction. I think the problem is that the values are rendering twice and that's throwing off the math. I don't know.
Here is my code:
// this is the component to get the balance
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
balance: []
}
}
getBalance = (total) => {
this.setState((prevState) => ({
balance: [prevState.balance, total].reduce((acc, currentVal) => {
return Number(currentVal) + Number(acc)
}, 0)
}));
}
render() {
return (
<div className="App" >
<div className="count">
<h2 className="balancetitle">Your Balance</h2>
<h1 style={{ color: this.state.balance >= 0 ? 'green' : 'red' }}>${this.state.balance}</h1>
</div>
<Transactions getBalance={(total) => this.getBalance(Number(total))} />
<Income getBalance={(total) => this.getBalance(Number(total))} />
</div>
);
}
}
// this is the code to get the transaction. I have another component that is identical to get the sum of the income.
const Transactions = (props) => {
const [expenses, setExpense] = useState([])
const [amount, setAmount] = useState([])
const [id, setId] = useState([])
const [listOfTrans, setListofTrans] = useState([])
const [total, setTotal] = useState([0])
//fires on click or enter
const handleSubmit = (e) => {
e.preventDefault()
addExpense({
amount,
expenses,
id
});
setAmount('')
setExpense('')
}
//get value of inputs
const getValue = (hookSetter) => (e) => {
let { value } = e.target;
return hookSetter(value)
}
// turn amount and expense into objects and put them setListofTranas
const addExpense = (expenseObject) => {
setListofTrans([...listOfTrans, expenseObject])
}
const show = () => {
if (listOfTrans.legnth > 1) {
return listOfTrans
} else return null
}
// get total amount of listoftrans
const getAmount = () => {
if (listOfTrans.length > 0) {
let listAmount = listOfTrans.map(list => {
if (list.amount) {
return -Math.abs(list.amount);
} else {
return 0;
}
})
return listAmount.reduce((acc, currentValue) => {
return Number(acc) + Number(currentValue)
}, 0)
} else return 0
}
//update amount total on click
useEffect(() => {
setTotal(getAmount())
props.getBalance(getAmount())
}, [listOfTrans])
// delete item from array
const deleteExpense = (i) => {
let objExpense = i
setListofTrans(listOfTrans.filter((list) => {
return list.id !== objExpense
}))
}
I am adding it here as the suggestion is not possible to add long description in comments section.
What you are doing buggy in the the solution above is making use of useEffect to do the calcualtions. The approach can be real buggy and difficult to debug.
//update amount total on click
useEffect(() => {
setTotal(getAmount())
props.getBalance(getAmount())
}, [listOfTrans])
In the code above listOfTans is an array , may be changing due to various operation, which cause the useEffect callback to run repeatedly. The callback is reponsible for updating the sum.
So instead of doing that, you should just call
props.getBalance(getAmount())
in onClick Handler.
This is just the suggestion for what I can understand from the question above.

Filtering an array and then mapping it isn't displaying results with React Hooks

import React, { useState, useEffect } from "react";
import axios from "axios";
const App = () => {
let [countries, setCountries] = useState([]);
const [newCountry, newStuff] = useState("");
const hook = () => {
//console.log("effect");
axios.get("https://restcountries.eu/rest/v2/all").then((response) => {
console.log("promise fulfilled");
setCountries(response.data);
//console.log(response.data);
});
};
const filter = (event) => {
newStuff(event.target.value);
if (event.target.value === undefined) {
return
} else {
let value = event.target.value;
console.log(value);
countries = countries.filter((country) => country.name.startsWith(value));
setCountries(countries);
console.log(countries);
}
};
useEffect(hook, []);
return (
<div>
<p>find countries</p>
<input value={newCountry} onChange={filter} />
<ul>
{countries.map((country) => (
<li key={country.name.length}>{country.name}</li>
))}
</ul>
</div>
);
};
export default App;
So I have a search bar so that when you enter a few characters it will update the state and show the countries that start with the respective first characters. However, nothing is being shown when I enter input into my search bar. Also, my filter function, when I console.log my countries array which is supposed to have the countries that start with the characters I entered, it's always an empty array.
You need some changes in order to make this work:
Use two states for countries, one for the list you
get in the initial render and another for the current filter
countries.
const [countriesStore, setCountriesStore] = useState([]); // this only change in the first render
const [countries, setCountries] = useState([]); // use this to print the list
I recomed to use any tool to manage the state and create a model for
the countries ther you can make the side effect there and create an
action that update the countries store. I'm using Easy Peasy in
my current project and it goes very well.
Take care of the filter method because startsWith
method is not case-insensitive. You need a regular expression or
turn the current country value to lower case. I recommend to use
includes method to match seconds names like island in the search.
const filterCountries = countriesStore.filter(country => {
return country.name.toLowerCase().includes(value);
});
Remove the if condition in the filter in order to include the
delete action in the search and get the full list again if
everything is removed.
Just in the case, empty the search string state in the first
render
useEffect(() => {
hook();
setSearchString("");
}, []);
Replace the length in the list key. You can use the name and trim to remove space.
<li key={country.name.trim()}>{country.name}</li>
The final code look like this:
export default function App() {
const [countriesStore, setCountriesStore] = useState([]);
const [countries, setCountries] = useState([]);
const [searchString, setSearchString] = useState("");
const hook = () => {
axios.get("https://restcountries.eu/rest/v2/all").then(response => {
console.log("promise fulfilled");
setCountriesStore(response.data);
setCountries(response.data);
});
};
const filter = event => {
setSearchString(event.target.value);
let value = event.target.value;
const filterCountries = countriesStore.filter(country => {
return country.name.toLowerCase().includes(value);
});
setCountries(filterCountries);
};
useEffect(() => {
hook();
setSearchString("");
}, []);
return (
<div>
<p>find countries</p>
<input value={searchString} onChange={filter} />
<ul>
{countries.map(country => (
<li key={country.name.trim()}>{country.name}</li>
))}
</ul>
</div>
);
}
You need to wrap your hook into async useCallback:
const hook = useCallback(async () => {
const {data} = await axios.get("https://restcountries.eu/rest/v2/all");
setCountries(data);
}, []);
you are not able to mutate state countries. Use immutable way to update your state:
const filter = (event) => {
newStuff(event.target.value);
if (event.target.value === undefined) {
return
} else {
let value = event.target.value;
setCountries(countries.filter((country) => country.name.startsWith(value)));
}
};
And useState is asynchronous function. You will not see result immediately. Just try to console.log outside of any function.

How to show all my arrays with Firebase and React?

I've some pushes in Firebase, and I've the data. I've separate the arrays side by side, and I want to show they. But, with my code, I just have the last array on my all arrays.
How to show all the arrays side by side ?
My code :
// useEffect()
let postJSON
firebase.database().ref('plugins/posts/').on('value', (snapshot) => {
const json = snapshot.toJSON()
for (const i in json) {
const element = json[i]
postJSON = [element.name, element.description, element.price, element.linkPlugin]
console.log(postJSON)
setPost(postJSON.map((x, i) => <p key={i}>{x}</p>))
}
})
// Render
return (
{post}
)
try
const MyComponent = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
firebase.database().ref('plugins/posts/').on('value', (snapshot) => {
const json = snapshot.toJSON()
const keys = Object.keys(json);
const postJSON = keys.map(key => {
const element = json[key];
return [element.name, element.description, element.price, element.linkPlugin]
});
setPosts(postJSON);
})
}, []);
return (
<div>{posts.map((x, i) => <p key={i}>{x}</p>)}</div>
)
}
as is you are calling setPosts 3 times, once for each array item, each time overriding the previous call to setPosts. You need to just call it once with an array of arrays

Categories