npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

oxidjs

v1.0.7

Published

A lightweight and powerful reactive signals library for building modern reactive applications. OxidJS provides a simple yet flexible API for managing reactive state with excellent TypeScript support.

Readme

OxidJS - Reactive Signals Library

A lightweight and powerful reactive signals library for building modern reactive applications. OxidJS provides a simple yet flexible API for managing reactive state with excellent TypeScript support.

Installation

npm install oxidjs
# or
yarn add oxidjs
# or
pnpm install oxidjs

Core Concepts

1. Signals

Signals are the basic building blocks. They hold values that can change over time and notify dependents of changes.

import { createSignal } from 'oxidjs';

// Basic signal usage
const [count, setCount] = createSignal(0);
console.log(count()); // 0
setCount(1);
console.log(count()); // 1

// With type annotation
const [name, setName] = createSignal<string>('John');

// Updating based on previous value
const [score, setScore] = createSignal(0);
setScore(prev => prev + 1);

// Multiple signals
const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');

2. Derived Signals

Derived signals automatically compute their value from other signals.

import { createSignal, derived } from 'oxidjs';

const [width, setWidth] = createSignal(10);
const [height, setHeight] = createSignal(20);

// Single dependency
const area = derived([width], w => w * w);

// Multiple dependencies
const rectangleArea = derived(
  [width, height],
  (w, h) => w * h
);

// Nested derived signals
const [price, setPrice] = createSignal(100);
const tax = derived([price], p => p * 0.2);
const total = derived([price, tax], (p, t) => p + t);

console.log(total()); // 120
setPrice(200);
console.log(total()); // 240

3. Computed Values

Computed values are similar to derived signals but with more flexibility in dependency tracking.

import { createSignal, computed } from 'oxidjs';

const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');

// Basic computed
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // "John Doe"

// Computed with conditions
const [age, setAge] = createSignal(20);
const canVote = computed(() => {
  if (age() < 18) return false;
  return true;
});

// Complex computations
const [items, setItems] = createSignal([1, 2, 3]);
const [filter, setFilter] = createSignal('even');

const filteredItems = computed(() => {
  const list = items();
  const filterType = filter();
  
  return list.filter(item => 
    filterType === 'even' ? item % 2 === 0 : item % 2 !== 0
  );
});

4. Linked Signals

Linked signals connect to another signal, mirroring its value (with optional transformation) and optionally syncing changes back to the source.

import { createSignal, linkedSignal } from 'oxidjs';

// Basic linked signal
const [count, setCount] = createSignal(5);
const [doubledCount, setDoubledCount] = linkedSignal([count, setCount]);

console.log(doubledCount()); // 5
setCount(10);
console.log(doubledCount()); // 10

// With transformation
const [price, setPrice] = createSignal(100);
const [formattedPrice, setFormattedPrice] = linkedSignal([price, setPrice], {
  transform: value => `$${value.toFixed(2)}`,
  reverseTransform: value => Number(value.replace('$', '')),
  syncBack: true
});

console.log(formattedPrice()); // "$100.00"
setPrice(199.99);
console.log(formattedPrice()); // "$199.99"

// Changes to linked signal sync back to source
setFormattedPrice('$299.99');
console.log(price()); // 299.99

// Use case: Unit conversion
const [celsius, setCelsius] = createSignal(0);
const [fahrenheit, setFahrenheit] = linkedSignal([celsius, setCelsius], {
  transform: c => c * 9/5 + 32,
  reverseTransform: f => (f - 32) * 5/9,
  syncBack: true
});

console.log(fahrenheit()); // 32
setCelsius(100);
console.log(fahrenheit()); // 212
setFahrenheit(68);
console.log(celsius()); // 20

5. Stateful Signals

Stateful signals extend regular signals with history tracking, undo/redo capabilities, and optional persistence.

import { statefulSignal } from 'oxidjs';

// Basic stateful signal
const counter = statefulSignal(0);
counter.set(1);
counter.set(2);
counter.set(3);

console.log(counter.get()); // 3

// Undo changes
counter.undo();
console.log(counter.get()); // 2

counter.undo();
console.log(counter.get()); // 1

// Redo changes
counter.redo();
console.log(counter.get()); // 2

// Check if undo/redo is available
console.log(counter.canUndo()); // true
console.log(counter.canRedo()); // true

// History handling
console.log(counter.getHistory()); // [0, 1, 2]
counter.resetHistory(); // Clears history but keeps current value

// With history limit
const limitedHistory = statefulSignal(0, { historyLimit: 5 });

// With persistence (stores in localStorage)
const persistedCounter = statefulSignal(0, {
  persist: true,
  persistKey: 'counter'
});

// With custom equality function
const objSignal = statefulSignal(
  { id: 1, name: 'Item 1' },
  {
    equalityFn: (a, b) => a.id === b.id
  }
);

// Text editor with undo/redo
const editor = statefulSignal('');

function insertText(text) {
  editor.set(prev => prev + text);
}

insertText('Hello');
insertText(' world!');
console.log(editor.get()); // "Hello world!"

editor.undo();
console.log(editor.get()); // "Hello"

6. Effects

Effects handle side effects and automatically track and respond to signal changes.

import { createSignal, effect } from 'oxidjs';

// Basic effect
const [count, setCount] = createSignal(0);
effect(() => {
  console.log(`Count changed to: ${count()}`);
});

// Effect with cleanup
const [isOnline, setIsOnline] = createSignal(false);
effect(() => {
  if (isOnline()) {
    const connection = connectToServer();
    // Cleanup function
    return () => connection.disconnect();
  }
});

// Multiple dependencies
const [user, setUser] = createSignal(null);
const [theme, setTheme] = createSignal('light');

effect(() => {
  console.log(`User ${user()} switched to ${theme()} theme`);
});

7. Batch Updates

Batch multiple updates together to optimize performance and prevent unnecessary re-renders.

import { createSignal, batch, effect } from 'oxidjs';

const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');
const [age, setAge] = createSignal(30);

// Effect will only run once after all updates
effect(() => {
  console.log(`${firstName()} ${lastName()}, ${age()} years old`);
});

// Without batch - effect runs three times
setFirstName('Jane');
setLastName('Smith');
setAge(25);

// With batch - effect runs only once
batch(() => {
  setFirstName('Jane');
  setLastName('Smith');
  setAge(25);
});

// Nested batches
batch(() => {
  setFirstName('Jane');
  batch(() => {
    setLastName('Smith');
    setAge(25);
  });
});

8. Resource Signals

Resource signals simplify managing asynchronous data fetching with built-in support for loading states, caching, and dependency tracking.

import { resourceSignal, createSignal } from 'oxidjs';

// Basic usage - fetch user data
const userResource = resourceSignal(
  async (params) => {
    const response = await fetch(`https://api.example.com/users/${params.id}`);
    if (!response.ok) throw new Error('Failed to fetch user');
    return response.json();
  },
  { id: 1 } // Initial parameters
);

// Access the data and status
console.log(userResource.status()); // 'loading', 'success', 'error', etc.
console.log(userResource.data()); // The fetched data or undefined
console.log(userResource.isLoading()); // boolean
console.log(userResource.isSuccess()); // boolean
console.log(userResource.isError()); // boolean
console.log(userResource.error()); // Error object or null

// Manually trigger refetch
userResource.refetch();

// Refetch with new parameters
userResource.refetch({ id: 2 });

// With reactive dependencies
const [userId, setUserId] = createSignal(1);

const userProfile = resourceSignal(
  async (params) => {
    const response = await fetch(`https://api.example.com/users/${params.id}`);
    return response.json();
  },
  { id: userId() },
  {
    // Automatically refetch when userId changes
    deps: [userId],
    // Keep previous data while loading new data
    keepPreviousData: true,
    // Initial data before first fetch completes
    initialData: { name: 'Loading...', email: '' },
  }
);

// Change userId to trigger a refetch
setUserId(2);

// Caching with staleTime
const cachedResource = resourceSignal(
  fetchData,
  { id: 1 },
  {
    // Cache data for 5 minutes
    staleTime: 5 * 60 * 1000,
    // How long to keep unused data in cache
    cacheTime: 10 * 60 * 1000,
  }
);

// Refetch will use cached data if within staleTime
cachedResource.refetch();

// Manually set data
userResource.setData({ id: 1, name: 'Custom User' });

// Update data based on previous value
userResource.setData(prev => ({ 
  ...prev, 
  lastSeen: new Date() 
}));

// Reset to initial state
userResource.reset();

// Abort ongoing request
userResource.abort();

// Cleanup when no longer needed
userResource.dispose();

9. Resource Signal Interceptors

Resource signals support powerful interceptors that allow you to modify requests and responses globally or per-resource.

import { resourceSignal } from 'oxidjs';

// 1. Authentication Interceptor Example
function fetchData(params) {
  return fetch(`https://api.example.com/data/${params.id}`).then(r => r.json());
}

// Create a resource with authentication interceptor
const authResource = resourceSignal(
  fetchData,
  { id: 1 },
  {
    requestInterceptor: {
      // Add auth token to all requests
      onRequest: (params) => {
        return {
          ...params,
          token: localStorage.getItem('auth_token')
        };
      },
      
      // Add extra context data that will be passed to response interceptors
      getContext: (params) => {
        return {
          requestTime: Date.now(),
          requestId: crypto.randomUUID()
        };
      }
    },
    
    responseInterceptor: {
      // Process successful responses
      onResponse: (response, params, context) => {
        console.log(`Request ${context.requestId} completed in ${Date.now() - context.requestTime}ms`);
        
        // Add metadata to response
        return {
          ...response,
          _metadata: {
            fetchedAt: new Date().toISOString(),
            requestId: context.requestId
          }
        };
      },
      
      // Handle errors gracefully
      onError: async (error, params, context) => {
        // Check for authentication errors
        if (error.message === 'Unauthorized' || error.status === 401) {
          // Try to refresh the token
          const newToken = await refreshAccessToken();
          
          if (newToken) {
            localStorage.setItem('auth_token', newToken);
            // Return a new fetch call with updated token
            const response = await fetchData({
              ...params,
              token: newToken
            });
            return response;
          }
        }
        
        // Return a fallback value instead of throwing
        if (error.status === 404) {
          return { 
            id: params.id, 
            name: 'Not Found',
            _isFallback: true
          };
        }
        
        // Otherwise, enrich error and let it propagate
        return new Error(`${error.message} (Request ID: ${context.requestId})`);
      }
    }
  }
);

// 2. Data Transformation Interceptor
const userResource = resourceSignal(
  async (params) => {
    const response = await fetch(`https://api.example.com/users/${params.id}`);
    return response.json();
  },
  { id: 1 },
  {
    responseInterceptor: {
      // Transform response data format
      onResponse: (user) => {
        // Convert snake_case to camelCase
        return {
          id: user.id,
          fullName: `${user.first_name} ${user.last_name}`,
          email: user.email,
          profileUrl: user.profile_pic_url || '/default-avatar.png',
          joinDate: new Date(user.created_at).toLocaleDateString()
        };
      }
    }
  }
);

// 3. Error Retry Interceptor
const retryResource = resourceSignal(
  fetchData,
  { id: 1 },
  {
    responseInterceptor: {
      onError: async (error, params) => {
        // For network errors, retry up to 3 times with exponential backoff
        if (error.message.includes('network') || error.message.includes('timeout')) {
          let retries = 0;
          const maxRetries = 3;
          
          while (retries < maxRetries) {
            retries++;
            try {
              // Wait with exponential backoff
              await new Promise(r => setTimeout(r, 1000 * Math.pow(2, retries - 1)));
              
              // Try again
              const result = await fetchData(params);
              console.log(`Retry succeeded after ${retries} attempts`);
              return result;
            } catch (retryError) {
              if (retries === maxRetries) {
                // Give up and throw the last error
                throw new Error(`Failed after ${maxRetries} retries: ${retryError.message}`);
              }
            }
          }
        }
        
        // For other errors, just let them through
        return error;
      }
    }
  }
);

// 4. Create a reusable auth interceptor
function createAuthenticatedResource(fetchFn, params, options = {}) {
  return resourceSignal(
    fetchFn,
    params,
    {
      ...options,
      requestInterceptor: {
        ...(options.requestInterceptor || {}),
        onRequest: params => {
          // Combine custom onRequest with auth token injection
          const customParams = options.requestInterceptor?.onRequest?.(params) || params;
          
          return {
            ...customParams,
            token: localStorage.getItem('auth_token')
          };
        }
      }
    }
  );
}

// Usage of the reusable auth interceptor
const userProfile = createAuthenticatedResource(
  fetchUserProfile,
  { userId: 123 }
);

const userOrders = createAuthenticatedResource(
  fetchUserOrders,
  { userId: 123, page: 1 }
);

// 5. Cache invalidation interceptor
const postsResource = resourceSignal(
  fetchPosts,
  {},
  {
    requestInterceptor: {
      onRequest: (params) => {
        // Add timestamp to force fresh data
        if (params.forceRefresh) {
          return {
            ...params,
            _t: Date.now(),
            forceRefresh: undefined // remove from actual request params
          };
        }
        return params;
      }
    }
  }
);

// Force a fresh fetch bypassing cache
postsResource.refetch({ forceRefresh: true });

10. Dependency Injection (provide/inject)

A dependency injection system to share values and signals throughout your application.

import { 
  provide, 
  inject, 
  provideSignal, 
  type InjectionKey 
} from 'oxidjs';

// Define injection keys
const themeKey: InjectionKey<string> = 'theme';
const userKey: InjectionKey<{id: number, name: string}> = 'currentUser';
const counterKey: InjectionKey<number> = 'counter'; 

// Basic provide and inject
provide(themeKey, 'dark');
const theme = inject<string>(themeKey);
console.log(theme); // 'dark'

// Using symbols as keys (safer for larger apps)
const API_URL = Symbol('apiUrl');
provide(API_URL, 'https://api.example.com');
const apiUrl = inject<string>(API_URL);

// Provide a reactive signal value
const [getCounter, setCounter] = provideSignal(counterKey, 0);

// Inject the signal getter
const counter = inject<() => number>(counterKey);
console.log(counter()); // 0

// Update the provided signal
setCounter(5);
console.log(counter()); // 5

// Update using a function
setCounter(prev => prev + 1);
console.log(counter()); // 6

// Default value if key is not found
const missingValue = inject('nonExistentKey', 'default');
console.log(missingValue); // 'default'

// Using with effects
provide(userKey, { id: 1, name: 'John' });

effect(() => {
  const user = inject<{id: number, name: string}>(userKey);
  console.log(`Current user: ${user.name}`);
});

// Update the provided value
provide(userKey, { id: 2, name: 'Jane' });
// Effect will run again: "Current user: Jane"

// Integration with computed values
const greeting = computed(() => {
  const user = inject<{name: string}>(userKey);
  const isDarkTheme = inject<string>(themeKey) === 'dark';
  
  return `Hello, ${user.name}! Using ${isDarkTheme ? 'dark' : 'light'} theme.`;
});

console.log(greeting()); // "Hello, Jane! Using dark theme."

// Provide a fallback object if not found
const settings = inject('settings', { notifications: true, sound: false });

11. Reactive Form System

A powerful reactive form system built on top of OxidJS's reactivity primitives. The form system enables easy management of HTML forms with validation, state tracking, and seamless integration with the rest of your reactive application.

import { createForm, required, email, minLength } from 'oxidjs';

// Create a form with initial values and validation rules
const form = createForm(
  {
    name: '',
    email: '',
    password: ''
  },
  {
    name: [required('Name is required')],
    email: [required('Email is required'), email('Invalid email format')],
    password: [required('Password is required'), minLength(8, 'Password too short')]
  }
);

// Get access to a specific field
const nameField = form.getField('name');

// Update a field value
nameField.setValue('John Doe');

// Check if the field is valid
const nameErrors = nameField.errors();
if (nameErrors.length === 0) {
  console.log('Name field is valid');
}

// Validate the entire form
const isValid = await form.validate();
if (isValid) {
  console.log('Form is valid:', form.values());
}

// Handle form submission
const handleSubmit = form.handleSubmit(values => {
  console.log('Submitting form with values:', values);
  // Submit to server, etc.
});

Form Features

  • Reactive Form State: All form state changes are tracked reactively using signals
  • Field-Level and Form-Level Validation: Validate fields individually or the entire form
  • Async Validation Support: Validate fields asynchronously
  • Rich Validation Library: Common validators for required fields, patterns, numeric constraints, and more
  • Form Field Tracking: Track if fields are dirty, touched, or validating
  • HTML Form Integration: Works with standard HTML form elements
  • TypeScript Support: Full type safety for form values and validation rules

HTML Integration

The form system works seamlessly with standard HTML elements:

// HTML binding example
const emailField = form.getField('email');
const emailBinding = emailField.bind();

// In your HTML:
// <input 
//   value={emailBinding.value} 
//   onInput={emailBinding.onInput} 
//   onBlur={emailBinding.onBlur}
// />

Available Validators

  • required: Ensures a field has a value
  • minLength: Validates minimum string length
  • maxLength: Validates maximum string length
  • email: Validates email format
  • pattern: Validates against a regular expression
  • min: Validates minimum numeric value
  • max: Validates maximum numeric value
  • isNumber: Ensures value is a number
  • custom: Create your own custom validator
  • asyncValidator: Create asynchronous validators

Integration with Reactive System

import { createForm, createSignal, effect } from 'oxidjs';

// Reactive signals for form default values
const [getName, setName] = createSignal('John');
const [getEmail, setEmail] = createSignal('[email protected]');

// Create form with signals
const form = createForm({
  name: getName(),
  email: getEmail()
});

// Effect to update form when signals change
effect(() => {
  form.setValues({
    name: getName(),
    email: getEmail()
  });
});

// Effect to update signals when form changes
effect(() => {
  const values = form.values();
  setName(values.name);
  setEmail(values.email);
});

Multi-Step Forms

Multi-step forms allow you to break complex forms into manageable steps with navigation controls and per-step validation.

import { createMultiStepForm } from 'oxidjs';

// Define your form type
type RegistrationForm = {
  // Step 1: Personal details
  firstName: string;
  lastName: string;
  email: string;
  
  // Step 2: Address
  street: string;
  city: string;
  postalCode: string;
};

// Create the multi-step form
const form = createMultiStepForm<RegistrationForm>(
  // Initial values
  {
    firstName: '',
    lastName: '',
    email: '',
    street: '',
    city: '',
    postalCode: ''
  },
  // Step configuration
  [
    // Step 1: Personal details
    {
      fields: ['firstName', 'lastName', 'email'],
      validation: {
        // Regular validation rules work here
        email: [required(), email()]
      }
    },
    // Step 2: Address
    {
      fields: ['street', 'city', 'postalCode'],
      validation: {
        street: [required()],
        city: [required()],
        postalCode: [required(), pattern(/^\d{5}$/)]
      }
    }
  ],
  // Options
  {
    rememberProgress: true, // Persist form state between sessions
    allowJumpingBetweenSteps: false // Require sequential navigation
  }
);

// Handle navigation and submission
if (form.hasNextStep()) {
  form.next(); // Validate current step and move to next
} else {
  form.handleSubmit(values => {
    // Process the complete form
  });
}

Zod Validation Integration

OxidJS provides optional integration with Zod for advanced validation scenarios.

import { createForm, zodValidator, zodSchemaValidator } from 'oxidjs';
import { z } from 'zod';

// Define your Zod schema
const userSchema = z.object({
  username: z.string().min(3).max(20),
  email: z.string().email(),
  age: z.number().min(18)
});

// Create form with Zod validators
const form = createForm(
  {
    username: '',
    email: '',
    age: 0
  },
  {
    username: [zodValidator(userSchema.shape.username)],
    email: [zodValidator(userSchema.shape.email)],
    age: [zodValidator(userSchema.shape.age)]
  }
);

// Or convert entire schema to validation rules
const schemaForm = createForm(
  {
    username: '',
    email: '',
    age: 0
  },
  zodSchemaValidator({
    username: userSchema.shape.username,
    email: userSchema.shape.email,
    age: userSchema.shape.age
  })
);

12. State Management with Store

OxidJS provides powerful and flexible state management capabilities through its Store API. Stores allow for managing complex application state with built-in persistence, reactivity, and integration with the signal system.

import { createStore, createReactiveStore, createGlobalStore } from 'oxidjs';
import { createSignal, effect } from 'oxidjs';

// Basic store with initial state
const store = createStore({ count: 0, user: { name: 'John', role: 'user' } });

// Get the current state
console.log(store.getState()); // { count: 0, user: { name: 'John', role: 'user' } }

// Update the state
store.setState({ count: 1, user: { name: 'John', role: 'user' } });

// Update with a function (recommended for updates based on previous state)
store.setState(prev => ({ 
  ...prev, 
  count: prev.count + 1,
  user: { ...prev.user, role: 'admin' }
}));

// Subscribe to state changes
const unsubscribe = store.subscribe(state => {
  console.log('State updated:', state);
});

// Later, unsubscribe when no longer needed
unsubscribe();

Store with Persistence

Stores can save and load state from various storage providers:

import { createStore } from 'oxidjs';

// Create a store with localStorage persistence
const persistedStore = createStore(
  { count: 0, theme: 'light' },
  { 
    storage: 'localStorage', // Uses localStorage by default
    storageKey: 'app-settings', // Key to use in storage
    loadFromStorageOnInit: true // Load saved state on initialization
  }
);

// Explicitly persist state (normally happens automatically on setState)
await persistedStore.persist();

// Create store with advanced storage options
const advancedStore = createStore(
  { user: null, preferences: {} },
  {
    // Will try IndexedDB, then localStorage, then sessionStorage, then memory
    storage: 'auto',
    storageKey: 'user-data',
    debounceTime: 500, // Debounce persistence operations by 500ms
    serializer: {
      // Custom serialization/deserialization
      serialize: (data) => JSON.stringify(data),
      deserialize: (str) => JSON.parse(str)
    }
  }
);

Reactive Store Integration with Signals

The reactive store provides seamless integration with OxidJS signals:

import { createReactiveStore, effect } from 'oxidjs';

// Create a reactive store
const store = createReactiveStore({ 
  count: 0, 
  user: { name: 'John', age: 30 } 
});

// Connect a property directly to signals
const [getCount, setCount] = store.connectProperty('count');

// Get the current value 
console.log(getCount()); // 0

// Update through signal (automatically updates store)
setCount(5);
console.log(store.getState().count); // 5

// Update through store (automatically updates signal)
store.setState({ count: 10, user: { name: 'John', age: 30 } });
console.log(getCount()); // 10

// Create a computed selector from store state
const fullName = store.select(state => 
  `${state.user.name} (${state.user.age})`
);

// Effect that reacts to store changes
effect(() => {
  console.log(`Current count: ${getCount()}`);
});

// Bi-directional computed values
const [getFahrenheit, setFahrenheit] = store.connectComputed(
  // Get derived value from state
  state => state.celsius * 9/5 + 32,
  // Update original state when derived value changes
  (state, fahrenheit) => ({
    ...state,
    celsius: (fahrenheit - 32) * 5/9
  })
);

Global Store for Application-wide State

For application-wide state management that can be accessed from anywhere:

import { createGlobalStore, hydrateStores, createHydrationData } from 'oxidjs';

// Create a global store
const userStore = createGlobalStore('user', { 
  id: null, 
  name: '',
  isLoggedIn: false
});

// Access the same store from anywhere in your application
function login(userId, userName) {
  // Get the same store instance
  const store = createGlobalStore('user');
  
  // Update the global state
  store.setState({
    id: userId,
    name: userName,
    isLoggedIn: true
  });
}

// State changes are reflected everywhere
login(1, 'John Doe');
console.log(userStore.getState()); // { id: 1, name: 'John Doe', isLoggedIn: true }

// Server-Side Rendering Support with Hydration
// On the server:
const serverStore = createGlobalStore('app', { theme: 'dark', lang: 'en' });
const getHydrationData = createHydrationData(['app', 'user']);
const hydrationData = getHydrationData();

// On the client:
hydrateStores(hydrationData);

Store Migrations

Stores support automatic data migrations between versions:

import { createStore } from 'oxidjs';

// Create a store with version and migration support
const store = createStore(
  { version: 2, settings: { theme: 'light', fontSize: 14 } },
  {
    version: 2, // Current version
    migrate: (oldData, oldVersion, newVersion) => {
      // Migrate from version 1 to version 2
      if (oldVersion === 1 && newVersion === 2) {
        return {
          ...oldData,
          version: 2,
          settings: {
            ...oldData.settings,
            fontSize: 14 // Add missing field
          }
        };
      }
      return oldData;
    }
  }
);

Advanced Store Integration with Signals

Combine stores with signals for powerful state management:

import { createStore, createSignal, derived, effect, batch } from 'oxidjs';

const store = createStore({
  user: { name: 'John', age: 30 },
  preferences: { theme: 'light', fontSize: 14 },
  todos: [
    { id: 1, text: 'Learn OxidJS', completed: false },
    { id: 2, text: 'Build an app', completed: false }
  ]
});

// Create signals that mirror store state
const [getUser, setUser] = createSignal(store.getState().user);
const [getTodos, setTodos] = createSignal(store.getState().todos);

// Create derived signals
const incompleteTodos = derived([getTodos], todos => 
  todos.filter(todo => !todo.completed)
);

const todoSummary = derived([getUser, incompleteTodos], 
  (user, todos) => `${user.name} has ${todos.length} tasks remaining`
);

// Sync store → signals
const unsubscribe = store.subscribe((state) => {
  setUser(state.user);
  setTodos(state.todos);
});

// Example: Update store
store.setState(prev => ({
  ...prev,
  todos: prev.todos.map(todo => 
    todo.id === 1 ? { ...todo, completed: true } : todo
  )
}));

// The derived signals automatically update
console.log(todoSummary()); // "John has 1 tasks remaining"

// Batch multiple updates for efficiency
batch(() => {
  store.setState(prev => ({
    ...prev,
    user: { ...prev.user, name: 'Jane' },
    preferences: { ...prev.preferences, theme: 'dark' }
  }));
});

Store with Fallback Storage

Handle multiple storage options with automatic fallback:

import { createStore } from 'oxidjs';

// Create a store that tries multiple storage options
const store = createStore(
  { userData: {}, appSettings: {} },
  {
    storage: 'fallback', // Use fallback storage
    storageOptions: {
      // Try these storage providers in order
      providers: ['indexedDB', 'localStorage', 'sessionStorage', 'memory'],
      // Customize storage options
      indexedDBOptions: {
        dbName: 'my-app-db',
        storeName: 'app-store'
      }
    },
    storageKey: 'app-data'
  }
);

// Check which storage provider is currently being used
const currentProvider = store.storage.getCurrentProvider();
console.log(`Using storage provider: ${currentProvider}`);

Advanced Usage

Combining Features

const [items, setItems] = createSignal([1, 2, 3]);
const [filter, setFilter] = createSignal('all');

// Computed value based on signals
const filteredItems = computed(() => {
  const currentItems = items();
  const currentFilter = filter();
  
  if (currentFilter === 'all') return currentItems;
  if (currentFilter === 'even') return currentItems.filter(n => n % 2 === 0);
  return currentItems.filter(n => n % 2 !== 0);
});

// Derived signal for count
const itemCount = derived([filteredItems], items => items.length);

// Effect for logging
effect(() => {
  console.log(`Showing ${itemCount()} ${filter()} items`);
});

// Batch updates
batch(() => {
  setItems([1, 2, 3, 4, 5]);
  setFilter('even');
});

Development

# Install dependencies
pnpm install

# Run tests
pnpm test

# Build library
pnpm build

# Development mode
pnpm dev

TypeScript Support

OxidJS is written in TypeScript and provides excellent type inference out of the box.

interface User {
  id: number;
  name: string;
}

const [user, setUser] = createSignal<User | null>(null);
const userName = computed(() => user()?.name ?? 'Anonymous');