Definedā„¢
Definedā„¢ā€¢2w ago

Sync local database to Convex database

Hi, I have been using Convex for a month, it's very good. However, Im developing a mobile app with Expo, I want to sync data in my app local storage to Convex but I don't know how. Furthermore, can u also suggest me what lcoal database should I use (WatermelonDB, rxdB, etc.)
5 Replies
jamalsoueidan
jamalsoueidanā€¢2w ago
Convex is working on a solution for offline sync data! Right now you have to do it yourself šŸ™‚
Definedā„¢
Definedā„¢OPā€¢2w ago
yeah but I dont know where I should start from I have 1 week left to finish my school project
erquhart
erquhartā€¢2w ago
Local first with Convex is currently non-trivial. Is local data storage a hard requirement for your project? If it is, can you share just a little about your use case, or is it just standard offline-first
MapleLeaf šŸ
MapleLeaf šŸā€¢2w ago
I'm on a phone and told Claude to write this, but this is how I'd go about it
import { useQuery, useMutation } from 'convex/react';
import { useEffect, useState } from 'react';
import type {
OptionalQueryArgs,
OptionalQueryArgsOrSkip,
FunctionArgs,
FunctionResult,
FunctionReference
} from 'convex/server';

/**
* Hook that wraps Convex useQuery with local storage caching
* @param key - Local storage key to use for caching
* @param queryFn - Convex query function reference
* @param args - Arguments to pass to query function
* @returns Data from local storage while loading, then remote data
*/
export function useQueryWithLocalStorage<Query extends FunctionReference<'query', 'public'>>(
key: string,
queryFn: Query,
...args: OptionalQueryArgsOrSkip<Query>
): FunctionResult<Query> | null {
// Get initial data from local storage
const [localData, setLocalData] = useState<FunctionResult<Query> | null>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : null;
});

// Get remote data
const remoteData = useQuery(queryFn, ...args);

// Update local storage when remote data changes
useEffect(() => {
if (remoteData !== undefined) {
localStorage.setItem(key, JSON.stringify(remoteData));
setLocalData(remoteData);
}
}, [key, remoteData]);

// Return local data while loading, then remote data
return remoteData === undefined ? localData : remoteData;
}

/**
* Hook that wraps Convex useMutation with optimistic local storage updates
* @param key - Local storage key to update
* @param mutationFn - Convex mutation function reference
* @returns Wrapped mutation function that handles local storage updates
*/
export function useMutationWithLocalStorage<Mutation extends FunctionReference<'mutation', 'public'>>(
key: string,
mutationFn: Mutation
): (args: FunctionArgs<Mutation>) => Promise<FunctionResult<Mutation>> {
const mutation = useMutation(mutationFn);

const wrappedMutation = async (args: FunctionArgs<Mutation>) => {
// Get current data
const currentData = localStorage.getItem(key);
const parsedData = currentData ? JSON.parse(currentData) : null;

try {
// Optimistically update local storage
if (parsedData) {
// Assuming the mutation affects the data in a predictable way
// You may need to customize this based on your mutation logic
const optimisticData = Array.isArray(parsedData)
? [...parsedData, args] // If array, append new item
: { ...parsedData, ...args }; // If object, merge new data

localStorage.setItem(key, JSON.stringify(optimisticData));
}

// Call remote mutation
const result = await mutation(args);
return result;

} catch (error) {
// Revert local storage on error
if (currentData) {
localStorage.setItem(key, currentData);
}
throw error;
}
};

return wrappedMutation;
}
import { useQuery, useMutation } from 'convex/react';
import { useEffect, useState } from 'react';
import type {
OptionalQueryArgs,
OptionalQueryArgsOrSkip,
FunctionArgs,
FunctionResult,
FunctionReference
} from 'convex/server';

/**
* Hook that wraps Convex useQuery with local storage caching
* @param key - Local storage key to use for caching
* @param queryFn - Convex query function reference
* @param args - Arguments to pass to query function
* @returns Data from local storage while loading, then remote data
*/
export function useQueryWithLocalStorage<Query extends FunctionReference<'query', 'public'>>(
key: string,
queryFn: Query,
...args: OptionalQueryArgsOrSkip<Query>
): FunctionResult<Query> | null {
// Get initial data from local storage
const [localData, setLocalData] = useState<FunctionResult<Query> | null>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : null;
});

// Get remote data
const remoteData = useQuery(queryFn, ...args);

// Update local storage when remote data changes
useEffect(() => {
if (remoteData !== undefined) {
localStorage.setItem(key, JSON.stringify(remoteData));
setLocalData(remoteData);
}
}, [key, remoteData]);

// Return local data while loading, then remote data
return remoteData === undefined ? localData : remoteData;
}

/**
* Hook that wraps Convex useMutation with optimistic local storage updates
* @param key - Local storage key to update
* @param mutationFn - Convex mutation function reference
* @returns Wrapped mutation function that handles local storage updates
*/
export function useMutationWithLocalStorage<Mutation extends FunctionReference<'mutation', 'public'>>(
key: string,
mutationFn: Mutation
): (args: FunctionArgs<Mutation>) => Promise<FunctionResult<Mutation>> {
const mutation = useMutation(mutationFn);

const wrappedMutation = async (args: FunctionArgs<Mutation>) => {
// Get current data
const currentData = localStorage.getItem(key);
const parsedData = currentData ? JSON.parse(currentData) : null;

try {
// Optimistically update local storage
if (parsedData) {
// Assuming the mutation affects the data in a predictable way
// You may need to customize this based on your mutation logic
const optimisticData = Array.isArray(parsedData)
? [...parsedData, args] // If array, append new item
: { ...parsedData, ...args }; // If object, merge new data

localStorage.setItem(key, JSON.stringify(optimisticData));
}

// Call remote mutation
const result = await mutation(args);
return result;

} catch (error) {
// Revert local storage on error
if (currentData) {
localStorage.setItem(key, currentData);
}
throw error;
}
};

return wrappedMutation;
}
you'd have to make sure your key is unique depending on the function args you pass; you could automatically compute a key from the function reference and the args, but i wanted to keep it simple this also assumes the data passed to the mutation is the same as what you get from the query, so uh... :kek: probably need to add like a localData arg to the mutation function or something
Definedā„¢
Definedā„¢OPā€¢2w ago
dammn, so advanced, anyways thanks u