Last active
June 7, 2023 23:13
-
-
Save glamp/8e8e804cda6a4937f98c2b3b012cc345 to your computer and use it in GitHub Desktop.
This file contains 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
const [widgets, setWidgets] = React.useState<Widget[]>([]); | |
const [animals, setAnimals] = React.useState<Animal[]>([]); | |
useSyncToTable<Widget[]>({ | |
thingToWatch: widgets, | |
tableName: "widgets", | |
debounce: 500, | |
}); | |
useSyncToTable<Animal[]>({ | |
thingToWatch: pets, | |
tableName: "animals", | |
debounce: 500, | |
}); |
This file contains 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 { arraysEqual, objectsEqual } from "./utils"; | |
import { useApi } from "../containers/ApiContainer"; | |
import { useDebounce } from "react-use"; | |
import { useSchema } from "../containers/SchemaContainer"; | |
import { useState } from "react"; | |
// this is a helper function to make sure we're always dealing with arrays. | |
export const asArray = (thing: any) => { | |
if (!thing) { | |
return []; | |
} | |
if (Array.isArray(thing)) { | |
return thing; | |
} | |
return [thing]; | |
}; | |
interface Props<T> { | |
thingToWatch: T; | |
tableName: string; | |
onUpdate?: (data: any[]) => void; | |
updateDatabase?: (data: any[]) => Promise<void>; | |
deleteFromDatabase?: (id: string) => Promise<void>; | |
debounce?: number; | |
} | |
export const useSyncToTable = <T>({ | |
thingToWatch, | |
tableName, | |
onUpdate, | |
updateDatabase, | |
deleteFromDatabase, | |
debounce = 0, | |
}: Props<T>): Array<T> => { | |
// we're going to track arrays. so even thingToWatch is an object, we'll put it in an array. | |
const arrayToWatch = Boolean(thingToWatch) ? asArray(thingToWatch) : null; | |
// we'll use this to track the array we're watching in memory. sometimes we get null data as input. we will | |
// go ahead and filter that out. | |
const [array, setArray] = useState( | |
Boolean(arrayToWatch) ? arrayToWatch.filter((x) => x) : null | |
); | |
const { postgrest, queue } = useApi(); | |
const { schema, loading: isSchemaLoading } = useSchema(); | |
// this is our main watcher. we're going to look for changes between the | |
// in memory array and the prop arrayToWatch. | |
useDebounce( | |
() => { | |
// load ze metadata! | |
if (isSchemaLoading) { | |
return; | |
} | |
// if thing to watch is null, we don't need to do anything yet. | |
if (!thingToWatch) { | |
return; | |
} | |
// the first time we see the arrayToWatch, we want to set the array. we'll consider this the initial state. | |
if (!array) { | |
setArray(arrayToWatch); | |
return; | |
} | |
(async () => { | |
// we only want to be looking at the fields that are in the table we're syncing to | |
// grab those fields from the objects in the array. if the field doesn't exist, set it to null. | |
const extractTableFields = (obj: any) => { | |
return Object.fromEntries( | |
schema[tableName].map((field) => [field, obj[field] ?? null]) | |
); | |
}; | |
// now grab the table fields for both the arrayToWatch and the tracked | |
// array we have in memory within the hook. | |
const arrayToWatchWithTableFields = arrayToWatch.map( | |
extractTableFields | |
); | |
const arrayTopLevelWithTableFields = array.map(extractTableFields); | |
// if the arrays are not equal, we need to update the database | |
if ( | |
!arraysEqual( | |
arrayToWatchWithTableFields, | |
arrayTopLevelWithTableFields | |
) | |
) { | |
// default update will be to do a bulk upsert | |
const updateDatabaseDefault = async (data: any[]) => { | |
try { | |
await postgrest | |
?.from(tableName) | |
.upsert(data, { returning: "minimal" }); | |
} catch (error) { | |
console.error(`Error updating ${tableName} table: ${error}`); | |
} | |
}; | |
// default delete will be to delete by id | |
const deleteFromDatabaseDefault = async (id: any) => { | |
try { | |
await postgrest | |
?.from(tableName) | |
.delete() | |
.eq("id", id); | |
} catch (error) { | |
console.error( | |
`Error deleting ${id} from ${tableName} table: ${error}` | |
); | |
} | |
}; | |
// find items that have been updated | |
const updatedItems = arrayToWatchWithTableFields.filter((item) => { | |
const existingItem = arrayTopLevelWithTableFields.find( | |
({ id }) => id === item.id | |
); | |
return !existingItem || !objectsEqual(existingItem, item); | |
}); | |
// and find items that have been removed from the watch array | |
const deletedItems = arrayTopLevelWithTableFields.filter( | |
(item) => | |
arrayToWatchWithTableFields | |
.map(({ id }) => id) | |
.indexOf(item.id) === -1 | |
); | |
console.log( | |
`Updating ${tableName} table => updates: ${updatedItems.length}, deletes: ${deletedItems.length}` | |
); | |
if (updatedItems.length) { | |
// update items that have been updated in the watch array | |
queue.addFunction(async () => | |
Boolean(updateDatabase) | |
? updateDatabase(updatedItems) | |
: updateDatabaseDefault(updatedItems) | |
); | |
} | |
// if we have an onUpdate function, call it with the updated items | |
if (updatedItems.length && Boolean(onUpdate)) { | |
onUpdate(updatedItems); | |
} | |
if (deletedItems.length) { | |
// delete items that have been removed from the watch array | |
queue.addFunction( | |
async () => | |
await Promise.all( | |
deletedItems.map((item) => | |
Boolean(deleteFromDatabase) | |
? deleteFromDatabase(item.id) | |
: deleteFromDatabaseDefault(item.id) | |
) | |
) | |
); | |
} | |
setArray(arrayToWatch); | |
} | |
})(); | |
}, | |
// optional debounce time. defaults to 0. | |
debounce, | |
[arrayToWatch] | |
); | |
// we'll return the array we're watching in memory, though likely won't need it. | |
return array; | |
}; | |
export default useSyncToTable; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment