[RFC] useSWRList #1988
-
Issue #1041 opened a request for a version of MotivationIn SWR, if multiple API calls need to be made, this is easy to do with multiple SWR hooks: const { data: user1 } = useSWR("/users/1");
const { data: user2 } = useSWR("/users/2");
const { data: user3 } = useSWR("/users/3"); However, in the above example, the developer needs to know in advance the number of users the component needs to fetch. If the number of users to fetch is variable, there isn't a good way to do that currently1. Due to the Rules of Hooks, you can't map over an array to call ProposalsAdopting this proposal would add a new hook, import useSWRList from "swr/list"; This applies to either of the proposals below, as minimizing bundle size is a common value across both. Additionally, both proposals have the same input API: useSWRList<Data = any, Error = any>(
keys: Key[],
fetcher: Fetcher<Data> | null,
config: typeof defaultConfig & SWRConfiguration<Data, Error>
); This mirrors the input API of Proposal A: "Array of SWRs"The return type of useSWRList<Data = any, Error = any>(
keys: Key[],
fetcher: Fetcher<Data> | null,
config: typeof defaultConfig & SWRConfiguration<Data, Error>
): SWRResponse<Data, Error>[]; As each request completes, the hook will trigger a rerender. In this approach, each request is effectively treated as a separate SWR hook call. An error thrown by a single hook will not affect the others. Individual requests can be refetched by calling their respective Advantages
Disadvantages
Proposal B: Aggregated ReturnThe return type of useSWRList<Data = any, Error = any>(
keys: Key[],
fetcher: Fetcher<Data> | null,
config: typeof defaultConfig & SWRConfiguration<Data, Error>
): SWRResponse<Data[], Error>; After all requests are complete, the hook rerenders with Advantages
Disadvantages
AlternativesSome potential areas to consider:
Open Questions
Footnotes
|
Beta Was this translation helpful? Give feedback.
Replies: 7 comments 12 replies
-
Proposal A is the react-query way (which was the initial request). Plus as you said, you can still implement B on top of A shall you need it. So A for me! |
Beta Was this translation helpful? Give feedback.
-
having used the React Query method (proposal A), all i'd say is easy access to the Key when looping over the responses would be greatly appreciated! additionally see what's possible to not require users utilize a "deep" object comparison when using hooks like |
Beta Was this translation helpful? Give feedback.
-
Thank you for putting together this RFC! I vote for proposal A because it’s simply “a list of |
Beta Was this translation helpful? Give feedback.
-
Any Update on if |
Beta Was this translation helpful? Give feedback.
-
Any update on this work? I'd like to be able to do the following:
I think this is essentially what useSWRList would do? |
Beta Was this translation helpful? Give feedback.
-
While this feature remains unreleased (if it ever will be), I've taken the initiative to code a solution that achieves (more or less) the same goal. I'm curious to receive feedback in case there's a better approach. Let's imagine an e-commerce website where we're working on the basket page. Our aim is to display the total basket value alongside the added products. We're adopting a loading strategy where each product loads individually. This approach ensures that each product request can be cached, enhancing the page's resilience to errors. For instance, if product A encounters an issue loading, it won't impede the loading of product B. While it might not secure the full sale, it allows customers to proceed to checkout with available products. Developing a component for each product in the basket page and fetching their information within these components is straightforward. The challenge arises in consolidating all these product details, loaded within their respective components, at the parent-level component responsible for rendering these product nodes. One feasible method involves establishing a list in the parent component and enabling the product components to update this list using useEffect as their loading status changes. This approach accumulates an updated list of all loaded products by the completion of the last product load. However, this approach tends to generate verbose, repetitive, and aesthetically unpleasing code rapidly. To address this, I crafted a solution: a generic component that tracks the loaded products and subsequently passes them to a child component via a function as a prop. This approach aims to streamline the code and prevent it from becoming overly convoluted. I acknowledge this is less than ideal, but I cannot visualise a better approach till we get this RFC released. This is essentially how we would use it: // page.tsx
type Product = {
price: number;
id: number;
name: string;
};
export function BasketPage() {
return (
<GetDataList
keys={['/test/1', '/test/2', '/test/3']}
render={(entries: Entry<Product>[]) => {
if (entries.some(entry => entry.isLoading)) {
return <div>Loading...</div>;
}
return (
<div>{sum(entries.map(({ data }) => ({ price: data.price })))}</div>
);
}}
/>
);
} And these are the supporting components and functions: // get-data-list.tsx
'use client';
export default function GetDataList<T extends {}>({
keys,
render,
}: {
keys: string[];
render: (entries: Entry<T>[]) => React.ReactNode;
}) {
const [entries, setEntries] = useState<Entry<T>[]>([]);
return (
<>
{keys.map(key => (
<GetDataListItem key={key} setEntries={setEntries} />
))}
{render(entries)}
</>
);
} // get-data-list-item.tsx
'use client';
import { useCallback, useEffect } from 'react';
import { addOrUpdateItem, fetcher, generateId, removeOnce } from './utils';
import useSWRImmutable from 'swr/immutable';
export type Entry<T> = {
_id: string;
isLoading: boolean;
error: any;
data: T;
};
export default function GetDataListItem<T extends {}>({
key,
setEntries,
}: {
key: string;
setEntries: (prev: (value: Entry<T>[]) => Entry<T>[]) => void;
}) {
const { data, isLoading, error } = useSWRImmutable(key, fetcher);
const getInternalId = useCallback(generateId, []);
useEffect(() => {
const _id = getInternalId();
setEntries(items => {
return addOrUpdateItem(items, { _id, data, isLoading, error });
});
return () => {
setEntries(items => {
return removeOnce(items, _id);
});
};
}, [getInternalId, data, isLoading, error, setEntries]);
return <></>;
} // utils.ts
export function generateId() {
return Math.random().toString(36).slice(2, 9);
}
export function addOrUpdateItem<T extends { _id: string }>(
items: T[],
item: T,
) {
const index = items.findIndex(i => i._id === item._id);
if (index === -1) {
return [...items, item];
}
return items.map(i => (i._id === item._id ? item : i));
}
export function fetcher(url: string) {
return fetch(url)
.then(r => r.json())
.catch(console.error);
}
export function removeOnce<T extends { _id: string }>(items: T[], _id: string) {
const entryIndex = items.findIndex(entry => entry._id === _id);
return items.filter((_, index) => entryIndex !== index);
}
export function sum<T extends { price: number }>(items: T[]) {
return items.reduce((acc, { price }) => acc + price, 0);
} |
Beta Was this translation helpful? Give feedback.
-
any update on this? |
Beta Was this translation helpful? Give feedback.
Thank you for putting together this RFC! I vote for proposal A because it’s simply “a list of
useSWR
hooks”, and the behavior is pretty clear. Hopefully it can be implemented with very little change to the core.