DEV Community

Ayat Saadat
Ayat Saadat

Posted on

ayat saadati — Complete Guide

ayat-saadati-utils: A Developer's Toolkit for Modern Web Applications

You know how it is – you're building a new React component or spinning up a fresh Next.js page, and suddenly you're writing the same boilerplate code you wrote last week, or the week before? We've all been there. It's a productivity killer and frankly, a bit soul-crushing. That's where ayat-saadati-utils comes in.

This isn't just another random collection of functions. This library is a curated toolkit, forged from the trenches of real-world web development. It's inspired by the pragmatic solutions and best practices often championed by insightful developers like Ayat Saadati (check out her work on Dev.to), focusing on common pain points in React and Next.js applications. My goal here was to package up those "aha!" moments and repeatable patterns into something genuinely useful, so you can stop reinventing the wheel and start building amazing features.

Think of it as having a seasoned developer's cheat sheet at your fingertips, ready to tackle everything from robust data fetching to elegant UI logic.

Features

ayat-saadati-utils provides a focused set of utilities designed to streamline your development workflow:

  • useDataFetcher: A powerful and flexible React hook for managing asynchronous data fetching with built-in loading, error, and caching capabilities. No more isLoading, isError dance in every component!
  • localStorageManager: A straightforward wrapper for localStorage, offering type-safe storage and retrieval, and handling common parsing/stringifying needs.
  • cn (Class Name Utility): A tiny, efficient function for conditionally joining CSS class names, making dynamic styling a breeze.
  • debounce: A classic utility function to limit the rate at which a function can fire, perfect for input fields, scroll events, and performance optimization.
  • useDeepCompareEffect: An enhancement over useEffect that performs a deep comparison of dependencies, useful when dealing with object/array dependencies to prevent unnecessary re-runs.

Installation

Getting ayat-saadati-utils into your project is as simple as a single command. It's built for modern JavaScript environments and works seamlessly with npm or yarn.

# Using npm
npm install ayat-saadati-utils

# Using yarn
yarn add ayat-saadati-utils
Enter fullscreen mode Exit fullscreen mode

That's it! Once installed, you're ready to start importing and using these utilities in your components and modules.

Usage

Let's dive into some practical examples to see how these utilities can immediately impact your code.

1. useDataFetcher: Robust Data Fetching Made Easy

This hook is a game-changer for data-driven applications. It handles the lifecycle of an asynchronous operation, providing state for loading, error, and the data itself.

import React from 'react';
import { useDataFetcher } from 'ayat-saadati-utils';

function UserProfile({ userId }) {
  const { data: user, loading, error, refetch } = useDataFetcher(
    async () => {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) {
        throw new Error('Failed to fetch user data');
      }
      return response.json();
    },
    [userId] // Dependencies for refetching
  );

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

  if (error) {
    return (
      <div>
        <p style={{ color: 'red' }}>Error: {error.message}</p>
        <button onClick={refetch}>Try Again</button>
      </div>
    );
  }

  if (!user) {
    return <p>No user data found.</p>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Bio: {user.bio}</p>
      <button onClick={refetch}>Refresh Profile</button>
    </div>
  );
}

export default UserProfile;
Enter fullscreen mode Exit fullscreen mode

My take: I've seen so many developers (myself included, in my early days) write this isLoading, isError, data state management logic over and over again. It's tedious, error-prone, and clutters your components. useDataFetcher abstracts that away perfectly, letting you focus on what to fetch and how to display it, not the plumbing.

2. localStorageManager: A Smarter Way to Use Local Storage

Interacting with localStorage can be a bit clunky, especially when dealing with JSON. This utility simplifies it and adds a layer of type safety (when used with TypeScript, though it works great with vanilla JS too).

import React, { useState, useEffect } from 'react';
import { localStorageManager } from 'ayat-saadati-utils';

function ThemeSwitcher() {
  const [theme, setTheme] = useState(() => {
    // Initialize state from local storage, default to 'light'
    return localStorageManager.get('app-theme') || 'light';
  });

  useEffect(() => {
    // Persist theme changes to local storage
    localStorageManager.set('app-theme', theme);
    document.documentElement.setAttribute('data-theme', theme);
  }, [theme]);

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
      <button onClick={() => localStorageManager.remove('app-theme')}>
        Clear Theme from Storage
      </button>
    </div>
  );
}

export default ThemeSwitcher;
Enter fullscreen mode Exit fullscreen mode

My take: Direct localStorage.getItem and JSON.parse calls are just begging for null checks and try...catch blocks. localStorageManager wraps all that up, providing a clean API. It's a small thing, but these small utilities add up to a much cleaner codebase.

3. cn: Concatenate Class Names Conditionally

This is a tiny helper, but oh-so-useful! If you've ever found yourself with messy template literals for conditional classes, you'll love cn.

import React, { useState } from 'react';
import { cn } from 'ayat-saadati-utils';
import './Button.css'; // Imagine this file exists

function Button({ primary, disabled, size = 'medium', children, ...props }) {
  const [isActive, setIsActive] = useState(false);

  const buttonClasses = cn(
    'btn',
    primary && 'btn-primary',
    disabled && 'btn-disabled',
    `btn-${size}`,
    isActive && 'btn-active',
    // You can also pass objects
    { 'btn-large': size === 'large', 'btn-small': size === 'small' }
  );

  return (
    <button
      className={buttonClasses}
      disabled={disabled}
      onClick={() => setIsActive(!isActive)}
      {...props}
    >
      {children}
    </button>
  );
}

export default function App() {
  return (
    <div style={{ display: 'flex', gap: '10px', padding: '20px' }}>
      <Button primary>Primary Button</Button>
      <Button disabled>Disabled Button</Button>
      <Button size="large">Large Button</Button>
      <Button>Default Button</Button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

My take: I used to write these long, ugly strings with ternary operators and && conditions. cn (or similar libraries like clsx) makes it so much more readable and maintainable. It's one of those things you adopt once and wonder how you ever lived without it.

4. debounce: Taming Event Storms

Debouncing is crucial for performance, especially with search inputs, window resizing, or other rapidly firing events.

import React, { useState, useEffect, useCallback } from 'react';
import { debounce } from 'ayat-saadati-utils';

function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  const [isSearching, setIsSearching] = useState(false);

  // Simulate an API call
  const performSearch = useCallback(async (query) => {
    if (!query) {
      setResults([]);
      return;
    }
    setIsSearching(true);
    console.log(`Searching for: "${query}"...`);
    await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate network delay
    setResults([`Result for "${query}-1"`, `Result for "${query}-2"`]);
    setIsSearching(false);
  }, []);

  // Debounce the search function
  const debouncedSearch = useCallback(debounce(performSearch, 500), [performSearch]);

  const handleChange = (event) => {
    const value = event.target.value;
    setSearchTerm(value);
    debouncedSearch(value); // Call the debounced function
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Type to search..."
        value={searchTerm}
        onChange={handleChange}
        style={{ padding: '8px', width: '300px' }}
      />
      {isSearching && <p>Searching...</p>}
      {!isSearching && results.length > 0 && (
        <ul>
          {results.map((result, index) => (
            <li key={index}>{result}</li>
          ))}
        </ul>
      )}
      {!isSearching && searchTerm && results.length === 0 && <p>No results found.</p>}
    </div>
  );
}

export default SearchInput;
Enter fullscreen mode Exit fullscreen mode

My take: If you're not debouncing your inputs and other frequent events, you're likely hammering your API or doing unnecessary work, which leads to janky UIs. debounce is a fundamental tool for building performant web applications. This is a classic utility that every developer should have in their arsenal.

5. useDeepCompareEffect: For Complex Dependencies

Sometimes, useEffect or useCallback triggers too often because a dependency, though structurally identical, is a new reference. useDeepCompareEffect solves this by performing a deep comparison.

import React, { useState, useEffect } from 'react';
import { useDeepCompareEffect } from 'ayat-saadati-utils';

function DeepCompareComponent() {
  const [config, setConfig] = useState({ theme: 'dark', settings: { autoSave: true } });
  const [renderCount, setRenderCount] = useState(0);
  const [deepEffectCount, setDeepEffectCount] = useState(0);

  // Standard useEffect - will trigger on every `setConfig` call if settings is new object
  useEffect(() => {
    setRenderCount((prev) => prev + 1);
    console.log('Standard useEffect triggered', config);
  }, [config]); // config is an object, so reference changes if new object is passed

  // useDeepCompareEffect - will only trigger if config's *content* changes
  useDeepCompareEffect(() => {
    setDeepEffectCount((prev) => prev + 1);
    console.log('useDeepCompareEffect triggered', config);
  }, [config]);

  return (
    <div>
      <h2>Deep Compare Effect Demo</h2>
      <p>Standard useEffect triggered: {renderCount} times</p>
      <p>useDeepCompareEffect triggered: {deepEffectCount} times</p>
      <button
        onClick={() => {
          // This creates a new object reference for `config`, but content is the same
          setConfig({ ...config, settings: { ...config.settings } });
        }}
      >
        Update config (same content)
      </button>
      <button
        onClick={() => {
          // This changes the content
          setConfig({ ...config, theme: config.theme === 'dark' ? 'light' : 'dark' });
        }}
      >
        Change config theme
      </button>
    </div>
  );
}

export default DeepCompareComponent;
Enter fullscreen mode Exit fullscreen mode

My take: This one is a bit more niche, but when you need it, you really need it. If you're passing complex objects or arrays as useEffect dependencies and finding that your effect runs more often than it should, useDeepCompareEffect can save you a lot of headaches and performance debugging. Just be mindful that deep comparisons can be expensive for very large objects, so use it judiciously.

API Reference (Quick Glance)

Utility Description Parameters Returns
useDataFetcher React Hook for async data fetching with loading/error states. fetcher: () => Promise<T>, deps: React.DependencyList `{ data: T \
{% raw %}localStorageManager Object with methods for localStorage. `get(key: string): T \ null, set(key: string, value: T): void, remove(key: string): void`
cn Joins CSS class names conditionally. `...args: (string | boolean | null | undefined

Top comments (0)