A Redux-based data management library that provides a uniform way to access, fetch, update, and manage application data with minimal boilerplate code.
npm i @recats/cdeebee@beta
# or
yarn add @recats/cdeebee@beta
# or
pnpm add @recats/cdeebee@betacdeebee is a data management library built on top of Redux Toolkit that aims to:
- Provide a uniform way of accessing data
- Decrease boilerplate code when working with data fetching, updating, committing, and rollbacking
- Manage normalized data structures similar to relational databases
- Handle API requests with built-in normalization, error handling, and request cancellation
cdeebee stores data in a normalized structure similar to a relational database. Data is organized into lists (tables) where each list is a JavaScript object with keys representing primary keys (IDs) of entities.
For example, a forum application might have this structure:
{
forumList: {},
threadList: {},
postList: {}
}After fetching data from the API (which returns data in the format { data: [...], primaryKey: 'id' }), cdeebee automatically normalizes it and the storage might look like:
{
forumList: {
1: { id: 1, title: 'Milky Way Galaxy' }
},
threadList: {
10001: { id: 10001, title: 'Solar system', forumID: 1 }
},
postList: {
2: { id: 2, title: 'Earth', threadID: 10001 }
}
}Note: The API should return list data in the format { data: [...], primaryKey: 'fieldName' }. cdeebee automatically converts this format into the normalized storage structure shown above. See the API Response Format section for details.
cdeebee uses a modular architecture with the following modules:
storage: Automatically normalizes and stores API responseshistory: Tracks request history (successful and failed requests)listener: Tracks active requests for loading statescancelation: Manages request cancellation (automatically cancels previous requests to the same API)queryQueue: Processes requests sequentially in the order they were sent, ensuring they complete and are stored in the correct sequence
import { configureStore, combineSlices } from '@reduxjs/toolkit';
import { factory } from '@recats/cdeebee';
// Define your storage structure
interface Storage {
forumList: Record<number, { id: number; title: string }>;
threadList: Record<number, { id: number; title: string; forumID: number }>;
postList: Record<number, { id: number; title: string; threadID: number }>;
}
// Create cdeebee slice
export const cdeebeeSlice = factory<Storage>(
{
modules: ['history', 'listener', 'cancelation', 'storage', 'queryQueue'],
fileKey: 'file',
bodyKey: 'value',
listStrategy: {
forumList: 'merge',
threadList: 'replace',
},
mergeWithData: {
sessionToken: 'your-session-token',
},
mergeWithHeaders: {
'Authorization': 'Bearer token',
},
},
{
// Optional initial storage state
forumList: {},
threadList: {},
postList: {},
}
);
// Combine with other reducers
const rootReducer = combineSlices(cdeebeeSlice);
export const store = configureStore({
reducer: rootReducer,
});import { request } from '@recats/cdeebee';
import { useAppDispatch } from './hooks';
function MyComponent() {
const dispatch = useAppDispatch();
const fetchForums = () => {
dispatch(request({
api: '/api/forums',
method: 'POST',
body: { filter: 'active' },
onResult: (result) => {
// onResult is always called with the response data
// For JSON responses, result is already parsed
console.log('Request completed:', result);
},
}));
};
return <button onClick={fetchForums}>Load Forums</button>;
}cdeebee provides React hooks to easily access data and track loading states:
import { useLoading, useStorageList } from '@recats/cdeebee';
function ForumsList() {
// Check if specific APIs are loading
const isLoading = useLoading(['/api/forums']);
// Get data from storage with full type safety
const forums = useStorageList<Storage, 'forumList'>('forumList');
return (
<div>
{isLoading && <p>Loading...</p>}
{Object.values(forums).map(forum => (
<div key={forum.id}>{forum.title}</div>
))}
</div>
);
}Available hooks:
useLoading(apiList: string[])- Check if any of the specified APIs are loadinguseIsLoading()- Check if any request is loadinguseStorageList(listName)- Get a specific list from storageuseStorage()- Get the entire storageuseRequestHistory(api)- Get successful request history for an APIuseRequestErrors(api)- Get error history for an API
See the React Hooks section for detailed documentation.
The factory function accepts a settings object with the following options:
interface CdeebeeSettings<T> {
modules: CdeebeeModule[]; // Active modules: 'history' | 'listener' | 'storage' | 'cancelation' | 'queryQueue'
fileKey: string; // Key name for file uploads in FormData
bodyKey: string; // Key name for request body in FormData
listStrategy?: CdeebeeListStrategy<T>; // Merge strategy per list: 'merge' | 'replace' | 'skip'
mergeWithData?: unknown; // Data to merge with every request body
mergeWithHeaders?: Record<string, string>; // Headers to merge with every request
normalize?: (storage, result, strategyList) => T; // Custom normalization function
}interface CdeebeeRequestOptions<T> {
api: string; // API endpoint URL
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
body?: unknown; // Request body
headers?: Record<string, string>; // Additional headers (merged with mergeWithHeaders)
files?: File[]; // Files to upload
fileKey?: string; // Override default fileKey
bodyKey?: string; // Override default bodyKey
listStrategy?: CdeebeeListStrategy<T>; // Override list strategy for this request
normalize?: (storage, result, strategyList) => T; // Override normalization
onResult?: (response: T) => void; // Callback called with response data (always called, even on errors)
ignore?: boolean; // Skip storing result in storage
responseType?: 'json' | 'text' | 'blob'; // Response parsing type (default: 'json')
historyClear?: boolean; // Auto-clear history for this API before making the request
}cdeebee supports three strategies for merging data:
merge: Merges new data with existing data, preserving existing keys not in the responsereplace: Completely replaces the list with new dataskip: Skips updating the list entirely, preserving existing data unchanged
listStrategy: {
forumList: 'merge', // New forums are merged with existing ones
threadList: 'replace', // Thread list is completely replaced
userList: 'skip', // User list is never updated, existing data is preserved
}cdeebee expects API responses in a format where list data is provided as arrays with a primaryKey field. The library automatically normalizes this data into the storage structure.
For lists (collections of entities), the API should return data in the following format:
{
forumList: {
data: [
{ id: 1, title: 'Forum 1' },
{ id: 2, title: 'Forum 2' },
],
primaryKey: 'id',
},
threadList: {
data: [
{ id: 101, title: 'Thread 1', forumID: 1 },
{ id: 102, title: 'Thread 2', forumID: 1 },
],
primaryKey: 'id',
}
}cdeebee automatically converts this format into normalized storage:
{
forumList: {
1: { id: 1, title: 'Forum 1' },
2: { id: 2, title: 'Forum 2' },
},
threadList: {
101: { id: 101, title: 'Thread 1', forumID: 1 },
102: { id: 102, title: 'Thread 2', forumID: 1 },
}
}The primaryKey field specifies which property of each item should be used as the key in the normalized structure. The primaryKey value is converted to a string to ensure consistent key types.
Example:
If your API returns:
{
sessionList: {
data: [
{
sessionID: 1,
token: 'da6ec385bc7e4f84a510c3ecca07f3',
expiresAt: '2034-03-28T22:36:09'
}
],
primaryKey: 'sessionID',
}
}It will be automatically normalized to:
{
sessionList: {
'1': {
sessionID: 1,
token: 'da6ec385bc7e4f84a510c3ecca07f3',
expiresAt: '2034-03-28T22:36:09'
}
}
}For non-list data (configuration objects, simple values, etc.), you can return them as regular objects:
{
config: {
theme: 'dark',
language: 'en',
},
userPreferences: {
notifications: true,
}
}These will be stored as-is in the storage.
const file = new File(['content'], 'document.pdf', { type: 'application/pdf' });
dispatch(request({
api: '/api/upload',
method: 'POST',
files: [file],
body: { description: 'My document' },
fileKey: 'file', // Optional: override default
bodyKey: 'metadata', // Optional: override default
}));By default, cdeebee parses responses as JSON. For other response types (CSV, text files, images, etc.), use the responseType option:
// CSV/text response
dispatch(request({
api: '/api/export',
responseType: 'text',
ignore: true, // Don't store in storage
onResult: (csvData) => {
// csvData is a string
downloadCSV(csvData);
},
}));
// Binary file (image, PDF, etc.)
dispatch(request({
api: '/api/image/123',
responseType: 'blob',
ignore: true,
onResult: (blob) => {
// blob is a Blob object
const url = URL.createObjectURL(blob);
setImageUrl(url);
},
}));
// JSON (default)
dispatch(request({
api: '/api/data',
// responseType: 'json' is default
onResult: (data) => {
console.log(data); // Already parsed JSON
},
}));Use the ignore option to prevent storing the response in storage while still receiving it in the onResult callback:
// Export CSV without storing in storage
dispatch(request({
api: '/api/export',
responseType: 'text',
ignore: true,
onResult: (csvData) => {
// Handle CSV data directly
downloadFile(csvData, 'export.csv');
},
}));// Global headers (in settings)
mergeWithHeaders: {
'Authorization': 'Bearer token',
'X-Custom-Header': 'value',
}
// Per-request headers (override global)
dispatch(request({
api: '/api/data',
headers: {
'Authorization': 'Bearer different-token', // Overrides global
'X-Request-ID': '123', // Additional header
},
}));When the cancelation module is enabled, cdeebee automatically cancels previous requests to the same API endpoint when a new request is made:
// First request
dispatch(request({ api: '/api/data', body: { query: 'slow' } }));
// Second request to same API - first one is automatically cancelled
dispatch(request({ api: '/api/data', body: { query: 'fast' } }));When the queryQueue module is enabled, all requests are processed sequentially in the order they were sent. This ensures that:
- Requests complete in the exact order they were dispatched
- Data is stored in the store in the correct sequence
- Even if a faster request is sent after a slower one, it will wait for the previous request to complete
This is particularly useful when you need to maintain data consistency and ensure that updates happen in the correct order.
// Enable queryQueue module
const cdeebeeSlice = factory<Storage>({
modules: ['history', 'listener', 'storage', 'queryQueue'],
// ... other settings
});
// Send multiple requests - they will be processed sequentially
dispatch(request({ api: '/api/data', body: { id: 1 } })); // Completes first
dispatch(request({ api: '/api/data', body: { id: 2 } })); // Waits for #1, then completes
dispatch(request({ api: '/api/data', body: { id: 3 } })); // Waits for #2, then completes
// Even if request #3 is faster, it will still complete last
// All requests are stored in the store in order: 1 → 2 → 3Note: The queryQueue module processes requests sequentially across all APIs. If you need parallel processing for different APIs, you would need separate cdeebee instances or disable the module for those specific requests.
You can manually update the storage using the set action:
import { batchingUpdate } from '@recats/cdeebee';
// Update multiple values at once
const updates = [
{ key: ['forumList', '1', 'title'], value: 'New Title' },
{ key: ['forumList', '1', 'views'], value: 100 },
{ key: ['threadList', '101', 'title'], value: 'Updated Thread' },
];
dispatch(cdeebeeSlice.actions.set(updates));You can access request history using hooks or selectors:
import { useRequestHistory, useRequestErrors } from '@recats/cdeebee';
// Using hooks (recommended)
const apiHistory = useRequestHistory('/api/forums');
const apiErrors = useRequestErrors('/api/forums');
// Or using selectors
const doneRequests = useAppSelector(state => state.cdeebee.request.done);
const errors = useAppSelector(state => state.cdeebee.request.errors);Clear old success/error history when needed (useful for forms that get reopened):
// Automatic: clear before request
dispatch(request({
api: '/api/posts',
historyClear: true, // Clears old history for this API
body: formData,
}));
// Manual: clear anytime
dispatch(cdeebeeSlice.actions.historyClear('/api/posts')); // Specific API
dispatch(cdeebeeSlice.actions.historyClear()); // All APIscdeebee provides a comprehensive set of React hooks for accessing state without writing selectors. These hooks assume your cdeebee slice is at state.cdeebee (which is the default when using combineSlices).
Check if any of the specified APIs are currently loading.
import { useLoading } from '@recats/cdeebee';
function MyComponent() {
// Check if any of these APIs are loading
const isLoading = useLoading(['/api/forums', '/api/threads']);
if (isLoading) return <Spinner />;
return <div>Content</div>;
}Check if any request is currently loading across all APIs.
import { useIsLoading } from '@recats/cdeebee';
function GlobalSpinner() {
const isAnythingLoading = useIsLoading();
return isAnythingLoading ? <GlobalSpinner /> : null;
}Get a specific list from storage with full type safety.
import { useStorageList } from '@recats/cdeebee';
interface MyStorage {
forumList: Record<string, { id: string; title: string }>;
}
function ForumsList() {
const forums = useStorageList<MyStorage, 'forumList'>('forumList');
return (
<div>
{Object.values(forums).map(forum => (
<div key={forum.id}>{forum.title}</div>
))}
</div>
);
}Get the entire cdeebee storage.
import { useStorage } from '@recats/cdeebee';
interface MyStorage {
forumList: Record<string, Forum>;
threadList: Record<string, Thread>;
}
function DataDebug() {
const storage = useStorage<MyStorage>();
return <pre>{JSON.stringify(storage, null, 2)}</pre>;
}Get successful request history for a specific API endpoint.
import { useRequestHistory } from '@recats/cdeebee';
function RequestStats({ api }: { api: string }) {
const history = useRequestHistory(api);
return (
<div>
Total successful requests to {api}: {history.length}
</div>
);
}Get error history for a specific API endpoint.
import { useRequestErrors } from '@recats/cdeebee';
function ErrorDisplay({ api }: { api: string }) {
const errors = useRequestErrors(api);
if (errors.length === 0) return null;
const lastError = errors[errors.length - 1];
return <div className="error">Last error: {lastError.request.message}</div>;
}If you're not using combineSlices or have cdeebee at a custom path in your state (not state.cdeebee), use createCdeebeeHooks:
// hooks.ts - Create once in your app
import { createCdeebeeHooks } from '@recats/cdeebee';
import type { RootState, MyStorage } from './store';
// Tell the hooks where to find cdeebee in your state
export const {
useLoading,
useStorageList,
useStorage,
useRequestHistory,
useRequestErrors,
useIsLoading,
} = createCdeebeeHooks<RootState, MyStorage>(
state => state.myCustomPath // Your custom path
);Note: Most users won't need createCdeebeeHooks because combineSlices automatically places the slice at state.cdeebee.
cdeebee is fully typed. Define your storage type and get full type safety:
interface MyStorage {
userList: Record<string, { id: string; name: string; email: string }>;
postList: Record<number, { id: number; title: string; userId: string }>;
}
const slice = factory<MyStorage>(settings);
// TypeScript knows the structure
const users = useSelector(state => state.cdeebee.storage.userList);
// users: Record<string, { id: string; name: string; email: string }>// Main exports
export { factory } from '@recats/cdeebee'; // Create cdeebee slice
export { request } from '@recats/cdeebee'; // Request thunk
export { batchingUpdate } from '@recats/cdeebee'; // Batch update helper
// React hooks
export {
createCdeebeeHooks, // Hook factory for custom state paths
useLoading, // Check if APIs are loading
useIsLoading, // Check if any request is loading
useStorageList, // Get a list from storage
useStorage, // Get entire storage
useRequestHistory, // Get successful request history
useRequestErrors, // Get error history
} from '@recats/cdeebee';
// Types
export type {
CdeebeeState,
CdeebeeSettings,
CdeebeeRequestOptions,
CdeebeeValueList,
CdeebeeActiveRequest,
CdeebeeModule,
} from '@recats/cdeebee';See the example/ directory in the repository for a complete working example with Next.js.
MIT