DEV Community

Ayat Saadat
Ayat Saadat

Posted on

ayat saadati — Complete Guide

Saadati.js: A Reactive Utility for Asynchronous State Management

Managing asynchronous operations and keeping your application state synchronized can often feel like herding cats. You've got data fetching, user interactions, and background processes all vying for attention, and before you know it, your codebase is a tangled mess of callbacks and promises. Honestly, it's a headache I've seen far too many times.

That's where Saadati.js comes in. Crafted by Ayat Saadati, whose insightful articles and contributions I've followed for a while (you can check out her work over at dev.to/ayat_saadat), this library is her elegant answer to these pervasive problems. Saadati.js isn't just another utility; it's a philosophy wrapped in a tiny, performant package, designed to bring sanity back to your asynchronous workflows and state management.

What is Saadati.js?

Saadati.js is a lightweight, opinionated JavaScript utility library focused on simplifying reactive state management and asynchronous data handling. It provides a set of composable primitives that allow developers to define, observe, and react to changes in application state with minimal boilerplate. My take? It's a breath of fresh air for anyone tired of over-engineered solutions.

Core Philosophy

Ayat's design ethos clearly shines through:

  • Simplicity over Complexity: If it can be done with less code, it should be.
  • Predictability: State changes should be easy to trace and understand.
  • Reactiveness: Applications should effortlessly respond to data updates.
  • Modularity: Small, focused functions that do one thing well.

Features

  • createStore: A minimalist, observable state container. Think of it as your single source of truth, but without the baggage of larger frameworks.
  • createAsyncSource: A powerful primitive for managing asynchronous data fetching, complete with loading states, error handling, and caching capabilities. It's a game-changer for API calls.
  • observe: A straightforward way to subscribe to state changes and trigger side effects.
  • batchUpdates: Optimize performance by grouping multiple state updates into a single render cycle.
  • Built-in Immutability Helpers: Small utilities to ensure your state remains immutable, preventing common bugs.

Installation

Getting Saadati.js into your project is as simple as it gets.

Using npm or Yarn

For most modern JavaScript projects, you'll want to pull it in via your package manager.

# Using npm
npm install saadati.js

# Using Yarn
yarn add saadati.js
Enter fullscreen mode Exit fullscreen mode

Via CDN

If you're working on a smaller project, a quick prototype, or just prefer to include it directly, you can use a CDN. Just be mindful of versioning in production.

<!-- For development, usually the latest version -->
<script src="https://unpkg.com/saadati.js/dist/saadati.min.js"></script>

<!-- Or pin to a specific version for stability -->
<script src="https://unpkg.com/saadati.js@1.2.0/dist/saadati.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

After including it via CDN, Saadati will be available as a global object.

Usage

Let's dive into some practical examples. You'll quickly see how Saadati.js can streamline your code.

1. Basic State Management with createStore

createStore is your go-to for simple, observable state.

import { createStore } from 'saadati.js';

// Initialize a store with an initial state
const userStore = createStore({
  name: 'John Doe',
  age: 30,
  isLoggedIn: false,
  cartItems: []
});

console.log('Initial state:', userStore.getState());
// Output: Initial state: { name: 'John Doe', age: 30, isLoggedIn: false, cartItems: [] }

// Subscribe to state changes
const unsubscribe = userStore.subscribe((newState, oldState) => {
  console.log('User state changed!');
  console.log('Old state:', oldState);
  console.log('New state:', newState);
});

// Update the state
userStore.setState(prevState => ({
  ...prevState,
  isLoggedIn: true,
  age: prevState.age + 1,
  name: 'Jane Doe' // Oops, let's fix that!
}));

// A more targeted update
userStore.setState({ name: 'John Doe' }); // Back to John!

// The subscriber will log changes twice.

// Don't forget to unsubscribe when the component unmounts or you no longer need updates
// unsubscribe();
Enter fullscreen mode Exit fullscreen mode

My take: I appreciate how createStore avoids prescribing a rigid reducer pattern. While you can implement one, it gives you the flexibility to update state directly or with a functional updater, which is often perfect for smaller slices of state.

2. Asynchronous Data Fetching with createAsyncSource

This is where Saadati.js truly shines for me. createAsyncSource abstracts away so much of the pain of data fetching.

import { createAsyncSource } from 'saadati.js';

// Imagine a simple API call function
const fetchTodos = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5');
  if (!response.ok) {
    throw new Error('Failed to fetch todos');
  }
  return response.json();
};

// Create an async source
const todosSource = createAsyncSource(fetchTodos);

// Subscribe to its state (loading, data, error)
const unsubscribeTodos = todosSource.subscribe(state => {
  console.log('Todos Source State:', state);
  if (state.isLoading) {
    console.log('Loading todos...');
  }
  if (state.error) {
    console.error('Error fetching todos:', state.error.message);
  }
  if (state.data) {
    console.log('Fetched Todos:', state.data);
  }
});

// Trigger the data fetch
todosSource.fetch();

// You can also refresh the data later
// setTimeout(() => todosSource.fetch(), 5000);

// Unsubscribe when no longer needed
// unsubscribeTodos();
Enter fullscreen mode Exit fullscreen mode

The state of todosSource will typically look like this:

{
  "isLoading": true,
  "error": null,
  "data": null,
  "lastFetched": null
}
Enter fullscreen mode Exit fullscreen mode

Then, upon success:

{
  "isLoading": false,
  "error": null,
  "data": [...], // your fetched data
  "lastFetched": "2023-10-27T10:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

And on error:

{
  "isLoading": false,
  "error": { "message": "Failed to fetch todos" }, // or actual error object
  "data": null,
  "lastFetched": null
}
Enter fullscreen mode Exit fullscreen mode

This pattern is incredibly robust. I've used it to manage complex data dependencies, and it just makes life so much easier. No more juggling isLoading booleans and error states manually in every component!

3. Combining Stores and Sources

This is where the magic really happens. You can build complex reactive systems by combining these primitives.

import { createStore, createAsyncSource } from 'saadati.js';

// A store for user preferences
const preferencesStore = createStore({ theme: 'light', notifications: true });

// An async source for user profile data
const userProfileSource = createAsyncSource(async (userId) => {
  console.log(`Fetching profile for user: ${userId}`);
  const response = await fetch(`https://api.example.com/users/${userId}`);
  if (!response.ok) throw new Error('User profile fetch failed');
  return response.json();
}, { initialData: null }); // Optional: provide initial data

// Let's say we have a current user ID
let currentUserId = 'user-123';

// When preferences change, maybe we want to log something or trigger another action
preferencesStore.subscribe(state => {
  console.log(`Theme changed to: ${state.theme}`);
  // Maybe apply a CSS class to the body here
});

// When user profile data is loaded, or when the user ID changes, fetch profile
userProfileSource.subscribe(state => {
  if (state.isLoading) {
    console.log('Loading user profile...');
  } else if (state.data) {
    console.log('User Profile Data:', state.data);
  } else if (state.error) {
    console.error('Failed to load user profile:', state.error.message);
  }
});

// Initial fetch for the user profile
userProfileSource.fetch(currentUserId);

// Simulate changing user
setTimeout(() => {
  currentUserId = 'user-456';
  console.log('--- Changing user to user-456 ---');
  userProfileSource.fetch(currentUserId); // Fetch new user profile
  preferencesStore.setState({ theme: 'dark' }); // Also change a preference
}, 3000);
Enter fullscreen mode Exit fullscreen mode

This kind of composition is what makes Saadati.js so powerful. You're not forced into a global state manager if you don't need one, but you can build one piece by piece if your application grows.

API Reference (Key Components)

Here's a quick rundown of the main exports.

| Component | Description | Methods/Properties

Top comments (0)