Skip to content

Instantly share code, notes, and snippets.

@Gergling
Last active May 18, 2025 10:31
Show Gist options
  • Save Gergling/982f591c7f4ad1f22cc0da64fc2112ba to your computer and use it in GitHub Desktop.
Save Gergling/982f591c7f4ad1f22cc0da64fc2112ba to your computer and use it in GitHub Desktop.
This is a PoC for a solution to a problem detailed in a https://medium.com/@gregmichaeldavies/fetching-array-updates-in-react-b5acd65e033c.
import { useQueries, useQuery } from "@tanstack/react-query";
import { useEffect, useReducer, useState } from "react";
// We're going to be listing items of this type:
type Item = {
base: {
id: number;
name: string;
speed: number;
};
progress: number;
embellishment?: {
shiny: number;
sweet: number;
colour: string;
};
};
// ... Yes, it is nonsense.
type ItemProgress = {
id: number;
progress: number;
}
type Status = 'pending' | 'in-progress' | 'completed';
const getStatus = ({
progress
}: Item): Status => progress > 100
? 'completed'
: progress > 0
? 'in-progress'
: 'pending';
const getProgress = ({ progress }: Item) => progress / 100;
const fetchList = () => new Promise<Item[]>((resolve) => {
const items: Item[] = Array
.from({ length: 5 })
.map((_, id) => ({
base: {
id,
name: `Item ${id}`,
speed: Math.random() * 40,
},
progress: 0,
}));
setTimeout(() => {
resolve(items);
}, 100);
});
const fetchProgress = (
item: Item
) => new Promise<ItemProgress>((resolve) => {
setTimeout(() => {
resolve({
id: item.base.id,
progress: item.progress + item.base.speed,
});
}, 100);
});
const fetchEmbellishment = (
item: Item
) => new Promise<Item>((resolve) => {
setTimeout(() => {
resolve({
...item,
embellishment: {
shiny: Math.random(),
sweet: Math.random(),
colour: Math.random().toString(),
}
});
}, 100);
});
const useInitialList = () => useQuery({
queryFn: fetchList,
queryKey: ['list'],
});
const useProgress = (list: Item[]) => useQueries({
queries: (list || [])
.filter((item) => getStatus(item) !== 'completed')
.map((item) => {
return {
queryFn: () => fetchProgress(item),
queryKey: ['progress', item.base.id],
refetchInterval: 1000,
};
}),
});
const useEmbellishments = (list: Item[]) => useQueries({
queries: (list || [])
.filter((item) => {
return getStatus(item) === 'completed' && !item.embellishment;
})
.map((item) => {
return {
queryFn: () => fetchEmbellishment(item),
queryKey: ['embellish', item.base.id]
};
}),
});
// SOLUTION 1: The hasChanges solution.
const useListHasChanges = () => {
const [list, updateList] = useState<Item[]>([]);
const { data: initialList } = useInitialList();
// Update progress for anything which hasn't completed yet.
const progress = useProgress(list);
// Load up data embellishment.
const embellishments = useEmbellishments(list);
// We update the state by checking for changes.
useEffect(() => {
// Yuck.
let hasChanges = false;
const updatedList = list.map((item): Item => {
const status = getStatus(item);
// If we already have our data, we don't care about any further changes.
if (status === 'completed' && item.embellishment) {
return item;
}
// Let's see if our embellishment for this data has loaded up.
const embellishedItem = embellishments.find(({ data }) => data?.base.id === item.base.id);
if (embellishedItem && embellishedItem.data) {
// If so, we set the change flag (yuck) and update the list.
hasChanges = true;
return {
...item,
embellishment: embellishedItem.data.embellishment,
};
}
// We didn't have the embellishment data for this item.
// Let's see if we have a progress report.
const itemProgress = progress.find(({ data }) => data?.id === item.base.id);
if (itemProgress && itemProgress.data && itemProgress.data.progress !== item.progress) {
// If so, we set the change flag (still yuck) and update the list.
hasChanges = true;
return {
...item,
progress: itemProgress.data.progress,
};
}
return item;
});
// Yuck 3: Return of the Yuck.
if (hasChanges) {
updateList(updatedList);
}
}, [embellishments, list, progress]);
useEffect(() => {
if (initialList) {
updateList(initialList);
}
}, [initialList]);
return {
list,
}
}
// SOLUTION 2: The reducer solution.
type ReducerAction = {
type: 'initialise';
items: Item[];
} | ItemProgress & {
type: 'progress';
} | {
type: 'embellish';
id: Item['base']['id'];
embellishment: Required<Item>['embellishment'];
}
const reducer = (state: Item[], action: ReducerAction) => {
switch (action.type) {
case 'initialise':
return action.items;
case 'embellish':
return state.map((item) => {
if (action.id === item.base.id) {
return {
...item,
embellishment: action.embellishment,
};
}
return item;
});
default:
return state.map((item) => {
if (action.id === item.base.id) {
return {
...item,
progress: action.progress
};
}
return item;
});
}
}
const useListReducer = () => {
const [list, updateList] = useReducer<typeof reducer>(reducer, []);
const { data: initialList } = useInitialList();
// Update progress for anything which hasn't completed yet.
const progress = useProgress(list || []);
// Load up data embellishment.
const embellishments = useEmbellishments(list || []);
// We update the state by checking for changes.
useEffect(() => {
if (list) {
list.forEach((item) => {
// Let's see if our embellishment for this data has loaded up.
const embellishedItem = embellishments.find(({ data }) => data?.base.id === item.base.id);
if (embellishedItem?.data?.embellishment) {
const { data: { base: { id }, embellishment } } = embellishedItem
// We know we have new data to embellish, and we just dispatch.
updateList({ type: 'embellish', embellishment, id });
}
// We didn't have the embellishment data for this item.
// Let's see if we have a progress report.
const itemProgress = progress.find(({ data }) => data?.id === item.base.id);
if (itemProgress?.data && itemProgress.data.progress !== item.progress) {
// We know we want to update the progress, so we dispatch.
updateList({ type: 'progress', ...itemProgress.data })
}
});
}
}, [embellishments, list, progress]);
useEffect(() => {
if (initialList) {
updateList({ type: 'initialise', items: initialList });
}
}, [initialList]);
return {
list,
};
};
// Components.
const ListItem = ({ item }: { item: Item }) => {
const { base: { name }, embellishment } = item;
const status = getStatus(item);
const progress = getProgress(item);
return (
<tr>
<td>{name}</td>
{status === 'completed' && embellishment ? (
<>
<td>{embellishment.shiny}</td>
<td>{embellishment.sweet}</td>
<td>{embellishment.colour}</td>
</>
) : (
status === 'pending' ? (
<td>Not started</td>
) : (
<td><progress value={progress} /></td>
)
)}
</tr>
);
}
const Table = ({ list }: { list: Item[] }) => {
return (
<table>
<tbody>
<tr>
<th>Name</th>
<th>Shiny</th>
<th>Sweet</th>
<th>Colour</th>
</tr>
{
list.map((item) => {
return (
<ListItem key={item.base.id} item={item} />
);
})
}
</tbody>
</table>
);
}
export const List = () => {
const { list: listHasChanges } = useListHasChanges();
const { list: listReducer } = useListReducer();
return (
<>
useEffect list:
<Table list={listHasChanges} />
useReducer list:
<Table list={listReducer} />
</>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment