Hi,
I started a discussion here: https://github.com/apollographql/apollo-client/discussions/9136 but I now saw that you want to move the discussions to this form instead so I create a copy here.
Let’s say I have the following requirements for a web app:
- Show a list of content
- Add a button at the bottom of the page to load more content that should be added at the bottom of the list
- Add a search field
- Show the text “Searching…” while searching (meanwhile we are waiting for the HTTP response)
- Show the old result until we have a new search result (ie. show the old result while we are waiting for the HTTP response)
- Do not search for every keystroke; add a debounce for the search input
- Cancel any ongoing request when sending a new request so only one request is active at the time
- Be able to load more search results (add pagination at the bottom)
- Be able to navigate from the page and then back again and keep the list state (pagination and search result)
The best solution I have come up with is the following:
export function PostList() {
/** Keep the search string in a global state to keep it on page shift */
const [searchTerm, setSearchTerm] = useSearchTerm();
/** We do not want to seatch for every keystroke so lets debounce the seach string change for 300 ms */
const debouncedSearchTerm = useDebounce(searchTerm, 300);
/** Create a AbortController so we can cancle any ongoing search request later on */
const abortSignalRef = React.useRef(new AbortController());
/** Create a state to keep track of if the user is searching or not, we can not
* use `notifyOnNetworkStatusChange` since that will set `data` to null while searching */
const [isSearching, setIsSearching] = React.useState(false);
/** Keep a ref of the initial search string; so we get the right cache when the user navigates back to the list */
const searchTermRef = React.useRef(searchTerm);
const { error, data, fetchMore, refetch } = useQuery(GET_POST_LIST, {
variables: {
limit: 10,
offset: 0,
search: searchTermRef.current || null,
},
context: {
fetchOptions: {
// We must pass signal as a geter. Otherwise can we only cancle the request once
get signal() {
return abortSignalRef.current.signal;
},
},
},
// This must be false, otherwise will `data` be null when calling `refetch`.
notifyOnNetworkStatusChange: false,
});
/**
* 1. Cancle the ongoing request if any
* 2. Start a new search when the debounced string changes
*/
useUpdateEffect(() => {
abortSignalRef.current = new AbortController();
const search = debouncedSearchTerm === '' ? null : debouncedSearchTerm;
refetch({
search,
offset: 0,
}).then((): void => setIsSearching(false));
return (): void => abortSignalRef.current.abort();
}, [debouncedSearchTerm]);
// Set `isSearching` to true when changing the search string
useUpdateEffect(() => {
setIsSearching(true);
}, [searchTerm]);
if (error) {
return <p>{JSON.stringify(error)}</p>;
} else if (!data) {
return <p>Loading...</p>;
}
const loadMore = () => {
fetchMore({
variables: {
offset: data.posts.offset + data.posts.limit,
},
});
};
return (
<div>
<input
type="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{isSearching && <p>Is Searching...</p>}
<ul>
{data.posts.posts.map((post) => (
<li key={post.id}>
<Link to={`/post/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
<button onClick={() => loadMore()}>Load more</button>
</div>
);
}
It feels however that I’m not using @apollo/client
as I should so now to my questions:
Abort signal
Is this the best way of allowing the user to abort a request:
const abortSignalRef = React.useRef(new AbortController());
const { error, data, fetchMore, refetch } = useQuery(GET_POST_LIST, {
...
context: {
fetchOptions: {
get signal() {
return abortSignalRef.current.signal;
},
},
},
});
// later on
abortSignalRef.current.abort();
Can I expect this to work in a later (non-breaking-change) release?
How can I test this? I see that on this page: Testing React components - Apollo GraphQL Docs that you can mock the data. Would it be possible to return a Promise instead of raw data and get access to the context to be able to test this?
Also, is there a reason why useQuery
and fetchMore
accept a context
but not refetch
?
Seaching state
When setting notifyOnNetworkStatusChange
to true, data
will be null
when calling refetch
but if notifyOnNetworkStatusChange
is false data will keep the value until we have a new value. Is this expected?
Keep search state when re-mount the component
Let’s say the user search for something. The user then clicks on an item to navigate to a new page. When the user navigates back I want to keep the search result.
The only solution I come up with was to get a separate state for the search string on the first render:
const searchTermRef = React.useRef(searchTerm);
const { error, data, fetchMore, refetch } = useQuery(GET_POST_LIST, {
variables: {
limit: 10,
offset: 0,
search: searchTermRef.current,
},
}
Does it exist a better/intuitive way?
I have created a simple GraphQL server to demonstrate this here: https://dash.deno.com/playground/windy-hawk-11
I also created a simple web app here: https://stackblitz.com/edit/react-ts-psfqir?file=PostList.tsx
The back and forth navigation does not work completely when sowing the editor in stackblitz so if you want to test the navigation use this link instead: https://react-ts-psfqir.stackblitz.io
Thanks!