A lightweight, type-safe remote data store for React applications with React Query integration.
- 🔒 Type-safe data operations
- 🔄 Built-in React Query integration
- 🎣 Simple hooks-based API
- 🔌 Extensible adapter system
- 📦 Zero config setup
- 🎯 Focused mutations API
npm install @asaidimu/remote-store @tanstack/react-query
# or
yarn add @asaidimu/remote-store @tanstack/react-query
# or
bun add @asaidimu/remote-store @tanstack/react-query
# or
pnpm add @asaidimu/remote-store @tanstack/react-query
- Create your store adapter:
// adapters/rest.adapter.ts
import { BaseStore, RemoteStoreRecord } from '@asaidimu/remote-store';
export class RestAdapter<T extends RemoteStoreRecord> implements BaseStore<T> {
constructor(private baseUrl: string) {}
async find({ filter }: { filter: string }) {
const response = await fetch(`${this.baseUrl}?filter=${filter}`);
return response.json();
}
async read({ id }: { id: string }) {
const response = await fetch(`${this.baseUrl}/${id}`);
return response.json();
}
async list({ filter = '', page = 1, pageSize = 10 }) {
const response = await fetch(
`${this.baseUrl}?filter=${filter}&page=${page}&pageSize=${pageSize}`
);
return response.json();
}
async create({ data }: { data: Omit<T, 'id'> }) {
const response = await fetch(this.baseUrl, {
method: 'POST',
body: JSON.stringify(data),
});
return response.json();
}
async update({ id, data }: { id: string; data: Partial<T> }) {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
return response.json();
}
async delete({ id }: { id: string }) {
await fetch(`${this.baseUrl}/${id}`, { method: 'DELETE' });
}
async upload({ id, field, file }: { id: string; field: string; file: File }) {
const formData = new FormData();
formData.append(field, file);
const response = await fetch(`${this.baseUrl}/${id}/upload`, {
method: 'POST',
body: formData,
});
return response.json();
}
}
- Create your store:
// stores/user.store.ts
import { useRemoteStore } from '@asaidimu/remote-store';
import { RestAdapter } from '../adapters/rest.adapter';
import { useQueryClient } from '@tanstack/react-query';
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
export function useUserStore() {
const queryClient = useQueryClient();
const adapter = new RestAdapter<User>('https://api.example.com/users');
return useRemoteStore({
store: adapter,
queryClient,
})('users');
}
- Use in your components:
// components/UserList.tsx
import { useUserStore } from '../stores/user.store';
export function UserList() {
const store = useUserStore();
// Setup mutations
const createUser = store.create({
onSuccess: (user) => console.log('Created:', user),
onError: (error) => console.error('Failed:', error)
});
const updateUser = store.update({
onSuccess: (user) => console.log('Updated:', user)
});
// Query data
const { data: users, isLoading } = store.list({
pageSize: 20
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{users?.map(user => (
<div key={user.id}>
{user.name}
<button onClick={() => updateUser(user.id, { name: 'New Name' })}>
Update
</button>
</div>
))}
<button onClick={() => createUser({
name: 'New User',
email: 'new@example.com'
})}>
Add User
</button>
</div>
);
}
Main hook for creating a store instance.
function useRemoteStore<R extends RemoteStoreRecord>({
store: BaseStore<R>,
queryClient: QueryClient
}): (collection: string) => Store<R>
-
find({ filter, options? })
- Find a single record by filter -
read({ id, options? })
- Read a single record by ID -
list({ filter?, page?, pageSize?, options? })
- List multiple records
-
create(callbacks?) => (data) => void
- Create a new record -
update(callbacks?) => (id, data) => void
- Update an existing record -
delete(callbacks?) => (id) => void
- Delete a record -
upload(callbacks?) => (id, field, file) => void
- Upload a file
interface RemoteStoreRecord {
readonly id: string;
[key: string]: unknown;
}
interface BaseStore<T extends RemoteStoreRecord> {
readonly find: (props: { filter: string; options?: QueryOptions }) => Promise<T>;
readonly read: (props: { id: string; options?: QueryOptions }) => Promise<T>;
readonly list: (props: ListOptions) => Promise<T[]>;
readonly update: (props: { id: string; data: Partial<T> }) => Promise<T>;
readonly delete: (props: { id: string }) => Promise<void>;
readonly create: (props: { data: Omit<T, 'id'> }) => Promise<T>;
readonly upload: (props: { id: string; field: string; file: File }) => Promise<T>;
}
You can create adapters for any data source by implementing the BaseStore
interface. Here are some examples:
import { BaseStore, RemoteStoreRecord } from '@asaidimu/remote-store';
import { collection, doc, getDoc, getDocs, query, where } from 'firebase/firestore';
export class FirebaseAdapter<T extends RemoteStoreRecord> implements BaseStore<T> {
constructor(
private db: Firestore,
private collectionName: string
) {}
async find({ filter }) {
const q = query(
collection(this.db, this.collectionName),
where('field', '==', filter)
);
const snapshot = await getDocs(q);
return snapshot.docs[0].data() as T;
}
async read({ id }) {
const docRef = doc(this.db, this.collectionName, id);
const snapshot = await getDoc(docRef);
return snapshot.data() as T;
}
// ... implement other methods
}
import { BaseStore, RemoteStoreRecord } from '@asaidimu/remote-store';
import { SupabaseClient } from '@supabase/supabase-js';
export class SupabaseAdapter<T extends RemoteStoreRecord> implements BaseStore<T> {
constructor(
private supabase: SupabaseClient,
private tableName: string
) {}
async find({ filter }) {
const { data, error } = await this.supabase
.from(this.tableName)
.select()
.match(filter)
.single();
if (error) throw error;
return data;
}
async create({ data }) {
const { data: created, error } = await this.supabase
.from(this.tableName)
.insert(data)
.single();
if (error) throw error;
return created;
}
// ... implement other methods
}
We welcome contributions! Please see our contributing guide for details.
MIT