A declarative JavaScript library for building user interfaces with fine-grained reactivity.
—
Nested reactive state management system with proxy-based stores, mutations, and advanced reconciliation for managing complex application state.
Create reactive stores for managing nested and complex state structures.
/**
* Creates a reactive store that can be read through a proxy object and written with a setter function
* @param store - Initial store value or existing store
* @param options - Configuration options
* @returns Tuple of [store getter proxy, store setter function]
*/
function createStore<T extends object = {}>(
...[store, options]: {} extends T
? [store?: T | Store<T>, options?: { name?: string }]
: [store: T | Store<T>, options?: { name?: string }]
): [get: Store<T>, set: SetStoreFunction<T>];
/**
* Returns the underlying data in the store without a proxy
* @param item - Store or value to unwrap
* @param set - Optional set to track unwrapped objects
* @returns Unwrapped data
*/
function unwrap<T>(item: T, set?: Set<unknown>): T;
interface StoreNode {
[$NODE]?: DataNodes;
[key: PropertyKey]: any;
}
type NotWrappable = string | number | bigint | symbol | boolean | Function | null | undefined;
type Store<T> = T;Usage Examples:
import { createStore, unwrap } from "solid-js/store";
// Basic store creation
const [user, setUser] = createStore({
name: "John Doe",
email: "john@example.com",
profile: {
age: 30,
bio: "Software developer",
preferences: {
theme: "dark",
notifications: true
}
},
posts: []
});
// Reading from store (reactive)
console.log(user.name); // "John Doe"
console.log(user.profile.age); // 30
// Updating store values
setUser("name", "Jane Doe");
setUser("profile", "age", 31);
setUser("profile", "preferences", "theme", "light");
// Functional updates
setUser("profile", "age", age => age + 1);
// Adding to arrays
setUser("posts", posts => [...posts, { id: 1, title: "First Post" }]);
// Unwrapping store data (removes reactivity)
const rawUserData = unwrap(user);
console.log(rawUserData); // Plain JavaScript objectPerform complex updates with nested path setting and batch operations.
interface SetStoreFunction<T> {
// Set entire store
(value: T): void;
(setter: (prev: T) => T): void;
// Set by key
<K extends keyof T>(key: K, value: T[K]): void;
<K extends keyof T>(key: K, setter: (prev: T[K]) => T[K]): void;
// Set by nested path
<K1 extends keyof T, K2 extends keyof T[K1]>(
key1: K1,
key2: K2,
value: T[K1][K2]
): void;
// Array operations
<K extends keyof T>(
key: K,
index: number,
value: T[K] extends readonly (infer U)[] ? U : never
): void;
// Conditional updates and more overloads...
}Usage Examples:
import { createStore } from "solid-js/store";
import { For, createEffect } from "solid-js";
function TodoApp() {
const [todos, setTodos] = createStore({
items: [
{ id: 1, text: "Learn Solid", completed: false },
{ id: 2, text: "Build an app", completed: false }
],
filter: "all" as "all" | "active" | "completed",
stats: {
total: 2,
active: 2,
completed: 0
}
});
// Update individual todo
const toggleTodo = (id: number) => {
setTodos(
"items",
item => item.id === id,
"completed",
completed => !completed
);
updateStats();
};
// Add new todo
const addTodo = (text: string) => {
const newTodo = {
id: Date.now(),
text,
completed: false
};
setTodos("items", items => [...items, newTodo]);
updateStats();
};
// Remove todo
const removeTodo = (id: number) => {
setTodos("items", items => items.filter(item => item.id !== id));
updateStats();
};
// Update statistics
const updateStats = () => {
setTodos("stats", {
total: todos.items.length,
active: todos.items.filter(item => !item.completed).length,
completed: todos.items.filter(item => item.completed).length
});
};
// Batch operations
const clearCompleted = () => {
setTodos("items", items => items.filter(item => !item.completed));
updateStats();
};
const toggleAll = () => {
const allCompleted = todos.items.every(item => item.completed);
setTodos("items", {}, "completed", !allCompleted);
updateStats();
};
return (
<div class="todo-app">
<h1>Todo App</h1>
<div class="stats">
<span>Total: {todos.stats.total}</span>
<span>Active: {todos.stats.active}</span>
<span>Completed: {todos.stats.completed}</span>
</div>
<div class="controls">
<button onClick={toggleAll}>Toggle All</button>
<button onClick={clearCompleted}>Clear Completed</button>
<select
value={todos.filter}
onChange={(e) => setTodos("filter", e.target.value as any)}
>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
</div>
<For each={filteredTodos()}>
{(todo) => (
<div class={`todo ${todo.completed ? "completed" : ""}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)}>Remove</button>
</div>
)}
</For>
</div>
);
function filteredTodos() {
switch (todos.filter) {
case "active":
return todos.items.filter(item => !item.completed);
case "completed":
return todos.items.filter(item => item.completed);
default:
return todos.items;
}
}
}Create mutable stores that can be modified directly like plain objects.
/**
* Creates a mutable store that can be mutated directly
* @param state - Initial state
* @param options - Configuration options
* @returns Mutable store object
*/
function createMutable<T extends StoreNode>(
state: T,
options?: { name?: string }
): T;
/**
* Modifies a mutable store using a modifier function
* @param state - Mutable store to modify
* @param modifier - Function that modifies the state (mutates in place)
*/
function modifyMutable<T>(
state: T,
modifier: (state: T) => void
): void;Usage Examples:
import { createMutable, modifyMutable } from "solid-js/store";
import { createEffect } from "solid-js";
function MutableStoreExample() {
const state = createMutable({
user: {
name: "John",
age: 30,
preferences: {
theme: "dark",
language: "en"
}
},
items: [1, 2, 3]
});
// Direct mutation (triggers reactivity)
const updateUser = () => {
state.user.name = "Jane";
state.user.age++;
state.user.preferences.theme = state.user.preferences.theme === "dark" ? "light" : "dark";
};
// Array mutations
const addItem = () => {
state.items.push(state.items.length + 1);
};
const removeItem = () => {
state.items.pop();
};
// Using modifyMutable for complex updates
const resetState = () => {
modifyMutable(state, (draft) => {
draft.user.name = "Anonymous";
draft.user.age = 0;
draft.user.preferences = { theme: "light", language: "en" };
draft.items = [];
return draft;
});
};
// Reactive effects work with mutable stores
createEffect(() => {
console.log("User changed:", state.user.name, state.user.age);
});
createEffect(() => {
console.log("Items count:", state.items.length);
});
return (
<div>
<h2>Mutable Store Example</h2>
<div>
<p>User: {state.user.name} (Age: {state.user.age})</p>
<p>Theme: {state.user.preferences.theme}</p>
<p>Items: {state.items.join(", ")}</p>
</div>
<div>
<button onClick={updateUser}>Update User</button>
<button onClick={addItem}>Add Item</button>
<button onClick={removeItem}>Remove Item</button>
<button onClick={resetState}>Reset</button>
</div>
</div>
);
}Use advanced modifiers for efficient updates and reconciliation.
/**
* Diff method for setStore to efficiently update nested data
* @param value - New value to reconcile with
* @param options - Reconciliation options (defaults to empty object)
* @returns Function that performs the reconciliation
*/
function reconcile<T extends U, U>(
value: T,
options: {
key?: string | null;
merge?: boolean;
} = {}
): (state: U) => T;
/**
* Immer-style mutation helper for stores
* @param fn - Function that mutates the draft state
* @returns Function that applies the mutations
*/
function produce<T>(
fn: (state: T) => void
): (state: T) => T;Usage Examples:
import { createStore, reconcile, produce } from "solid-js/store";
function AdvancedStoreExample() {
const [data, setData] = createStore({
users: [
{ id: 1, name: "John", posts: [] },
{ id: 2, name: "Jane", posts: [] }
],
settings: {
theme: "dark",
language: "en"
}
});
// Reconcile with new data (efficient updates)
const updateUsers = async () => {
const newUsers = await fetchUsers(); // Assume this returns updated user data
setData("users", reconcile(newUsers, { key: "id" }));
};
// Using produce for complex mutations
const addPostToUser = (userId: number, post: { title: string; content: string }) => {
setData(produce((draft) => {
const user = draft.users.find(u => u.id === userId);
if (user) {
user.posts.push({ id: Date.now(), ...post });
}
}));
};
// Reconcile with merge option
const updateSettings = (newSettings: Partial<typeof data.settings>) => {
setData("settings", reconcile(newSettings, { merge: true }));
};
// Complex nested updates with produce
const complexUpdate = () => {
setData(produce((draft) => {
// Multiple complex operations
draft.users.forEach(user => {
if (user.posts.length > 5) {
user.posts = user.posts.slice(-5); // Keep only last 5 posts
}
});
// Update settings
draft.settings.theme = draft.settings.theme === "dark" ? "light" : "dark";
// Add new user if needed
if (draft.users.length < 10) {
draft.users.push({
id: Date.now(),
name: `User ${draft.users.length + 1}`,
posts: []
});
}
}));
};
return (
<div>
<div class="controls">
<button onClick={updateUsers}>Update Users</button>
<button onClick={() => addPostToUser(1, { title: "New Post", content: "Content" })}>
Add Post to User 1
</button>
<button onClick={() => updateSettings({ language: "es" })}>
Change Language
</button>
<button onClick={complexUpdate}>Complex Update</button>
</div>
<div class="data">
<h3>Users ({data.users.length})</h3>
<For each={data.users}>
{(user) => (
<div class="user">
<h4>{user.name}</h4>
<p>Posts: {user.posts.length}</p>
</div>
)}
</For>
<h3>Settings</h3>
<p>Theme: {data.settings.theme}</p>
<p>Language: {data.settings.language}</p>
</div>
</div>
);
}
async function fetchUsers() {
// Simulate API call
return [
{ id: 1, name: "John Updated", posts: [{ id: 1, title: "Post 1" }] },
{ id: 2, name: "Jane Updated", posts: [] },
{ id: 3, name: "New User", posts: [] }
];
}Access raw data and debugging utilities for store management.
/**
* Symbol for accessing raw store data
*/
const $RAW: unique symbol;
/**
* Symbol for accessing store nodes
*/
const $NODE: unique symbol;
/**
* Symbol for tracking store property access
*/
const $HAS: unique symbol;
/**
* Symbol for self-reference in stores
*/
const $SELF: unique symbol;
/**
* Checks if an object can be wrapped by the store proxy
* @param obj - Object to check
* @returns True if the object can be wrapped
*/
function isWrappable<T>(obj: T | NotWrappable): obj is T;
/**
* Development utilities for stores
*/
const DEV: {
$NODE: symbol;
isWrappable: (obj: any) => boolean;
hooks: {
onStoreNodeUpdate: OnStoreNodeUpdate | null;
};
} | undefined;
type OnStoreNodeUpdate = (node: any, property: string | number | symbol, value: any, prev: any) => void;Usage Examples:
import { createStore, unwrap, $RAW } from "solid-js/store";
function StoreDebugging() {
const [store, setStore] = createStore({
nested: {
data: { count: 0 },
array: [1, 2, 3]
}
});
// Access raw data using $RAW symbol
console.log(store[$RAW]); // Raw underlying data
// Unwrap for serialization
const serializeStore = () => {
const unwrapped = unwrap(store);
return JSON.stringify(unwrapped, null, 2);
};
// Compare wrapped vs unwrapped
const compareData = () => {
console.log("Wrapped:", store.nested.data);
console.log("Unwrapped:", unwrap(store.nested.data));
console.log("Are same reference:", store.nested.data === unwrap(store.nested.data)); // false
};
// Development hooks (only available in dev mode)
if (DEV) {
DEV.hooks.onStoreNodeUpdate = (node, property, value, prev) => {
console.log("Store update:", { node, property, value, prev });
};
}
return (
<div>
<h2>Store Debugging</h2>
<div>
<p>Count: {store.nested.data.count}</p>
<p>Array: [{store.nested.array.join(", ")}]</p>
</div>
<div>
<button onClick={() => setStore("nested", "data", "count", c => c + 1)}>
Increment Count
</button>
<button onClick={() => setStore("nested", "array", arr => [...arr, arr.length + 1])}>
Add to Array
</button>
<button onClick={compareData}>
Compare Data
</button>
</div>
<div>
<h3>Serialized Store:</h3>
<pre>{serializeStore()}</pre>
</div>
</div>
);
}Advanced internal functions exported for library authors and debugging.
/**
* Gets data nodes from a store target
* @param target - Store target object
* @param symbol - Symbol to access ($NODE or $HAS)
* @returns Data nodes for the target
*/
function getNodes(target: StoreNode, symbol: typeof $NODE | typeof $HAS): DataNodes;
/**
* Gets a specific node from data nodes
* @param nodes - Data nodes collection
* @param property - Property key to access
* @param value - Optional value for node creation
* @returns Data node for the property
*/
function getNode(nodes: DataNodes, property: PropertyKey, value?: any): DataNode;
/**
* Sets a property value on a store node
* @param state - Store node to update
* @param property - Property key to set
* @param value - Value to set
* @param deleting - Whether this is a deletion operation
*/
function setProperty(state: StoreNode, property: PropertyKey, value: any, deleting?: boolean): void;
/**
* Updates a nested path in the store
* @param current - Current store node
* @param path - Path array to update
* @param traversed - Properties already traversed
*/
function updatePath(current: StoreNode, path: any[], traversed?: PropertyKey[]): void;type Store<T> = T;
interface StoreNode {
[$NODE]?: DataNodes;
[key: PropertyKey]: any;
}
type NotWrappable = string | number | bigint | symbol | boolean | Function | null | undefined;
type DataNodes = Record<PropertyKey, DataNode>;
interface DataNode {
value: any;
listeners?: Set<Function>;
}
interface SetStoreFunction<T> {
// Root level updates
(value: T): void;
(setter: (prev: T) => T): void;
// Key-based updates
<K extends keyof T>(key: K, value: T[K]): void;
<K extends keyof T>(key: K, setter: (prev: T[K]) => T[K]): void;
// Nested path updates (multiple overloads for different depths)
<K1 extends keyof T, K2 extends keyof T[K1]>(
key1: K1,
key2: K2,
value: T[K1][K2]
): void;
// Conditional updates
<K extends keyof T>(
key: K,
predicate: (item: T[K], index: number) => boolean,
value: T[K]
): void;
}interface ReconcileOptions {
key?: string | null;
merge?: boolean;
}
type ReconcileFunction<T, U = T> = (state: U) => T;Install with Tessl CLI
npx tessl i tessl/npm-solid-js