Skip to content

Instantly share code, notes, and snippets.

@glamp
Last active June 7, 2023 23:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save glamp/8e8e804cda6a4937f98c2b3b012cc345 to your computer and use it in GitHub Desktop.
Save glamp/8e8e804cda6a4937f98c2b3b012cc345 to your computer and use it in GitHub Desktop.
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,
});
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