Skip to content

New way of automating requests and update data in frontend based on cqs, mvvm and reactive programming.

Notifications You must be signed in to change notification settings

Reactive-Query-Lab/reactive-query

Repository files navigation

Reactive Query

A framework-agnostic library for model part in MVVM architectural pattern, automating querying, storing, and caching data in frontend applications based on MVVM or any MV*, CQS, and reactive programming paradigms.

Main Documentation

Table of Contents

Description

Reactive Query is a framework-agnostic library designed specifically for the Model part in the MVVM (Model-View-ViewModel) or any MV* architectural pattern. It automates the process of querying, storing, and managing data in frontend applications by implementing CQS (Command Query Separation) and reactive programming paradigms.

The library provides a bridge between push-based and pull-based rendering strategies, enabling granular control over re-rendering in pull-based frameworks like React and Vue while maintaining the efficiency of push-based frameworks like Angular.

Motivation

In modern frontend development, there's a significant gap in libraries that can effectively manage data and automate the processes of managing, caching, and invalidating data in frontend applications while fitting seamlessly into the MVVM architectural pattern. Most existing solutions either:

  • Don't follow any software architectural patterns principles
  • Lack proper single responsibility
  • Don't provide granular control over re-rendering
  • Are framework-specific rather than framework-independent

We created Reactive Query to address these challenges by providing a specialized library that handles all logic related to data manipulation in the Model part of MVVM or any MV*.

Bridge Between Push and Pull Strategies

Modern frontend frameworks use different rendering strategies:

Push-based (Angular): The framework automatically detects changes and re-renders components when data changes.

Pull-based (React/Vue): Components must explicitly request re-renders when their state changes.

Reactive Query bridges this gap by providing reactive observables that can be easily connected to pull-based frameworks. For example, in React, you can pipe and map changes to specific object keys, triggering setState only when relevant data changes:

// Instead of re-rendering on any data change
userModel.query().subscribe(setUserData);

// You can be granular and only re-render when specific fields change
userModel.query().pipe(
  distinctUntilChanged((prev, next) => prev.places.length === next.places.length)
).subscribe(setPlaces);

CQS Pattern Implementation

We implemented the Command Query Separation (CQS) pattern to handle different types of data operations:

  • Queries: Read operations that don't modify state of the software and just need to be cached and refresh the data in some scenarios.
  • Commands: Write operations that modify software state

This separation allows for better performance, caching strategies, and state management. For more information about CQS, see Command Query Separation.

Reactive Programming with RxJS

To provide subscribing capabilities and maintain framework agnosticism, we use the reactive programming paradigm with RxJS. This enables:

  • Automatic subscription management
  • Powerful data transformation operators
  • Framework-independent state management
  • Efficient change detection and propagation

Features

  • 🏗️ MVVM Architecture - Designed specifically for the Model part of MVVM
  • 🔄 CQS Pattern - Clear separation between Commands and Queries
  • Reactive Programming - Built on RxJS for real-time state updates
  • 💾 Smart Caching - Automatic caching with configurable stale times
  • 🔄 Retry Mechanism - Built-in retry logic for failed operations
  • 🎯 TypeScript Support - Full TypeScript support with type safety
  • 📦 Lightweight - Minimal bundle size with zero dependencies (except RxJS)
  • 🔧 Framework Agnostic - Works with any frontend framework
  • 🎛️ Granular Control - Fine-grained control over re-rendering
  • 🔌 Extensible - Easy to extend with custom stores and events

Installation

npm install reactive-query rxjs
# or
yarn add reactive-query rxjs
# or
pnpm add reactive-query rxjs

Architecture Overview

Reactive Query follows a clear architectural pattern:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Query Models  │    │  Command Models │    │     Stores      │
│                 │    │                 │    │                 │
│ • ReactiveQuery │    │ • ReactiveCmd   │    │ • Query Vault   │
│ • Caching       │    │ • Mutations     │    │ • Command Store │
│ • Parameters    │    │ • Parameters    │    │ • Events        │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘
                                 │
                    ┌─────────────────┐
                    │   RxJS Streams  │
                    │                 │
                    │ • Observables   │
                    │ • Subscriptions │
                    │ • Operators     │
                    └─────────────────┘

Query Models

Query Models handle read operations and implement intelligent caching strategies.

Thinking with Query Models

Query Models are designed around the concept of parameterized queries that return cached results. Think of them as smart data fetchers that:

  1. Cache by parameters - Different parameters create different cache entries
  2. Auto-refresh stale data - Automatically fetch fresh data when cache expires
  3. Handle loading states - Provide loading, error, and success states
  4. Retry on failure - Automatically retry failed requests

Vault and Store

Vault: A collection of stores indexed by hashed parameters. Think of it as a cache container.

Store: Individual cache entries containing data, loading states, and metadata.

// Vault structure
{
  "user_123": { data: User, isLoading: false, isFetched: true, ... },
  "user_456": { data: User, isLoading: true, isFetched: false, ... },
  "products_filters": { data: Product[], isLoading: false, isFetched: true, ... }
}

Query and Refresh

Query: The public method that returns an observable of query results.

Refresh: The protected method you implement to fetch data from your API.

class UserQueryModel extends ReactiveQueryModel<User> {
  protected async refresh(userId: number): Promise<User> {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error('Failed to fetch user');
    return response.json();
  }
}

// Usage
const userModel = new UserQueryModel();
const user$ = userModel.query(123); // Observable<User>

Key Hashing and Parameters

Parameters are automatically hashed to create cache keys. The library provides intelligent hashing that:

  • Handles primitive values (strings, numbers, booleans)
  • Sorts object keys for consistent hashing (Just one layer to avoid heavy time complexity. You can overwrite hashing logics for custom algorithms)
  • Handles arrays and nested objects
  • Supports custom hashing algorithms
// These all create the same hash key
userModel.query({ id: 123, include: 'profile' });
userModel.query({ include: 'profile', id: 123 });

// Different parameters create different cache entries
userModel.query(123);        // Key: "123"
userModel.query(456);        // Key: "456"
userModel.query({ id: 123 }); // Key: '{"id":123}'

Configuration

Query Models support various configuration options:

class UserQueryModel extends ReactiveQueryModel<User> {
  protected get configs() {
    return {
      maxRetryCall: 3,           // Retry failed requests 3 times
      cachTime: 5 * 60 * 1000,   // Cache for 5 minutes
      emptyVaultOnNewValue: false, // Keep old cache when new data arrives
      initStore: {
        key: 'default',
        value: { id: 0, name: 'Loading...' },
        staleTime: 60 * 1000
      }
    };
  }
}

Query API Reference

Exported Types

// Main response type for queries
type QueryResponse<DATA> = {
  data?: DATA;
  isLoading: boolean;
  isFetching: boolean;
  isFetched: boolean;
  error?: unknown;
  staled: boolean;
  staleTime?: number;
  lastFetchedTime?: number;
};

// Base store type
type BaseReactiveStore<DATA> = {
  data: DATA;
  isLoading: boolean;
  isFetching: boolean;
  isFetched: boolean;
  error?: unknown;
  staled: boolean;
  staleTime?: number;
  lastFetchedTime?: number;
};

// Vault type for multiple stores
type ReactiveQueryVault<DATA, EVENTS = undefined> = {
  store$: Observable<{ [key: string]: BaseReactiveStore<DATA> }>;
} & QueryVaultEvents<DATA> & EVENTS;

Protected Methods (Can be overridden)

// Override to implement your data fetching logic
protected abstract refresh(params?: unknown): Promise<DATA>;

// Override for custom parameter hashing
protected getHashedKey(params?: unknown): string;

// Override for custom configuration
protected get configs(): {
  maxRetryCall: number;
  cachTime: number;
  emptyVaultOnNewValue: boolean;
  initStore?: {
    key: string;
    value: DATA;
    staleTime?: number;
  };
};

Public Methods

// Main query method
query(params?: unknown, configs?: { staleTime?: number }): Observable<QueryResponse<DATA>>

// Store management
get storeHandler(): {
  invalidate(): void;
  invalidateByKey(params?: unknown): void;
  resetStore(params?: unknown): void;
  resetVault(): void
}

// Utility methods
isSameBaseData(prev: QueryResponse<DATA>, curr: QueryResponse<DATA>): boolean;

Configuration Options

Option Type Default Description
maxRetryCall number 1 Maximum retry attempts for failed requests
cachTime number 3 * 60 * 1000 Default cache time in milliseconds
emptyVaultOnNewValue boolean false Clear vault when new data arrives
initStore object undefined Initial store configuration

Command Models

Command Models handle write operations (create, update, delete) and manage parameter state.

Understanding Mutate

The mutate method is the core of Command Models. It handles write operations and manages the command lifecycle:

class CreateUserCommandModel extends ReactiveCommandModel<CreateUserParams, User> {
  async mutate(params: CreateUserParams): Promise<User> {
    const response = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(params)
    });
    return response.json();
  }
}

Store Architecture

Unlike Query Models, Command Models use a single store instead of a vault because:

  • No parameter-based caching - Commands don't need to cache by parameters
  • Single state management - One set of parameters per command
  • Immediate execution - Commands execute immediately, not on demand
// Command store structure
{
  isLoading: boolean;
  params: Partial<PARAMS>;
  // ... extended store properties
}

Parameter Management

Command Models provide built-in parameter management:

// Get current parameters
const params = commandModel.getParams();

// Update parameters
commandModel.updateModificationStore({ name: 'John', email: 'john@example.com' });

// Get specific parameter
const name = commandModel.getModificationValueByKey('name');

// Subscribe to parameter changes
commandModel.subscribeToParam().subscribe(({ params, isLoading }) => {
  console.log('Parameters changed:', params);
});

Store Extension

Command Models support extended stores and custom events:

class ExtendedCommandModel extends ReactiveCommandModel<
  UserParams,
  User,
  { validationErrors: string[] },
  { onValidationError: (errors: string[]) => void }
> {
  protected initExtendedStore() {
    return {
      initExtendedStore: { validationErrors: [] },
      extendedEvents: (store$) => ({
        onValidationError: (errors: string[]) => {
          store$.next({
            ...store$.value,
            validationErrors: errors
          });
        }
      })
    };
  }
}

Command API Reference

Exported Types

// Command response type
type CommandModelSubscribeResponse<PARAMS> = {
  params: Partial<PARAMS>;
  isLoading: boolean;
};

// Base command store
type BaseReactiveCommandStore<PARAMS, EXTENDED_STORE> = {
  isLoading: boolean;
  params: Partial<PARAMS>;
} & EXTENDED_STORE;

// Command store with events
type ReactiveCommandStore<PARAMS, EXTENDED_STORE, EXTENDED_EVENTS> = {
  store$: BehaviorSubject<BaseReactiveCommandStore<PARAMS, EXTENDED_STORE>>;
} & BaseReactiveCommandEvents<PARAMS, EXTENDED_STORE> & EXTENDED_EVENTS;

Protected Methods (Can be overridden)

// Override to implement your mutation logic
abstract mutate(...args: any[]): Promise<RESPONSE>;

// Override for initial parameters
getInitialParams(): PARAMS;

// Override for extended store and events
protected initExtendedStore(): {
  initExtendedStore?: EXTENDED_STORE;
  extendedEvents?: (store$: BehaviorSubject<BaseReactiveCommandStore<PARAMS, EXTENDED_STORE>>) => EXTENDED_EVENTS;
};

Public Methods

// Subscribe to store changes
subscribeToParam(): Observable<CommandModelSubscribeResponse<PARAMS>>;

// Parameter management
getModificationValueByKey<T extends keyof PARAMS>(key: T): PARAMS[T] | undefined;
updateModificationStore(params: Partial<PARAMS>): void;
getParams(): PARAMS;
getStore(): BaseReactiveCommandStore<PARAMS, EXTENDED_STORE>;

// State management
updateIsLoading(isLoading: boolean): void;
resetStore(): void;

Adapters

React Integration

For seamless React integration, we provide a dedicated React adapter library: reactive-query-react

Note: This library act as the Model in the MV* architectures, highly suggested to be used beside of ReactVVM which acts as the ViewModel and View in the MVVM. With these two approaches you can have solid MVVM architecture in your react application in any scale of projects.

Using the React Adapter (Recommended)

npm install reactive-query-react
import React, { useRef } from 'react';
import { useRXQuery } from 'reactive-query-react';
import { ReactiveQueryModel } from 'reactive-query';

class TodoQueryModel extends ReactiveQueryModel<Todo[]> {
  protected async refresh(): Promise<Todo[]> {
    const response = await fetch('/api/todos');
    return response.json();
  }
}

function TodoList() {
  const todoModel = useRef(new TodoQueryModel()).current;
  const queryData = useRXQuery(todoModel.query);

  if (queryData.loading) {
    return <p>Loading...</p>;
  }

  if (queryData.error) {
    return <p>Error: {queryData.error.message}</p>;
  }

  return (
    <ul>
      {queryData.data?.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

Note: This library act as the Model in the MV* architectures, highly suggested to be used beside of ReactVVM which acts as the ViewModel and View in the MVVM. With these two approaches you can have solid MVVM architecture in your react application in any scale of projects.

Svelte

For now we don't have any adapter for Svelte but to see an example of how to use it with Svelte You can check this gist

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

New way of automating requests and update data in frontend based on cqs, mvvm and reactive programming.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •