Skip to content

Instantly share code, notes, and snippets.

@ryansukale
Last active May 15, 2023 11:32
Show Gist options
  • Save ryansukale/f217810e36dd697e319ce649d4624146 to your computer and use it in GitHub Desktop.
Save ryansukale/f217810e36dd697e319ce649d4624146 to your computer and use it in GitHub Desktop.
Pagination using urql
import { get, set } from "lodash";
import { produce } from "immer";
export default class Paginator {
path: string;
limit: number;
constructor({ path, limit }) {
this.path = path;
this.limit = limit;
}
getCanLoadMore = (data) => {
if (!data) {
return true;
}
const { limit, path } = this;
const items = get(data, path, []);
return items.length === limit;
};
getInitialValue = () => {
return set({}, this.path, []);
};
onDataLoaded = (existingData, newData) => {
const { path } = this;
const newItems = get(newData, path, []);
const existingItems = get(existingData, path, []);
const nextState = produce(newData, (draft) => {
set(draft, path, existingItems.concat(newItems));
});
return nextState;
};
}
import { useMemo } from "react";
import { useQuery } from "urql";
export default function useFetchData(args) {
const [result, refresh] = useQuery(args);
return useMemo(
() => ({
...result,
actions: {
refresh,
hardRefresh: () => {
refresh({ requestPolicy: "network-only" });
},
},
}),
[result, refresh]
);
}
import { useCallback, useState, useEffect, useMemo } from "react";
import { dequal } from "dequal";
import usePrevious from "lib/hooks/usePrevious";
import useFetchData from "lib/hooks/useFetchData";
export default function usePaginatedFetchData(
args,
{ getInitialValue, onDataLoaded, getCanLoadMore, limit = 10 }
) {
const { variables, ...rest } = args;
const [offset, setOffset] = useState(0);
const [data, setData] = useState(getInitialValue);
const [isInitialLoadDone, setIsInitialLoadDone] = useState(true);
const [loadMoreState, setLoadMoreState] = useState({
isRequested: false,
isDone: false,
});
const req = useFetchData({
requestPolicy: "cache-and-network",
...rest,
variables: {
...variables,
offset,
limit,
},
});
const initialVariables = usePrevious(variables);
const hasDifferentVariables = !dequal(initialVariables, variables);
useEffect(() => {
if (req.fetching || req.stale) {
return;
}
if (!isInitialLoadDone) {
setIsInitialLoadDone(true);
setData((d) => onDataLoaded(d, req.data));
return;
}
if (loadMoreState.isRequested) {
setLoadMoreState({
isRequested: false,
isDone: true,
});
setData((d) => onDataLoaded(d, req.data));
}
}, [
req.data,
req.fetching,
req.stale,
onDataLoaded,
loadMoreState,
isInitialLoadDone,
]);
useEffect(() => {
if (hasDifferentVariables) {
setData(getInitialValue);
setIsInitialLoadDone(false);
setLoadMoreState({
isRequested: false,
isDone: false,
});
setOffset(0);
}
}, [hasDifferentVariables, getInitialValue]);
const loadMore = useCallback(() => {
setOffset((offset) => offset + limit);
setLoadMoreState({
isRequested: true,
isDone: false,
});
}, [limit]);
const actions = useMemo(
() => ({ ...req.actions, loadMore }),
[req.actions, loadMore]
);
return useMemo(
() => ({
...req,
data,
isInitialLoadDone,
canLoadMore: isInitialLoadDone ? getCanLoadMore(req.data) : true,
isLoadMoreRequested: loadMoreState.isRequested,
isLoadMoreDone: loadMoreState.isDone,
actions,
}),
[req, data, isInitialLoadDone, getCanLoadMore, loadMoreState, actions]
);
}
import { useRef, useEffect } from "react";
// Taken from https://usehooks.com/usePrevious/
export default function usePrevious<T>(value: T): T {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef<T> ();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment