React – can't filter array properly - reactjs

I'm trying to add filter functionality to my project. From a list of entries with different languages, an array of all languages is created. The user should be able to click a language and have React filter to only show entries with that language. However, I can't seem to update the entries state properly when running the filterIt function. If I console.log entries after running setEntries(filtered), the result is the same.
const Archive = () => {
const [entries, setEntries] = useState([]);
let dreamFilter = [];
//get all entries from firestore
useEffect(() => {
const unsubscribe = firebase
.firestore()
.collection("entries")
.onSnapshot((snapshot) => {
const newEntries = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setEntries(newEntries);
});
return () => unsubscribe();
}, []);
//after entries are loaded, create filter of all languages for 'dream'
if (entries.length > 0) {
const categoryMap = Object.values(entries)
.reduce(
(concatedArr, item) => concatedArr.concat(Object.entries(item)),
[]
)
.reduce((result, [category, values]) => {
result[category] = result[category] || [];
result[category] = result[category].concat(values);
return result;
}, {});
dreamFilter = categoryMap.dream.filter(
(item, pos) => categoryMap.dream.indexOf(item) === pos
);
}
function filterIt(value) {
const filtered = entries.filter(entry => ({
...entry,
filtered: entry.dream.includes(value)
}));
console.log("filtered results = " + JSON.stringify(filtered));
setEntries(filtered);
return filtered;
}
return (
<>
<Navigation />
<ul>
{dreamFilter.map((language, i) => (
<li key={i}>
<a href="/" onClick={(value) => { filterIt(value); value.preventDefault(); }}>{language}</a>
</li>
))}
</ul>
<ArchiveContainer>
{entries.map((entry) => (
<div key={entry.id}>
<a href={"/entry/" + entry.id}>
<h5>{entry.id}</h5>
<p>{entry.dream}</p>
</a>
</div>
))}
</ArchiveContainer>
</>
);
}

Filter method should return either true or false. Read more here.
If you want to convert one array to an array of another object, then use map.
You need to modify filter method to be like this:
const filtered = entries.filter(entry => entry.dream.includes(value));

In functional component you can't update states directly.
assuming here you are trying to add filtered: true/false also in entries array.
array.filter is for adding or removing the object from array by returning true/false.
function filterIt(value) {
setEntries(entryList => entryList.map(item => ({
...item,
filtered: item.dream.includes(value)
})));
}

Related

Convert an array to array map in reactjs firebase

In my app, user can input the timings of his slots and the data will be stored in the firebase, but the data is not being stored as a map. It's being stored like this, can someone tell how to achieve this or share a tutorial.
I want it to be stored as an array map, where people can add multiple slots instead of just one slot, i realise i need it store as an array map but i am not able to create one, something like this :
Code :
const [recipes, setRecipes] = useState([])
const [form, setForm] = useState({
ingredients: [],
const [popupActive, setPopupActive] = useState(false)
const recipesCollectionRef = collection(db, "recipes")
useEffect(() => {
onSnapshot(recipesCollectionRef, snapshot => {
setRecipes(snapshot.docs.map(doc => {
return {
id: doc.id,
viewing: false,
...doc.data()
}
}))
})
}, [])
const handleSubmit = e => {
e.preventDefault()
if (
!form.ingredients ||
!form.steps
) {
alert("Please fill out all fields")
return
}
addDoc(recipesCollectionRef, form)
setForm({
ingredients: [],
steps: []
})
setPopupActive(false)
}
const handleIngredient = (e, i) => {
const ingredientsClone = [...form.ingredients]
ingredientsClone[i] = e.target.value
setForm({
...form,
ingredients: ingredientsClone
})
}
return (
<div className="App">
<h1>My recipes</h1>
<button onClick={() => setPopupActive(!popupActive)}>Add recipe</button>
<div className="recipes">
{ recipes.map((recipe, i) => (
<div className="recipe" key={recipe.id}>
{ recipe.viewing && <div>
<h4>Ingredients</h4>
<ul>
{ recipe.ingredients.map((ingredient, i) => (
<li key={i}>{ ingredient }</li>
))}
</ul>

Trying to get data from api and map to another component in React

I'm trying to map an array of movies which I get from an API.
The data is fetched successfully but when I try to map the values and display, it becomes undefined and does not show anything.
I'm new to React so any help and advice would be helpful.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios
.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
})
.catch((error) => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return (
<div>
{items.map((item) => {
<p>{item.title}</p>;
})}
</div>
);
The data is stored like this:
0: {
adult: false,
backdrop_path: '/9eAn20y26wtB3aet7w9lHjuSgZ3.jpg',
id: 507086,
title: 'Jurassic World Dominion',
original_language: 'en',
...
}
You're not returning anything from your map
{
items.map((item) => {
// Add a return
return <p>{item.title}</p>
})
}
First, your items value is an empty array[] as you have initialized with setState([]) and your useEffect() runs only after your component is rendered which means even before you could do your data fetching, your HTML is being displayed inside which you are trying to get {item.title} where your items is an empty array currently and hence undefined. You will face this issue often as you learn along. So if you want to populate paragraph tag with item.title you should fast check if your items is an empty array or not and only after that you can do the mapping as follow and also you need to return the element from the map callback. If it takes some time to fetch the data, you can choose to display a loading indicator as well.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
}).catch(error => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return ( < div > {
items.length !== 0 ? items.map((item) => {
return <p > {
item.title
} < /p>
}) : < LoadingComponent / >
}
<
/div>
);
Good catch by Ryan Zeelie, I did not see it.
Another thing, since you're using promises and waiting for data to retrieve, a good practice is to check if data is present before mapping.
Something like :
return (
<div>
{ (items.length === 0) ? <p>Loading...</p> : items.map( (item)=>{
<p>{item.title}</p>
})}
</div>
);
Basically, if the array is empty (data is not retrieved or data is empty), display a loading instead of mapping the empty array.

useQuery with selector is causing infinite requests / re renders

I have a large list of items that can be filtered, updated, and deleted. I'm using ReactQuery to fetch the list of items like this
export function useLibraryItems() {
return useQuery(['items'], () => API.FETCH_LIBRARY_ITEMS().then(response => response.data))
}
// And used later in my component like this
const items = useLibraryItems()
Due to the size of the list I'm rendering the items through a virtualized list like this
const ItemRow = ({ index, style }) => (
<Item
key={index}
item={items.data[index]}
style={style}
/>
)
<FixedSizeList
height={virtualListDimensions.height}
width={virtualListDimensions.width}
itemSize={30}
itemCount={items.data.length}
>
{ItemRow}
</FixedSizeList>
A simplified version of my item component looks like this
function Item({ item, style }) {
const [_item, setItem] = useState({ ...item })
const updateItem = useItemUpdate()
const onSaveClick = () => {
updateItem.mutate({ ..._item })
}
return (
<div>
...inputs to update item values
<button>
Update
</button>
</div>
)
}
The update item mutation looks like this
export function useUpdateLibraryItem() {
let client = useQueryClient()
return useMutation(args => API.UPDATE_LIBRARY_ITEM(args.id, args.params).then(response => response.data), {
onMutate: async (args) => {
await client.cancelQueries(['items'])
let prev = client.getQueriesData(['items'])
client.setQueriesData('items', items => [
...items.map(item => {
if(item._id === args.id)
return { ...item, ...args.params }
else
return item
})
])
return { prev }
}
})
}
This is all working as expecting. Now I'm trying to optimize this by making each item a selector. One issue with this current implementation is that if there are updates to multiple items in the list and you save the changes for one item it will optimistically update that item and the list which will clear all other updates in the list that have not been saved. So my attempts to subscribe to a single item of the items list currently looks like this
const ItemRow = ({ index, style }) => (
<Item
key={index}
id={items.data[index]._id}
style={style}
/>
)
And a new useQuery hook to select the item from the cache
export const useLibraryItem = (id) => {
return useQuery(['items'], () => API.FETCH_LIBRARY_ITEMS().then(response => response.data), { select: (items) => items.find(item => item._id === id) })
}
function Item({ id, style }) {
const item = useLibraryItem(id)
const [_item, setItem] = useState({ ...initial_item })
useEffect(() => {
setItem({ ...item.data })
}, [item.status])
const updateItem = useItemUpdate()
const onSaveClick = () => {
updateItem.mutate({ ..._item })
}
return (
<div>
...inputs to update item values
<button>
Update
</button>
</div>
)
}
When this code compiles it runs infinitely. If i add a console.log into the useEffect I will see it endlessly. So to solve this I needed to add the id's into the query key which looks like this
export const useLibraryItem = (id) => {
return useQuery(['items', id], () => API.FETCH_LIBRARY_ITEMS().then(response => response.data), { select: (items) => items.find(item => item._id === id) })
}
return useMutation(args => API.UPDATE_LIBRARY_ITEM(args.id, args.params).then(response => response.data), {
onMutate: async (args) => {
await client.cancelQueries(['items', args.id])
let prev = client.getQueriesData(['items', args.id])
client.setQueriesData(['item', args.id], item => {
return { ...item, ...args.params }
})
return { prev }
}
})
}
When this code compiles it no longer runs infinitely, but it makes a new request to API.FETCH_LIBRARY_ITEMS for every item in the list. This must be due to no longer referencing the query key of ['items'] as its now ['items', id] so it no longer caches.
Does React Query not support selectors in the way that I'm trying to use them or is there a way to implement them the way that I am trying to?
*I'm working on a sandbox for this question and will be updating with the link shortly
Does React Query not support selectors in the way that I'm trying to use them or is there a way to implement them the way that I am trying to?
react-query has selectors, but you are not using them in your example:
export function useLibraryItems(select) {
return useQuery(['items'], () => API.FETCH_LIBRARY_ITEMS().then(response => response.data), { select })
}
export function useLibraryItem(id) {
return useLibraryItems(data => data.find(item => item.id === id))
}
every time you call useLibraryItem, you will use the libraryItems query and only select a slice of the resulting data (the item that matches your id). Keep in mind that with the default settings of react-query, you will still get a background refetch of the list query every time a new item query mounts, because refetchOnMount is true and staleTime is zero.
The best way is to set a staleTime on the list query that tells react-query for how long the data is considered fresh. During that time, there won't be any additional requests.

Text field should only change for one value and not over the entire list

I have a list and this list has several elements and I iterate over the list. For each list I display two buttons and an input field.
Now I have the following problem: as soon as I write something in a text field, the same value is also entered in the other text fields. However, I only want to change a value in one text field, so the others should not receive this value.
How can I make it so that one text field is for one element and when I write something in this text field, it is not for all the other elements as well?
import React, { useState, useEffect } from 'react'
import axios from 'axios'
function Training({ teamid }) {
const [isTrainingExisting, setIsTrainingExisting] = useState(false);
const [trainingData, setTrainingData] = useState([]);
const [addTraining, setAddTraining] = useState(false);
const [day, setDay] = useState('');
const [from, setFrom] = useState('');
const [until, setUntil] = useState('');
const getTrainingData = () => {
axios
.get(`${process.env.REACT_APP_API_URL}/team/team_training-${teamid}`,
)
.then((res) => {
if (res.status === 200) {
if (typeof res.data !== 'undefined' && res.data.length > 0) {
// the array is defined and has at least one element
setIsTrainingExisting(true)
setTrainingData(res.data)
}
else {
setIsTrainingExisting(false)
}
}
})
.catch((error) => {
//console.log(error);
});
}
useEffect(() => {
getTrainingData();
}, []);
const deleteTraining = (id) => {
axios
.delete(`${process.env.REACT_APP_API_URL}/team/delete/team_training-${teamid}`,
{ data: { trainingsid: `${id}` } })
.then((res) => {
if (res.status === 200) {
var myArray = trainingData.filter(function (obj) {
return obj.trainingsid !== id;
});
//console.log(myArray)
setTrainingData(() => [...myArray]);
}
})
.catch((error) => {
console.log(error);
});
}
const addNewTraining = () => {
setAddTraining(true);
}
const addTrainingNew = () => {
axios
.post(`${process.env.REACT_APP_API_URL}/team/add/team_training-${teamid}`,
{ von: `${from}`, bis: `${until}`, tag: `${day}` })
.then((res) => {
if (res.status === 200) {
setAddTraining(false)
const newTraining = {
trainingsid: res.data,
mannschaftsid: teamid,
von: `${from}`,
bis: `${until}`,
tag: `${day}`
}
setTrainingData(() => [...trainingData, newTraining]);
//console.log(trainingData)
}
})
.catch((error) => {
console.log(error);
});
}
const [editing, setEditing] = useState(null);
const editingTraining = (id) => {
//console.log(id)
setEditing(id);
};
const updateTraining = (trainingsid) => {
}
return (
<div>
{trainingData.map((d, i) => (
<div key={i}>
Trainingszeiten
<input class="input is-normal" type="text" key={ d.trainingsid } value={day} placeholder="Wochentag" onChange={event => setDay(event.target.value)} readOnly={false}></input>
{d.tag} - {d.von} bis {d.bis} Uhr
<button className="button is-danger" onClick={() => deleteTraining(d.trainingsid)}>Löschen</button>
{editing === d.trainingsid ? (
<button className="button is-success" onClick={() => { editingTraining(null); updateTraining(d.trainingsid); }}>Save</button>
) : (
<button className="button is-info" onClick={() => editingTraining(d.trainingsid)}>Edit</button>
)}
<br />
</div>
))}
)
}
export default Training
The reason you see all fields changing is because when you build the input elements while using .map you are probably assigning the same onChange event and using the same state value to provide the value for the input element.
You should correctly manage this information and isolate the elements from their handlers. There are several ways to efficiently manage this with help of either useReducer or some other paradigm of your choice. I will provide a simple example showing the issue vs no issue with a controlled approach,
This is what I suspect you are doing, and this will show the issue. AS you can see, here I use the val to set the value of <input/> and that happens repeatedly for both the items for which we are building the elements,
const dataSource = [{id: '1', value: 'val1'}, {id: '2', value: 'val2'}]
export default function App() {
const [val, setVal]= useState('');
const onTextChange = (event) => {
setVal(event.target.value);
}
return (
<div className="App">
{dataSource.map(x => {
return (
<div key={x.id}>
<input type="text" value={val} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
This is how you would go about it.
export default function App() {
const [data, setData]= useState(dataSource);
const onTextChange = (event) => {
const id = String(event.target.dataset.id);
const val = String(event.target.value);
const match = data.find(x => x.id === id);
const updatedItem = {...match, value: val};
if(match && val){
const updatedArrayData = [...data.filter(x => x.id !== id), updatedItem];
const sortedData = updatedArrayData.sort((a, b) => Number(a.id) - Number(b.id));
console.log(sortedData);
setData(sortedData); // sorting to retain order of elements or else they will jump around
}
}
return (
<div className="App">
{data.map(x => {
return (
<div key={x.id}>
<input data-id={x.id} type="text" value={x.value} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
What im doing here is, finding a way to map an element to its own with the help of an identifier. I have used the data-id attribute for it. I use this value again in the callback to identify the match, update it correctly and update the state again so the re render shows correct values.

React - How to find out the last document in a FireStore collection

The following component connects to a Cloud FireStore collection when it's mounted and shows only one document .limit(1).
Each time the user clicks the Next button a new request is sent to FireStore and shows the next document form the collection.
It's working fine. There's only one issue:
When the user clicks the Next button several times and reaches the last document inside the FireStore collection, the next click breaks the code, which makes sense.
I want that when the last document is reached, the next click on the Next button shows the first document or at least a message that the last item is reached.
How can I do this? How can I find out the last document in a FireStore collection?
import React, { useState, useEffect } from 'react';
import { db } from '../firebase';
const FlipCard = () => {
const [cards, setCards] = useState([]);
useEffect(() => {
const fetchData = async () => {
const data = await db
.collection('FlashCards')
.orderBy('customId', 'asc')
.limit(1)
.get();
setCards(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
};
fetchData();
}, []);
const showNext = ({ card }) => {
const fetchNextData = async () => {
const data = await db
.collection('FlashCards')
.orderBy('customId', 'asc')
.limit(1)
.startAfter(card.customId)
.get();
setCards(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
};
fetchNextData();
};
return (
<>
<div>
{cards.map((card) => (
<div className='card__face card__face--front'>
{<img src={card.imgURL} alt='' />}
{card.originalText}
</div>
<div className='card__face card__face--back'>
{card.translatedText}
</div>
</div>
))}
</div>
<div>
<button
onClick={() => showNext({ card: cards[cards.length - 1] })}
>
Next Card
</button>
</div>
</>
);
};
export default FlipCard;
The way to know that you're out of docs in a collection is when the number of results (cards.length) is smaller than the limit on the query (.limit(1)).
The code is breaking because it doesn't anticipate that condition:
onClick={() => showNext({ card: cards[cards.length - 1] })}
^ this expression might be -1
One way to fix is to conditionally render the button only if cards.length > 0.
Another way is to conditionally compute the parameter to showNext...
{ card: cards.length > 0 ? cards[cards.length - 1] : {} }
...and handle that case in the method.
just handle an error
const showNext = ({ card }) => {
const fetchNextData = async () => {
const data = await db
.collection('FlashCards')
.orderBy('customId', 'asc')
.limit(1)
.startAfter(card.customId)
.get()
.catch(error => { console.log('end of the row') });
if(!data.docs.length) console.log('end of the row');
setCards(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
};
fetchNextData();
};

Resources