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 oxidjsCore 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()); // 2403. 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()); // 205. 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 valueminLength: Validates minimum string lengthmaxLength: Validates maximum string lengthemail: Validates email formatpattern: Validates against a regular expressionmin: Validates minimum numeric valuemax: Validates maximum numeric valueisNumber: Ensures value is a numbercustom: Create your own custom validatorasyncValidator: 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 devTypeScript 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');