Last active
May 18, 2025 10:31
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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