So I'm trying to build search feature for a website and I'm facing an issue where when a specific search is done the old state is not there, and if you delete a character is deleted I don't have the old state and doesn't not filter anything.
Here is my function:
function filterSearch(state, query) {
const orders = state.orders.filter((order) =>
order.id.toLowerCase().includes(query.toLowerCase())); // this filters the state
state.orders = orders; // this is redux state but you can think it as a setState
}
The problem is that you're storing the original state and overwriting the same. These are my suggestions
First Approach: storing filtered orders
function filterSearch(state, query) {
const orders = state.orders.filter((order) => {
const id = order.id.toLowerCase();
return id.includes(query.toLowerCase()));
});
state.filteredOrders = orders;
}
Basically you'll create a new state to store the filtered orders. That way the state orders will maintain the original list
Second Approach: returning filtered
function filterSearch(state, query) {
const orders = state.orders.filter((order) => {
const id = order.id.toLowerCase();
return id.includes(query.toLowerCase()));
});
return orders;
}
This way instead of storing the filtered data or overwriting the original data, use the function to return the filtered data and use it where you need it
you have to maintain two state for this, one state would be of the initial order then you can put that state into the local state which you can use to display the orders and always search on the initial state so that user will always get the correct result, something like following
const Table = ({ initailSearch }) => {
const [data, setData] = React.useState(initailSearch);
const [query, setQuery] = React.useState("");
const handleChange = (e) => {
setQuery(e.target.value);
};
useEffect(() => {
let filterData = initailSearch;
if (query.length > 0) {
filterData = initailSearch.filter((item) => {
return item.id.toLowerCase().includes(query.toLowerCase());
});
}
setData(filterData);
}, [query]);
return (
<>
<input onChange={handleChange} />
<table>
<thead>
<tr>
<th>order</th>
</tr>
{data.map((item) => {
return (
<tr key={item.id}>
<td>{item.id}</td>
</tr>
);
})}
</thead>
</table>
</>
);
};
working code sandbox link
Related
I'm attempting to create a data table with react hooks but I keep getting duplicates rows in my state. Here is the call where I'm getting values doing some manipulation and then calling my function to update state:
var tableRowIndex = 0;
const CoinTable = () => {
const baseURL = 'endpoint/';
const [tableRows, setRows] = useState([] as any);
const getCurrentPrice = async (inputs: any) => {
const response = await axios.get(`${baseURL}${inputs.currency}/USD`)
let currentPrice = response.data
inputs.cryptoPrice = currentPrice.rate;
let coinsRequired = inputs.amount / inputs.cryptoPrice;
inputs.amtCrypto = coinsRequired.toFixed(8);
addNewRow(inputs)
}
Here is my function where I'm attempting to update state
const addNewRow = (inputs: any) => {
tableRowIndex++
inputs.index = tableRowIndex
setRows([...tableRows, inputs])
}
This is the rest of the components where I'm mapping through my rows and outputting in my JSX.
const rows = tableRows.map((row: any) => {
return (
<TableRow
key={tableRowIndex}
addNewRow={addNewRow}
removeRow={removeRowHandler}
getCurrentPrice={getCurrentPrice}
row={row}
/>
)
})
return (
<>
<AddItem
getCurrentPrice={getCurrentPrice}
/>
{tableRows.length > 0 &&
<table>
<tbody>
<tr>
<th>Merchant</th>
<th>Item</th>
<th>Amount(Crypto)</th>
<th>Currency</th>
<th>Price/crypto(USD)</th>
<th>Amount(USD)</th>
</tr>
{rows}
</tbody>
</table>
}
</>
)
}
export default CoinTable;
Inputs is object containing user inputs to be rendered as a new row. It appears to be an issue as to how I'm updating state using the spread operator but I'm not sure.
It appears as though you are using the single tableRowIndex "global" value as the React key for every mapped element. You likely meant to use the row indexgenerated inaddNewRowwhen adding an element to thetableRows` state.
const addNewRow = (inputs: any) => {
tableRowIndex++;
inputs.index = tableRowIndex; // <-- assigned here
setRows([...tableRows, inputs]);
}
...
const rows = tableRows.map((row: any) => {
return (
<TableRow
key={row.index} // <-- used here
addNewRow={addNewRow}
removeRow={removeRowHandler}
getCurrentPrice={getCurrentPrice}
row={row}
/>
)
})
A more idiomatic method would be to call this augmented property id so it's abundantly clear it's not any array index and actually a unique value assigned to each element. I'd even go as far as to say you might want to use a library that generates GUIDs for you. uuid is a great one.
import { v4 as uuidV4 } from 'uuid';
...
const addNewRow = (inputs: any) => {
setRows(rowData => [
...rowData,
{
...inputs, // <-- don't mutate inputs object
id: v4uuid() // <-- assign unique id
},
]);
}
...
const rows = tableRows.map((row: any) => {
return (
<TableRow
key={row.id}
addNewRow={addNewRow}
removeRow={removeRowHandler}
getCurrentPrice={getCurrentPrice}
row={row}
/>
)
})
I have been struggling on this piece of codes which I suppose a button's clicked, the table will toggle between show all items and show winner items only.
Problem: The button has to be clicked two times to show winner items. Can't revert back to show all.
Do appreciate if someone can help. Thank you so much.
const MovieList = () => {
// Get Movies
const [movies, setMovies] = useState([])
const [winner, filterWinner] = useState(false)
const fetchMovies = async () => {
const res = await fetch('http://localhost:5000/data')
const data = await res.json()
return data
}
useEffect(() => {
const getMovies = async () => {
const moviesFromServer = await fetchMovies()
setMovies(moviesFromServer)
}
getMovies()
}, [])
//toggle between setting movies to all movies and winner movies.
//movie is an object that has a key and value pair "winner" : "True" or "winner" : "False"
const toggleWinner = () => {
filterWinner(!winner)
if (winner === true) {
const winners = movies.filter((movie) => movie.winner === 'True');
setMovies(winners);
} else {
setMovies(movies);
}
}
return (
<div className="container">
<h1>Movies</h1>
<hr />
<div>
<Button onClick={toggleWinner} color="info">{winner ? "Show All" : "Show Winners"}</Button>
</div>
<div>
<table className="table table-bordered table-striped">
<thead className="thead-dark">
<tr>
<th>Year</th>
<th>Film Name</th>
<th>Oscar Winner</th>
<th>Country</th>
</tr>
</thead>
<tbody>
{movies.map(movie => (
<tr key={movie.id}>
<td>{movie.year}</td>
<td>{movie.filmName}</td>
<td>{movie.winner}</td>
<td>{movie.country}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
export default MovieList;
The problem here is when you set state, that state will not be updated immediately, so you can't compare winner to true.
You can try this approach
const toggleWinner = () => {
//winner = false
filterWinner(prev => {
if(!prev) { // winner = true
const winners = movies.filter((movie) => movie.winner === "True");
setMovies(winners);
}
else {
setMovies(movies);
}
return !prev
});
};
Another problem is that you mutated the movies, so when you toggle again, old movies value is gone.
Check this codesandbox to see how I fixed that: https://codesandbox.io/s/frosty-browser-o8jeb?file=/src/App.js
If your state change depends on previous state value you need to call a function inside your update function.
const [state,setState]=useState(false);
If you want to toggle the state value you need to call update function like this.
setState(state=>!state)
In your case
filterWinner(winner=>!winner)
Note: you can use any name you want as argument inside update function.
I'm new at Reactjs and in this case, I'm trying to show a list of operations. I need to show only the LAST 10 operations of the list and I'm trying to do this using .splice() on the array. I tried a lot but couldnĀ“t make it work.
I'm getting the following error:
TypeError: list is not iterable.
Any idea how to do this?
This is my component code so far:
export default function ListOperations() {
const dispatch = useDispatch();
// const list = useSelector((state) => state.operations);
const [list, setList] = React.useState({});
React.useEffect(async () => {
try {
const response = await axios.get("http://localhost:3000/operation");
dispatch({
type: "LIST_OPERATIONS",
list: response.data,
});
} catch (e) {
swal("Error", e.message, "error");
}
}, []);
const currentListCopy = [...list];
if (currentListCopy >= 10) {
currentListCopy.splice(10);
setList(currentListCopy);
}
return (
<div>
<div>
<h2>OPERATIONS HISTORY:</h2>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Reason</th>
<th>Amount</th>
<th>Date</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{list.map((oneOperation) =>
oneOperation ? (
<tr key={oneOperation.id}>
<td>{oneOperation.id}</td>
<td>{oneOperation.reason}</td>
<td>{oneOperation.amount}</td>
<td>{oneOperation.date}</td>
<td>{oneOperation.type}</td>
</tr>
) : null
)}
</tbody>
</table>
</div>
);
}
UPDATED VERSION:
export default function ListOperations(){
const dispatch = useDispatch();
const storeList = useSelector((state) => state.operations);
const [list, setList] = React.useState([]);
React.useEffect(async () => {
try{
const response = await axios.get('http://localhost:3000/operation');
dispatch({
type: 'LIST_OPERATIONS',
list: response.data
})
if(Array.isArray(storeList) && storeList.length){
const currentListCopy = [...storeList];
if(currentListCopy.length >= 10){
currentListCopy.splice(10);
setList(currentListCopy);
}
}
}
catch(e){
swal("Error", e.message, "error");
}
}, [storeList]);
There are a couple of issues, which are causing the error and also, if the error is fixed, the fetched results will not be shown in the application.
Issue 1
const [list, setList] = React.useState({});
In the above code, you're initializing state as an object, which is causing the error list is not iterable, in the below code, when you're trying to use the spread operator to create an array of state object.
const currentListCopy = [...list];
Fix
You can fix this issue by initialing the list state as an empty array.
const [list, setList] = React.useState({});
Issue 2
The second issue is you're dispatching an action in the useEffect hook, but not getting the updated state from the store, since this line // const list = useSelector((state) => state.operations); is commented out. Since you're not fetching any state from store also nor updating the local state list, you'll not see any changes in the map function, as its empty, even though some data is being returned from the network in the API call.
Fix
If you wish to use the state from the store to update the local store, than you've to uncomment this line // const list = useSelector((state) => state.operations) and rename list to something else.
Also you need to move your splice code to the useEffect hook, so, whenever the list updated in the global state, your local state also updated accordingly.
React.useEffect(() => {
if (Array.isArray(list) && list.length) { // assuming list is the global state and we need to ensure the list is valid array with some indexes in it.
const currentListCopy = [...list];
if(currentListCopy.length >= 10) { // as above answer point out
currentListCopy.splice(10);
setList(currentListCopy)
}
}
}, [list]); // added list as a dependency to run the hook on any change in the list
Also, as above answer point out, you should avoid async functions in the useEffect.
Update
the complete code
export default function ListOperations() {
const dispatch = useDispatch();
const storeList = useSelector((state) => state.operations);
const [list, setList] = React.useState([]);
React.useEffect(async () => {
try {
const response = await axios.get("http://localhost:3000/operation");
dispatch({
type: "LIST_OPERATIONS",
list: response.data,
});
} catch (e) {
swal("Error", e.message, "error");
}
}, []);
React.useEffect(() => {
if (Array.isArray(storeList) && storeList.length) {
const currentListCopy = [...storeList];
if(currentListCopy.length >= 10) {
currentListCopy.splice(10);
setList(currentListCopy)
}
}
}, [storeList]);
return (
<div>
<div>
<h2>OPERATIONS HISTORY:</h2>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Reason</th>
<th>Amount</th>
<th>Date</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{list.map((oneOperation) =>
oneOperation ? (
<tr key={oneOperation.id}>
<td>{oneOperation.id}</td>
<td>{oneOperation.reason}</td>
<td>{oneOperation.amount}</td>
<td>{oneOperation.date}</td>
<td>{oneOperation.type}</td>
</tr>
) : null
)}
</tbody>
</table>
</div>
);
}
if(currentListCopy >= 10){
currentListCopy.splice(10);
setList(currentListCopy)
}
you're missing "length" :
if(currentListCopy.length >= 10){
currentListCopy.splice(10);
setList(currentListCopy)
}
also, you shouldn't use promise inside useEffect
https://dev.to/danialdezfouli/what-s-wrong-with-the-async-function-in-useeffect-4jne
I'm able to fetch the data from API, but not able to set the data into react state variable. Using useEffect. It's Weird because Initially Code was working fine, I was able to set the data into state variable, but after writing bunch of code. I'm getting this error.
App.js
const fetchData = async () => {
try {
const response = await axios.get(
"https://60d007f67de0b200171079e8.mockapi.io/bakery"
);
const { data } = response;
return data;
} catch (err) {
console.error(err)
}
};
const extractData = (bakerys) => {
const bakery = bakerys[0];
const header = [];
Object.keys(bakery).forEach((objKeys) => {
const value = bakery[objKeys];
// if(type of value !== 'object'){
header.push(objKeys);
})
return header;
};
export default function App() {
const [bakerys, setBakerys] = useState([]);
const [flatbakery, setFlatbakery] = useState([]);
useEffect(() => {
fetchData().then((randomData) => {
console.log('randomData ->', randomData) // able to console data as an Array of object
setBakerys(randomData); // Not able to set the randomData into state variable
console.log('bakerys', bakerys)
})
}, []);
useEffect(() => {
setFlatbakery(extractData(bakerys));
}, [bakerys]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Edit to see some magic happen!</h2>
<table>
<thead>
<tr>
{flatbakery.map((headers, idx) => (
<th key={idx}>
{headers}
</th>
))
}
</tr>
</thead>
</table>
</div>
);
}
Output
This case would come up when your bakerys array is empty, that is bakerys[0] is essentially undefined. You probably need to add some sort of check before you try to iterate the keys of it.
const extractData = (bakerys) => {
const bakery = bakerys[0];
const header = [];
if(bakery) { // only run the following block if bakery is not undefined(or falsey)
Object.keys(bakery).forEach((objKeys) => {
const value = bakery[objKeys];
// if(type of value !== 'object'){
header.push(objKeys);
})
}
return header;
};
EDIT: It appears I have forgotten to mention WHY bakerys may be empty initially. UseEffect runs when the component mounts as well, so the first time that it is called, bakerys is still an empty array. As subsequent updates are made to it, it will eventually be populated with data, so you should always attempt to check if the value has been populated before attempting to run any operations on it.
Experience: I am a total beginner in React.
What I am trying to learn: Hooks (useState) - but I do not know how to update the state and rerender the view with this. As far as I understood React does not rerender the view if the updated state is somewhat similar to the last one... After googling, I tried to copy the state and update it somehow, but I am missing something, and I do not know what.
What I am trying to do in the project: I have a list of countries I want to filter through when the user selects a region from a dropdown. This is the function that gets fired when the selection happens, along with comments that I hope explain what I am trying to do:
const change = event => {
//copy the `data` state (which has a list of all the countries)
let newData = [...data];
console.log(newData);
//filter through the countries list to get only those with the selected region
let filtered = newData.filter(obj => obj.region === event.target.value);
console.log(filtered);
//change the countries list with the filtered one, and rerender the view
setData([data, newData]);
console.log(data);
};
You can find the file and the code in question HERE (scroll down to get to the change function)
Select a region from the 'Fitler by region dropdown'
See the errors/outputs in the console
You are updating the state to an array of objects and the last item will be the filtered list
Instead, pass in a single array that holds the filtered countries.
Note that your state will be lost the second time you select a different region because you are modifying the entire collection of countries.
setData(data.filter(obj => obj.region === event.target.value))
So what you can we to avoid losing the state?
We can filter the list based on the selected region.
Added comments where i changed the code
export default function CountriesList() {
const [data, setData] = useState([]);
const [distinctRegions, setDistinctRegions] = useState([]);
const [loading, setLoading] = useState(true);
// added state to track the selected region
const [selectedRegion, setSelectedRegion] = useState("");
useEffect(() => {
CountriesAPI().then(res => {
onLoad(res);
setLoading(false);
});
}, []);
const onLoad = dataList => {
setData(...data, dataList);
getRegions(dataList);
};
const getRegions = dataList => {
let regions = [];
dataList.map(dataItem =>
dataItem.region.length ? regions.push(dataItem.region) : ""
);
let regionsFiltered = regions.filter(
(item, index, arr) => arr.indexOf(item) === index
);
setDistinctRegions(...distinctRegions, regionsFiltered);
};
const renderLoading = () => {
return <div>Loading...</div>;
};
// now we only need to update the selected region
const change = event => {
setSelectedRegion(event.target.value);
};
const renderData = (dataList, distinctRegionsItem) => {
if (dataList && dataList.length) {
return (
<div>
<Container>
<Input type="text" placeholder="Search for a country..." />
<Select className="select-region" onChange={change}>
<option value="" hidden>
Filter by region
</option>
// added show all
<option value="">Show All</option>
{distinctRegionsItem.map(item => {
return (
<option key={item} value={item}>
{item}
</option>
);
})}
</Select>
</Container>
<CardList>
// filter the array based on selectedRegion and then render the list.
// if selectedRegion is empty show all
{dataList
.filter(
country => !selectedRegion || country.region === selectedRegion
)
.map(country => (
<CountryCard
population={country.population}
region={country.region}
capital={country.capital}
flag={country.flag}
key={country.alpha3Code}
id={country.alpha3Code}
name={country.name}
/>
))}
</CardList>
</div>
);
} else {
return <div>No items found</div>;
}
};
return loading ? renderLoading() : renderData(data, distinctRegions);
}