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.
Related
Version of react - 17.0.2
I have a piece of code that checks if an image errors (img.onerror) out and removes the item associated with the image if it does.
I have a state to store the ids of the items that error out and then filter out these items from the original list.
The problem is all my items are erroring out and the filtering is returning an empty array and I've verified it's empty by rendering array.length onto the UI. However this is not removing the items from the ui. I'm using key prop as well and the ids are unique. However, removing the key prop resolves this issue.
Anyone have any idea why this is happening?
const ListComponent = ({list}) => {
const [imgErrorIds, setImgErrorIds] = useState([])
const validList = useMemo(
() =>
list.filter(
({ id }) =>
!imgErrorIds.find(
(imgId) => imgId === id,
),
),
[imgErrorIds, list],
)
useEffect(() => {
list.forEach(({ imageUrl, id }) => {
const img = new Image()
const validImgUrl = imageUrl
img.onerror = () => {
img.onerror = null
setImgErrorIds((imgErrorIds) => [
...imgErrorIds,
id,
])
}
img.src = validImgUrl
})
}, [list])
return (<>
validList.map(
({
name,
id,
imageUrl,
}) =>
<div key={id}>
<img src={imageUrl}/>
<p>{name}</p>
</div>)
</>)
}
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.
I have a ul that displays users with a checkbox input. When searching for a user by surname/first name, the previously selected input checkboxes are removed. How to prevent it?
function App() {
let copyList = useRef();
const [contacts, setContacts] = useState([]);
useEffect(() => {
fetch(api)
.then((res) => res.json())
.then((data) => {
copyList.current = data;
setContacts(copyList.current);
})
.catch((err) => console.log(err));
}, []);
contacts.sort((a, b) => (a.last_name > b.last_name ? 1 : -1));
const searchHandler = (value) => {
const contactsList = [...copyList.current].filter((x) => {
if (value.toLowerCase().includes(x.last_name.toLowerCase())) {
return x.last_name.toLowerCase().includes(value.toLowerCase());
} else if (value.toLowerCase().includes(x.first_name.toLowerCase())) {
return x.first_name.toLowerCase().includes(value.toLowerCase());
} else if (value === "") {
return x;
}
});
setContacts(contactsList);
};
return (
<div className="App">
<Header />
<SearchBar onSearch={(value) => searchHandler(value)} />
<ContactsList contacts={contacts} />
</div>
);
}
Input component is in ContactsList component.
function Input(props) {
const [isChecked, setIsChecked] = useState(false);
const [id] = useState(props.id);
const handleChange = () => {
setIsChecked(!isChecked);
console.log(id);
};
return (
<input
type="checkbox"
className={style.contact_input}
checked={isChecked}
onChange={handleChange}
value={id}
/>
);
}
When you filter the contacts and update the contacts state, the list in ContactList will be re-rendered as it is a new array which means you will have a new set of Input components with the default state. In order to persist the previously selected items you will also have to store the array of selected IDs in state. The Input component should receive the isChecked and onChange values from props so you can pass in a condition where you check if the current ID is in the list of selected IDs which means it is checked and when the user clicks on it, you can just simply update that array by adding the ID if it's not currently in the array or removing from it if it is already present (copying the array first since it's a state variable).
In my React project using React Query, I have a functional component MoveKeywordModal such that:
when it first loads, it fetches from API endpoint api/keyword_lists to fetch a bunch of keywordLists data. For each of these keywordLists, call it list, I create a clickable element.
When the clickable element (wrapped in a HoverWrapper) gets clicked, I want to send a POST API request to api/keyword_lists/:list_id/keyword_list_items/import with some data.
where :list_id is the id of the list just clicked.
export const MoveKeywordModal = ({
setShowMoveKeywordModal,
keywordsToMove
}) => {
const { data: keywordLists } = useQuery('api/keyword_lists', {})
const [newKeywordList, setNewKeywordList] = useState({})
const { mutate: moveKeywordsToList } = useMutation(
`api/keyword_lists/${newKeywordList.id}/keyword_list_items/import`,
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
const availableKeywordLists = keywordLists
.filter(l => l.id !== activeKeywordList.id)
.map(list => (
<HoverWrapper
id={list.id}
onClick={() => {
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList, data: keywordsToMove }
})
}}>
<p>{list.name}</p>
</HoverWrapper>
))
return (
<>
<StyledModal
isVisible
handleBackdropClick={() => setShowMoveKeywordModal(false)}>
<div>{availableKeywordLists}</div>
</StyledModal>
</>
)
}
Despite calling setNewKeywordList(list) in the onClick of the HoverWrapper, it seems the newKeywordList.id is still not defined, not even newKeywordList is defined.
What should I do to fix it?
Thanks!
react doesn’t perform state updates immediately when you call the setter of useState - an update is merely 'scheduled'. So even though you call setNewKeywordList, the newKeywordList will not have the new value in the next line of code - only in the next render cycle.
So while you are in your event handler, you’ll have to use the list variable:
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList: list, data: keywordsToMove }
})
/edit: I just realized that your call to useMutation is not correct. It doesn’t have a key like useQuery, it has to provide a function as the first argument that takes variables, known as the mutation function:
const { mutate: moveKeywordsToList } = useMutation(
(variables) => axios.post(`api/keyword_lists/${variables.newKeywordList.id}/keyword_list_items/import`),
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
see also: https://react-query.tanstack.com/guides/mutations
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)
})));
}