The fastest, production-ready DX & UX for Firestore.
- instant UI. All data mutations run synchronously, optimistic in-memory.
- zero Redux boilerplate. no reducers, no slices, no selectors, no entity mappers, no normalization
- data-driven testing. no boilerplate, no mocks, no spys, seemlessly switch between unit & integation tests
- offline-first NoSQL. with live subscriptions
[![License][license-image]][license-url]
useRead({ path, ...query })
Query & load & subscribe to live updates from Firestore.
const tasks = useRead({
path: 'tasks',
where: [
['status', '==', 'done'],
['assignee', '==', myUID]
],
orderBy: ['createdAt', 'desc'],
});
createMutate({ action, read, write })
Create a Redux action creator to create, update & delete data. Mutations synchrnously update the Redux store making React components feel instant.
const archiveTask = createMutate({
action: 'ArchiveTask',
read: (taskId) => ({ taskId: () => taskId }),
write: ({ taskId }) => ({
path:'tasks',
id: taskId,
archived: true
}),
});
Action creators return a promise when Firestore accepts or rejects your mutation.
useDispatch(archiveTask('task-one'))
.then(() => alert('task archived.'));
it.each([{ payload, mutation, returned }])(...shouldPass)
it.each([{ payload, returned }])(...shouldFail)
Zero bolierplate testing. No mocks or spies; just data. Instantly switch between unit & integration tests.
import { archiveTask } from '../mutations';
const RUN_AS_INTEGRATION = false; // 'true' runs loads/saves to Firestore in parallel
it.each({
payload: { taskId: '99' },
setup: [{
id: '99',
path: 'tasks',
archived: false,
title: 'sample'
}],
results: [{
id: '99',
path: 'tasks',
archived: true,
title: 'sample'
}],
})(...shouldPass(archiveTask, RUN_AS_INTEGRATION));
it.each([{
payload: { taskId: 'not-valid-id' },
returned: new Error('Document not found.'),
}])(...shouldFail(archiveTask, RUN_AS_INTEGRATION));
setCache({[alias]: [DocumentOne, DocumentTwo]});
Storybook tests are as simple as providing the data that should return to the useRead & useCache calls.
const cache = setCache({
myAlias: [
{ path:'tasks', id:'task-one', title: 'test task' }
],
});
export const Default = (): JSX.Element => (
<Provider store={cache}>
<TaskList />
</Provider>
);
API Documentation
Code deep-dives
Design Fundamentals
- Add the libraries to your project.
yarn add read-write-web3 firebase @reduxjs/toolkit redux
- Include the firestore/firebase reducers and thunk middleware.
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import {
getFirebase,
getFirestore,
firebaseReducer,
firestoreReducer,
} from 'read-write-firestore';
import thunk from 'redux-thunk';
import firebase from 'firebase/compat/app';
// Create store with reducers and initial state
export const store = configureStore({
// Add Firebase to reducers
reducer: combineReducers({
firebase: firebaseReducer,
firestore: firestoreReducer,
}),
middleware: [
thunk.withExtraArgument({
getFirestore,
getFirebase,
}),
],
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
- Initialize Firebase and pass store to your component's context using react-redux's
Provider
:
import React from 'react';
import { render } from 'react-dom';
import App from './App';
import { store } from './app/store';
import { Provider } from 'react-redux';
import {
ReactReduxFirebaseProvider,
createFirestoreInstance,
} from 'read-write-firestore';
import firebase from 'firebase/compat/app';
import 'firebase/compat/firestore';
import 'firebase/compact/auth';
const firebaseApp = firebase.initializeApp({
authDomain: process.env.REACT_APP_FIREBASE_authDomain,
databaseURL: process.env.REACT_APP_FIREBASE_databaseUrl,
projectId: process.env.REACT_APP_FIREBASE_projectId,
});
render(
<Provider store={store}>
<ReactReduxFirebaseProvider
firebase={firebaseApp}
dispatch={store.dispatch}
createFirestoreInstance={createFirestoreInstance}
>
<App />
</ReactReduxFirebaseProvider>
</Provider>,
document.querySelector('body'),
);