Compare commits
3 Commits
90f91bd017
...
29a2dfc606
| Author | SHA1 | Date | |
|---|---|---|---|
| 29a2dfc606 | |||
| 2999325de9 | |||
| 06584ffedc |
@@ -6,7 +6,7 @@
|
|||||||
home.sessionVariables = {
|
home.sessionVariables = {
|
||||||
OPENCODE_ENABLE_EXA = 1;
|
OPENCODE_ENABLE_EXA = 1;
|
||||||
OPENCODE_EXPERIMENTAL_LSP_TOOL = 1;
|
OPENCODE_EXPERIMENTAL_LSP_TOOL = 1;
|
||||||
# OPENCODE_EXPERIMENTAL_MARKDOWN = 1;
|
OPENCODE_EXPERIMENTAL_MARKDOWN = 1;
|
||||||
OPENCODE_EXPERIMENTAL_PLAN_MODE = 1;
|
OPENCODE_EXPERIMENTAL_PLAN_MODE = 1;
|
||||||
OPENCODE_EXPERIMENTAL_FILE_WATCHER = 1;
|
OPENCODE_EXPERIMENTAL_FILE_WATCHER = 1;
|
||||||
};
|
};
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
package = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.opencode;
|
package = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.opencode;
|
||||||
settings = {
|
settings = {
|
||||||
model = "opencode/claude-opus-4-5";
|
model = "anthropic/claude-opus-4-5";
|
||||||
small_model = "opencode/minimax-m2.1";
|
small_model = "opencode/minimax-m2.1";
|
||||||
theme = "catppuccin";
|
theme = "catppuccin";
|
||||||
|
plugin = ["opencode-anthropic-auth"];
|
||||||
keybinds = {
|
keybinds = {
|
||||||
leader = "ctrl+o";
|
leader = "ctrl+o";
|
||||||
};
|
};
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
};
|
};
|
||||||
agent = {
|
agent = {
|
||||||
plan = {
|
plan = {
|
||||||
model = "opencode/gpt-5.2-codex";
|
model = "anthropic/claude-opus-4-5";
|
||||||
};
|
};
|
||||||
explore = {
|
explore = {
|
||||||
model = "opencode/minimax-m2.1";
|
model = "opencode/minimax-m2.1";
|
||||||
@@ -64,22 +65,6 @@
|
|||||||
type = "local";
|
type = "local";
|
||||||
command = ["bunx" "opensrc-mcp"];
|
command = ["bunx" "opensrc-mcp"];
|
||||||
};
|
};
|
||||||
appsignal = {
|
|
||||||
enabled = true;
|
|
||||||
type = "local";
|
|
||||||
command = [
|
|
||||||
"docker"
|
|
||||||
"run"
|
|
||||||
"-i"
|
|
||||||
"--rm"
|
|
||||||
"-e"
|
|
||||||
"APPSIGNAL_API_KEY"
|
|
||||||
"appsignal/mcp"
|
|
||||||
];
|
|
||||||
environment = {
|
|
||||||
APPSIGNAL_API_KEY = "{env:APPSIGNAL_API_KEY}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
overseer = {
|
overseer = {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
type = "local";
|
type = "local";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
description: Review changes with parallel @code-review subagents
|
description: Review changes with parallel @code-review subagents
|
||||||
agent: plan
|
|
||||||
---
|
---
|
||||||
Review the code changes using THREE (3) @code-review subagents and correlate results into a summary ranked by severity. Use the provided user guidance to steer the review and focus on specific code paths, changes, and/or areas of concern.
|
Review the code changes using THREE (3) @code-review subagents and correlate results into a summary ranked by severity. Use the provided user guidance to steer the review and focus on specific code paths, changes, and/or areas of concern.
|
||||||
|
|
||||||
|
|||||||
464
profiles/opencode/skill/solidjs/SKILL.md
Normal file
464
profiles/opencode/skill/solidjs/SKILL.md
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
---
|
||||||
|
name: solidjs
|
||||||
|
description: |
|
||||||
|
SolidJS framework development skill for building reactive web applications with fine-grained reactivity.
|
||||||
|
Use when working with SolidJS projects including: (1) Creating components with signals, stores, and effects,
|
||||||
|
(2) Implementing reactive state management, (3) Using control flow components (Show, For, Switch/Match),
|
||||||
|
(4) Setting up routing with Solid Router, (5) Building full-stack apps with SolidStart,
|
||||||
|
(6) Data fetching with createResource, (7) Context API for shared state, (8) SSR/SSG configuration.
|
||||||
|
Triggers: solid, solidjs, solid-js, solid start, solidstart, createSignal, createStore, createEffect.
|
||||||
|
---
|
||||||
|
|
||||||
|
# SolidJS Development
|
||||||
|
|
||||||
|
SolidJS is a declarative JavaScript library for building user interfaces with fine-grained reactivity. Unlike virtual DOM frameworks, Solid compiles templates to real DOM nodes and updates them with fine-grained reactions.
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
1. **Components run once** — Component functions execute only during initialization, not on every update
|
||||||
|
2. **Fine-grained reactivity** — Only the specific DOM nodes that depend on changed data update
|
||||||
|
3. **No virtual DOM** — Direct DOM manipulation via compiled templates
|
||||||
|
4. **Signals are functions** — Access values by calling: `count()` not `count`
|
||||||
|
|
||||||
|
## Reactivity Primitives
|
||||||
|
|
||||||
|
### Signals — Basic State
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
|
||||||
|
// Read value (getter)
|
||||||
|
console.log(count()); // 0
|
||||||
|
|
||||||
|
// Update value (setter)
|
||||||
|
setCount(1);
|
||||||
|
setCount(prev => prev + 1); // Functional update
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
```tsx
|
||||||
|
const [value, setValue] = createSignal(initialValue, {
|
||||||
|
equals: false, // Always trigger updates, even if value unchanged
|
||||||
|
name: "debugName" // For devtools
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Effects — Side Effects
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createEffect } from "solid-js";
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("Count changed:", count());
|
||||||
|
// Runs after render, re-runs when dependencies change
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key behaviors:**
|
||||||
|
- Initial run: after render, before browser paint
|
||||||
|
- Subsequent runs: when tracked dependencies change
|
||||||
|
- Never runs during SSR or hydration
|
||||||
|
- Use `onCleanup` for cleanup logic
|
||||||
|
|
||||||
|
### Memos — Derived/Cached Values
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createMemo } from "solid-js";
|
||||||
|
|
||||||
|
const doubled = createMemo(() => count() * 2);
|
||||||
|
|
||||||
|
// Access like signal
|
||||||
|
console.log(doubled()); // Cached, only recalculates when count changes
|
||||||
|
```
|
||||||
|
|
||||||
|
Use memos when:
|
||||||
|
- Derived value is expensive to compute
|
||||||
|
- Derived value is accessed multiple times
|
||||||
|
- You want to prevent downstream updates when result unchanged
|
||||||
|
|
||||||
|
### Resources — Async Data
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createResource } from "solid-js";
|
||||||
|
|
||||||
|
const [user, { mutate, refetch }] = createResource(userId, fetchUser);
|
||||||
|
|
||||||
|
// In JSX
|
||||||
|
<Show when={!user.loading} fallback={<Loading />}>
|
||||||
|
<div>{user()?.name}</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
// Resource properties
|
||||||
|
user.loading // boolean
|
||||||
|
user.error // error if failed
|
||||||
|
user.state // "unresolved" | "pending" | "ready" | "refreshing" | "errored"
|
||||||
|
user.latest // last successful value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stores — Complex State
|
||||||
|
|
||||||
|
For nested objects/arrays with fine-grained updates:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
|
||||||
|
const [state, setState] = createStore({
|
||||||
|
user: { name: "John", age: 30 },
|
||||||
|
todos: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Path syntax updates
|
||||||
|
setState("user", "name", "Jane");
|
||||||
|
setState("todos", todos => [...todos, newTodo]);
|
||||||
|
setState("todos", 0, "completed", true);
|
||||||
|
|
||||||
|
// Produce for immer-like updates
|
||||||
|
import { produce } from "solid-js/store";
|
||||||
|
setState(produce(s => {
|
||||||
|
s.user.age++;
|
||||||
|
s.todos.push(newTodo);
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Store utilities:**
|
||||||
|
- `produce` — Immer-like mutations
|
||||||
|
- `reconcile` — Diff and patch data (for API responses)
|
||||||
|
- `unwrap` — Get raw non-reactive object
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Basic Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Component } from "solid-js";
|
||||||
|
|
||||||
|
const MyComponent: Component<{ name: string }> = (props) => {
|
||||||
|
return <div>Hello, {props.name}</div>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props Handling
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { splitProps, mergeProps } from "solid-js";
|
||||||
|
|
||||||
|
// Default props
|
||||||
|
const merged = mergeProps({ size: "medium" }, props);
|
||||||
|
|
||||||
|
// Split props (for spreading)
|
||||||
|
const [local, others] = splitProps(props, ["class", "onClick"]);
|
||||||
|
return <button class={local.class} {...others} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Props rules:**
|
||||||
|
- Props are reactive getters — don't destructure at top level
|
||||||
|
- Use `props.value` in JSX, not `const { value } = props`
|
||||||
|
|
||||||
|
### Children Helper
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { children } from "solid-js";
|
||||||
|
|
||||||
|
const Wrapper: Component = (props) => {
|
||||||
|
const resolved = children(() => props.children);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("Children:", resolved());
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>{resolved()}</div>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Control Flow Components
|
||||||
|
|
||||||
|
### Show — Conditional Rendering
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
|
||||||
|
<Show when={user()} fallback={<Login />}>
|
||||||
|
{(user) => <Profile user={user()} />}
|
||||||
|
</Show>
|
||||||
|
```
|
||||||
|
|
||||||
|
### For — List Rendering (keyed by reference)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { For } from "solid-js";
|
||||||
|
|
||||||
|
<For each={items()} fallback={<Empty />}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div>{index()}: {item.name}</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** `index` is a signal, `item` is the value.
|
||||||
|
|
||||||
|
### Index — List Rendering (keyed by index)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Index } from "solid-js";
|
||||||
|
|
||||||
|
<Index each={items()}>
|
||||||
|
{(item, index) => (
|
||||||
|
<input value={item().text} />
|
||||||
|
)}
|
||||||
|
</Index>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** `item` is a signal, `index` is the value. Better for primitive arrays or inputs.
|
||||||
|
|
||||||
|
### Switch/Match — Multiple Conditions
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Switch, Match } from "solid-js";
|
||||||
|
|
||||||
|
<Switch fallback={<Default />}>
|
||||||
|
<Match when={state() === "loading"}>
|
||||||
|
<Loading />
|
||||||
|
</Match>
|
||||||
|
<Match when={state() === "error"}>
|
||||||
|
<Error />
|
||||||
|
</Match>
|
||||||
|
<Match when={state() === "success"}>
|
||||||
|
<Success />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic — Dynamic Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Dynamic } from "solid-js/web";
|
||||||
|
|
||||||
|
<Dynamic component={selected()} someProp="value" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Portal — Render Outside DOM Hierarchy
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Portal } from "solid-js/web";
|
||||||
|
|
||||||
|
<Portal mount={document.body}>
|
||||||
|
<Modal />
|
||||||
|
</Portal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ErrorBoundary — Error Handling
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ErrorBoundary } from "solid-js";
|
||||||
|
|
||||||
|
<ErrorBoundary fallback={(err, reset) => (
|
||||||
|
<div>
|
||||||
|
Error: {err.message}
|
||||||
|
<button onClick={reset}>Retry</button>
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
|
<RiskyComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Suspense — Async Loading
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Suspense } from "solid-js";
|
||||||
|
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<AsyncComponent />
|
||||||
|
</Suspense>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context API
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createContext, useContext } from "solid-js";
|
||||||
|
|
||||||
|
// Create context
|
||||||
|
const CounterContext = createContext<{
|
||||||
|
count: () => number;
|
||||||
|
increment: () => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Provider component
|
||||||
|
export function CounterProvider(props) {
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CounterContext.Provider value={{
|
||||||
|
count,
|
||||||
|
increment: () => setCount(c => c + 1)
|
||||||
|
}}>
|
||||||
|
{props.children}
|
||||||
|
</CounterContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumer hook
|
||||||
|
export function useCounter() {
|
||||||
|
const ctx = useContext(CounterContext);
|
||||||
|
if (!ctx) throw new Error("useCounter must be used within CounterProvider");
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { onMount, onCleanup } from "solid-js";
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
onMount(() => {
|
||||||
|
console.log("Mounted");
|
||||||
|
const handler = () => {};
|
||||||
|
window.addEventListener("resize", handler);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener("resize", handler);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>Content</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Refs
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
let inputRef: HTMLInputElement;
|
||||||
|
|
||||||
|
<input ref={inputRef} />
|
||||||
|
<input ref={(el) => { /* el is the DOM element */ }} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Handling
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Standard events (lowercase)
|
||||||
|
<button onClick={handleClick}>Click</button>
|
||||||
|
<button onClick={(e) => handleClick(e)}>Click</button>
|
||||||
|
|
||||||
|
// Delegated events (on:)
|
||||||
|
<input on:input={handleInput} />
|
||||||
|
|
||||||
|
// Native events (on:) - not delegated
|
||||||
|
<div on:scroll={handleScroll} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Conditional Classes
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { clsx } from "clsx"; // or classList
|
||||||
|
|
||||||
|
<div class={clsx("base", { active: isActive() })} />
|
||||||
|
<div classList={{ active: isActive(), disabled: isDisabled() }} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Updates
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { batch } from "solid-js";
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
setName("John");
|
||||||
|
setAge(30);
|
||||||
|
// Effects run once after batch completes
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Untrack
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { untrack } from "solid-js";
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log(count()); // tracked
|
||||||
|
console.log(untrack(() => other())); // not tracked
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import type { Component, ParentComponent, JSX } from "solid-js";
|
||||||
|
|
||||||
|
// Basic component
|
||||||
|
const Button: Component<{ label: string }> = (props) => (
|
||||||
|
<button>{props.label}</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
// With children
|
||||||
|
const Layout: ParentComponent<{ title: string }> = (props) => (
|
||||||
|
<div>
|
||||||
|
<h1>{props.title}</h1>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Event handler types
|
||||||
|
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (e) => {
|
||||||
|
console.log(e.currentTarget);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create new project
|
||||||
|
npm create solid@latest my-app
|
||||||
|
|
||||||
|
# With template
|
||||||
|
npx degit solidjs/templates/ts my-app
|
||||||
|
|
||||||
|
# SolidStart
|
||||||
|
npm create solid@latest my-app -- --template solidstart
|
||||||
|
```
|
||||||
|
|
||||||
|
**vite.config.ts:**
|
||||||
|
```ts
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import solid from "vite-plugin-solid";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solid()]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
1. **Destructuring props** — Breaks reactivity
|
||||||
|
```tsx
|
||||||
|
// ❌ Bad
|
||||||
|
const { name } = props;
|
||||||
|
|
||||||
|
// ✅ Good
|
||||||
|
props.name
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Accessing signals outside tracking scope**
|
||||||
|
```tsx
|
||||||
|
// ❌ Won't update
|
||||||
|
console.log(count());
|
||||||
|
|
||||||
|
// ✅ Will update
|
||||||
|
createEffect(() => console.log(count()));
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Forgetting to call signal getters**
|
||||||
|
```tsx
|
||||||
|
// ❌ Passes the function
|
||||||
|
<div>{count}</div>
|
||||||
|
|
||||||
|
// ✅ Passes the value
|
||||||
|
<div>{count()}</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Using array index as key** — Use `<For>` for reference-keyed, `<Index>` for index-keyed
|
||||||
|
|
||||||
|
5. **Side effects during render** — Use `createEffect` or `onMount`
|
||||||
777
profiles/opencode/skill/solidjs/references/api_reference.md
Normal file
777
profiles/opencode/skill/solidjs/references/api_reference.md
Normal file
@@ -0,0 +1,777 @@
|
|||||||
|
# SolidJS API Reference
|
||||||
|
|
||||||
|
Complete reference for all SolidJS primitives, utilities, and component APIs.
|
||||||
|
|
||||||
|
## Basic Reactivity
|
||||||
|
|
||||||
|
### createSignal
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
const [getter, setter] = createSignal<T>(initialValue, options?);
|
||||||
|
|
||||||
|
// Options
|
||||||
|
interface SignalOptions<T> {
|
||||||
|
equals?: false | ((prev: T, next: T) => boolean);
|
||||||
|
name?: string;
|
||||||
|
internal?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```tsx
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
const [user, setUser] = createSignal<User | null>(null);
|
||||||
|
|
||||||
|
// Always update
|
||||||
|
const [data, setData] = createSignal(obj, { equals: false });
|
||||||
|
|
||||||
|
// Custom equality
|
||||||
|
const [items, setItems] = createSignal([], {
|
||||||
|
equals: (a, b) => a.length === b.length
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setter forms
|
||||||
|
setCount(5); // Direct value
|
||||||
|
setCount(prev => prev + 1); // Functional update
|
||||||
|
```
|
||||||
|
|
||||||
|
### createEffect
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createEffect } from "solid-js";
|
||||||
|
|
||||||
|
createEffect<T>(fn: (prev: T) => T, initialValue?: T, options?);
|
||||||
|
|
||||||
|
// Options
|
||||||
|
interface EffectOptions {
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```tsx
|
||||||
|
// Basic
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("Count:", count());
|
||||||
|
});
|
||||||
|
|
||||||
|
// With previous value
|
||||||
|
createEffect((prev) => {
|
||||||
|
console.log("Changed from", prev, "to", count());
|
||||||
|
return count();
|
||||||
|
}, count());
|
||||||
|
|
||||||
|
// With cleanup
|
||||||
|
createEffect(() => {
|
||||||
|
const handler = () => {};
|
||||||
|
window.addEventListener("resize", handler);
|
||||||
|
onCleanup(() => window.removeEventListener("resize", handler));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### createMemo
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createMemo } from "solid-js";
|
||||||
|
|
||||||
|
const getter = createMemo<T>(fn: (prev: T) => T, initialValue?: T, options?);
|
||||||
|
|
||||||
|
// Options
|
||||||
|
interface MemoOptions<T> {
|
||||||
|
equals?: false | ((prev: T, next: T) => boolean);
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```tsx
|
||||||
|
const doubled = createMemo(() => count() * 2);
|
||||||
|
const filtered = createMemo(() => items().filter(i => i.active));
|
||||||
|
|
||||||
|
// Previous value
|
||||||
|
const delta = createMemo((prev) => count() - prev, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### createResource
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createResource } from "solid-js";
|
||||||
|
|
||||||
|
const [resource, { mutate, refetch }] = createResource(
|
||||||
|
source?, // Optional reactive source
|
||||||
|
fetcher, // (source, info) => Promise<T>
|
||||||
|
options?
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resource properties
|
||||||
|
resource() // T | undefined
|
||||||
|
resource.loading // boolean
|
||||||
|
resource.error // any
|
||||||
|
resource.state // "unresolved" | "pending" | "ready" | "refreshing" | "errored"
|
||||||
|
resource.latest // T | undefined (last successful value)
|
||||||
|
|
||||||
|
// Options
|
||||||
|
interface ResourceOptions<T> {
|
||||||
|
initialValue?: T;
|
||||||
|
name?: string;
|
||||||
|
deferStream?: boolean;
|
||||||
|
ssrLoadFrom?: "initial" | "server";
|
||||||
|
storage?: (init: T) => [Accessor<T>, Setter<T>];
|
||||||
|
onHydrated?: (key, info: { value: T }) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```tsx
|
||||||
|
// Without source
|
||||||
|
const [users] = createResource(fetchUsers);
|
||||||
|
|
||||||
|
// With source
|
||||||
|
const [user] = createResource(userId, fetchUser);
|
||||||
|
|
||||||
|
// With options
|
||||||
|
const [data] = createResource(id, fetchData, {
|
||||||
|
initialValue: [],
|
||||||
|
deferStream: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
mutate(newValue); // Update locally
|
||||||
|
refetch(); // Re-fetch
|
||||||
|
refetch(customInfo); // Pass to fetcher's info.refetching
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stores
|
||||||
|
|
||||||
|
### createStore
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
|
||||||
|
const [store, setStore] = createStore<T>(initialValue);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update patterns:**
|
||||||
|
```tsx
|
||||||
|
const [state, setState] = createStore({
|
||||||
|
user: { name: "John", age: 30 },
|
||||||
|
todos: [{ id: 1, text: "Learn Solid", done: false }]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Path syntax
|
||||||
|
setState("user", "name", "Jane");
|
||||||
|
setState("user", "age", a => a + 1);
|
||||||
|
setState("todos", 0, "done", true);
|
||||||
|
|
||||||
|
// Array operations
|
||||||
|
setState("todos", t => [...t, newTodo]);
|
||||||
|
setState("todos", todos.length, newTodo);
|
||||||
|
|
||||||
|
// Multiple paths
|
||||||
|
setState("todos", { from: 0, to: 2 }, "done", true);
|
||||||
|
setState("todos", [0, 2, 4], "done", true);
|
||||||
|
setState("todos", i => i.done, "done", false);
|
||||||
|
|
||||||
|
// Object merge (shallow)
|
||||||
|
setState("user", { age: 31 }); // Keeps other properties
|
||||||
|
```
|
||||||
|
|
||||||
|
### produce
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { produce } from "solid-js/store";
|
||||||
|
|
||||||
|
setState(produce(draft => {
|
||||||
|
draft.user.age++;
|
||||||
|
draft.todos.push({ id: 2, text: "New", done: false });
|
||||||
|
draft.todos[0].done = true;
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### reconcile
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { reconcile } from "solid-js/store";
|
||||||
|
|
||||||
|
// Replace with diff (minimal updates)
|
||||||
|
setState("todos", reconcile(newTodosFromAPI));
|
||||||
|
|
||||||
|
// Options
|
||||||
|
reconcile(data, { key: "id", merge: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
### unwrap
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { unwrap } from "solid-js/store";
|
||||||
|
|
||||||
|
const raw = unwrap(store); // Non-reactive plain object
|
||||||
|
```
|
||||||
|
|
||||||
|
### createMutable
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createMutable } from "solid-js/store";
|
||||||
|
|
||||||
|
const state = createMutable({
|
||||||
|
count: 0,
|
||||||
|
user: { name: "John" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Direct mutation (like MobX)
|
||||||
|
state.count++;
|
||||||
|
state.user.name = "Jane";
|
||||||
|
```
|
||||||
|
|
||||||
|
### modifyMutable
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { modifyMutable, reconcile, produce } from "solid-js/store";
|
||||||
|
|
||||||
|
modifyMutable(state, reconcile(newData));
|
||||||
|
modifyMutable(state, produce(s => { s.count++ }));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component APIs
|
||||||
|
|
||||||
|
### children
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { children } from "solid-js";
|
||||||
|
|
||||||
|
const resolved = children(() => props.children);
|
||||||
|
|
||||||
|
// Access
|
||||||
|
resolved(); // JSX.Element | JSX.Element[]
|
||||||
|
resolved.toArray(); // Always array
|
||||||
|
```
|
||||||
|
|
||||||
|
### createContext / useContext
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createContext, useContext } from "solid-js";
|
||||||
|
|
||||||
|
const MyContext = createContext<T>(defaultValue?);
|
||||||
|
|
||||||
|
// Provider
|
||||||
|
<MyContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</MyContext.Provider>
|
||||||
|
|
||||||
|
// Consumer
|
||||||
|
const value = useContext(MyContext);
|
||||||
|
```
|
||||||
|
|
||||||
|
### createUniqueId
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createUniqueId } from "solid-js";
|
||||||
|
|
||||||
|
const id = createUniqueId(); // "0", "1", etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### lazy
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { lazy } from "solid-js";
|
||||||
|
|
||||||
|
const LazyComponent = lazy(() => import("./Component"));
|
||||||
|
|
||||||
|
// Use with Suspense
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<LazyComponent />
|
||||||
|
</Suspense>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
### onMount
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { onMount } from "solid-js";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Runs once after initial render
|
||||||
|
console.log("Mounted");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### onCleanup
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { onCleanup } from "solid-js";
|
||||||
|
|
||||||
|
// In component
|
||||||
|
onCleanup(() => {
|
||||||
|
console.log("Cleaning up");
|
||||||
|
});
|
||||||
|
|
||||||
|
// In effect
|
||||||
|
createEffect(() => {
|
||||||
|
const sub = subscribe();
|
||||||
|
onCleanup(() => sub.unsubscribe());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reactive Utilities
|
||||||
|
|
||||||
|
### batch
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { batch } from "solid-js";
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
setA(1);
|
||||||
|
setB(2);
|
||||||
|
setC(3);
|
||||||
|
// Effects run once after batch
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### untrack
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { untrack } from "solid-js";
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log(a()); // Tracked
|
||||||
|
console.log(untrack(() => b())); // Not tracked
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### on
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { on } from "solid-js";
|
||||||
|
|
||||||
|
// Explicit dependencies
|
||||||
|
createEffect(on(count, (value, prev) => {
|
||||||
|
console.log("Count changed:", prev, "->", value);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Multiple dependencies
|
||||||
|
createEffect(on([a, b], ([a, b], [prevA, prevB]) => {
|
||||||
|
console.log("Changed");
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Defer first run
|
||||||
|
createEffect(on(count, (v) => console.log(v), { defer: true }));
|
||||||
|
```
|
||||||
|
|
||||||
|
### mergeProps
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { mergeProps } from "solid-js";
|
||||||
|
|
||||||
|
const merged = mergeProps(
|
||||||
|
{ size: "medium", color: "blue" }, // Defaults
|
||||||
|
props // Overrides
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### splitProps
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { splitProps } from "solid-js";
|
||||||
|
|
||||||
|
const [local, others] = splitProps(props, ["class", "onClick"]);
|
||||||
|
// local.class, local.onClick
|
||||||
|
// others contains everything else
|
||||||
|
|
||||||
|
const [a, b, rest] = splitProps(props, ["foo"], ["bar"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### createRoot
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createRoot } from "solid-js";
|
||||||
|
|
||||||
|
const dispose = createRoot((dispose) => {
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
// Use signals...
|
||||||
|
return dispose;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Later
|
||||||
|
dispose();
|
||||||
|
```
|
||||||
|
|
||||||
|
### getOwner / runWithOwner
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { getOwner, runWithOwner } from "solid-js";
|
||||||
|
|
||||||
|
const owner = getOwner();
|
||||||
|
|
||||||
|
// Later, in async code
|
||||||
|
runWithOwner(owner, () => {
|
||||||
|
createEffect(() => {
|
||||||
|
// This effect has proper ownership
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### mapArray
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { mapArray } from "solid-js";
|
||||||
|
|
||||||
|
const mapped = mapArray(
|
||||||
|
() => items(),
|
||||||
|
(item, index) => ({ ...item, doubled: item.value * 2 })
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### indexArray
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { indexArray } from "solid-js";
|
||||||
|
|
||||||
|
const mapped = indexArray(
|
||||||
|
() => items(),
|
||||||
|
(item, index) => <div>{index}: {item().name}</div>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### observable
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { observable } from "solid-js";
|
||||||
|
|
||||||
|
const obs = observable(signal);
|
||||||
|
obs.subscribe((value) => console.log(value));
|
||||||
|
```
|
||||||
|
|
||||||
|
### from
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { from } from "solid-js";
|
||||||
|
|
||||||
|
// Convert observable/subscribable to signal
|
||||||
|
const signal = from(rxObservable);
|
||||||
|
const signal = from((set) => {
|
||||||
|
const unsub = subscribe(set);
|
||||||
|
return unsub;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### catchError
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { catchError } from "solid-js";
|
||||||
|
|
||||||
|
catchError(
|
||||||
|
() => riskyOperation(),
|
||||||
|
(err) => console.error("Error:", err)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secondary Primitives
|
||||||
|
|
||||||
|
### createComputed
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createComputed } from "solid-js";
|
||||||
|
|
||||||
|
// Like createEffect but runs during render phase
|
||||||
|
createComputed(() => {
|
||||||
|
setDerived(source() * 2);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### createRenderEffect
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createRenderEffect } from "solid-js";
|
||||||
|
|
||||||
|
// Runs before paint (for DOM measurements)
|
||||||
|
createRenderEffect(() => {
|
||||||
|
const height = element.offsetHeight;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### createDeferred
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createDeferred } from "solid-js";
|
||||||
|
|
||||||
|
// Returns value after idle time
|
||||||
|
const deferred = createDeferred(() => expensiveComputation(), {
|
||||||
|
timeoutMs: 1000
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### createReaction
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createReaction } from "solid-js";
|
||||||
|
|
||||||
|
const track = createReaction(() => {
|
||||||
|
console.log("Something changed");
|
||||||
|
});
|
||||||
|
|
||||||
|
track(() => count()); // Start tracking
|
||||||
|
```
|
||||||
|
|
||||||
|
### createSelector
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createSelector } from "solid-js";
|
||||||
|
|
||||||
|
const isSelected = createSelector(selectedId);
|
||||||
|
|
||||||
|
<For each={items()}>
|
||||||
|
{(item) => (
|
||||||
|
<div class={isSelected(item.id) ? "selected" : ""}>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Show
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Show when={condition()} fallback={<Fallback />}>
|
||||||
|
<Content />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
// With callback (narrowed type)
|
||||||
|
<Show when={user()}>
|
||||||
|
{(user) => <div>{user().name}</div>}
|
||||||
|
</Show>
|
||||||
|
```
|
||||||
|
|
||||||
|
### For
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<For each={items()} fallback={<Empty />}>
|
||||||
|
{(item, index) => <div>{index()}: {item.name}</div>}
|
||||||
|
</For>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Index
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Index each={items()} fallback={<Empty />}>
|
||||||
|
{(item, index) => <input value={item().text} />}
|
||||||
|
</Index>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Switch / Match
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Switch fallback={<Default />}>
|
||||||
|
<Match when={state() === "loading"}>
|
||||||
|
<Loading />
|
||||||
|
</Match>
|
||||||
|
<Match when={state() === "error"}>
|
||||||
|
<Error />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Dynamic } from "solid-js/web";
|
||||||
|
|
||||||
|
<Dynamic component={selected()} prop={value} />
|
||||||
|
<Dynamic component="div" class="dynamic">Content</Dynamic>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Portal
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Portal } from "solid-js/web";
|
||||||
|
|
||||||
|
<Portal mount={document.body}>
|
||||||
|
<Modal />
|
||||||
|
</Portal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ErrorBoundary
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ErrorBoundary fallback={(err, reset) => (
|
||||||
|
<div>
|
||||||
|
<p>Error: {err.message}</p>
|
||||||
|
<button onClick={reset}>Retry</button>
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
|
<Content />
|
||||||
|
</ErrorBoundary>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Suspense
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<AsyncContent />
|
||||||
|
</Suspense>
|
||||||
|
```
|
||||||
|
|
||||||
|
### SuspenseList
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<SuspenseList revealOrder="forwards" tail="collapsed">
|
||||||
|
<Suspense fallback={<Loading />}><Item1 /></Suspense>
|
||||||
|
<Suspense fallback={<Loading />}><Item2 /></Suspense>
|
||||||
|
<Suspense fallback={<Loading />}><Item3 /></Suspense>
|
||||||
|
</SuspenseList>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rendering
|
||||||
|
|
||||||
|
### render
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { render } from "solid-js/web";
|
||||||
|
|
||||||
|
const dispose = render(() => <App />, document.getElementById("root")!);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
dispose();
|
||||||
|
```
|
||||||
|
|
||||||
|
### hydrate
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { hydrate } from "solid-js/web";
|
||||||
|
|
||||||
|
hydrate(() => <App />, document.getElementById("root")!);
|
||||||
|
```
|
||||||
|
|
||||||
|
### renderToString
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { renderToString } from "solid-js/web";
|
||||||
|
|
||||||
|
const html = renderToString(() => <App />);
|
||||||
|
```
|
||||||
|
|
||||||
|
### renderToStringAsync
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { renderToStringAsync } from "solid-js/web";
|
||||||
|
|
||||||
|
const html = await renderToStringAsync(() => <App />);
|
||||||
|
```
|
||||||
|
|
||||||
|
### renderToStream
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { renderToStream } from "solid-js/web";
|
||||||
|
|
||||||
|
const stream = renderToStream(() => <App />);
|
||||||
|
stream.pipe(res);
|
||||||
|
```
|
||||||
|
|
||||||
|
### isServer
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { isServer } from "solid-js/web";
|
||||||
|
|
||||||
|
if (isServer) {
|
||||||
|
// Server-only code
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSX Attributes
|
||||||
|
|
||||||
|
### ref
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
let el: HTMLDivElement;
|
||||||
|
<div ref={el} />
|
||||||
|
<div ref={(e) => console.log(e)} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### classList
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div classList={{ active: isActive(), disabled: isDisabled() }} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### style
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div style={{ color: "red", "font-size": "14px" }} />
|
||||||
|
<div style={`color: ${color()}`} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### on:event (native)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div on:click={handleClick} />
|
||||||
|
<div on:scroll={handleScroll} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### use:directive
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function clickOutside(el: HTMLElement, accessor: () => () => void) {
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (!el.contains(e.target as Node)) accessor()();
|
||||||
|
};
|
||||||
|
document.addEventListener("click", handler);
|
||||||
|
onCleanup(() => document.removeEventListener("click", handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
<div use:clickOutside={() => setOpen(false)} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### prop:property
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<input prop:value={value()} /> // Set as property, not attribute
|
||||||
|
```
|
||||||
|
|
||||||
|
### attr:attribute
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div attr:data-custom={value()} /> // Force attribute
|
||||||
|
```
|
||||||
|
|
||||||
|
### bool:attribute
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<input bool:disabled={isDisabled()} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### @once
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div title={/*@once*/ staticValue} /> // Never updates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import type {
|
||||||
|
Component,
|
||||||
|
ParentComponent,
|
||||||
|
FlowComponent,
|
||||||
|
VoidComponent,
|
||||||
|
JSX,
|
||||||
|
Accessor,
|
||||||
|
Setter,
|
||||||
|
Signal,
|
||||||
|
Resource,
|
||||||
|
Owner
|
||||||
|
} from "solid-js";
|
||||||
|
|
||||||
|
// Component types
|
||||||
|
const MyComponent: Component<Props> = (props) => <div />;
|
||||||
|
const Parent: ParentComponent<Props> = (props) => <div>{props.children}</div>;
|
||||||
|
const Flow: FlowComponent<Props, Item> = (props) => props.children(item);
|
||||||
|
const Void: VoidComponent<Props> = (props) => <input />;
|
||||||
|
|
||||||
|
// Event types
|
||||||
|
type Handler = JSX.EventHandler<HTMLButtonElement, MouseEvent>;
|
||||||
|
type ChangeHandler = JSX.ChangeEventHandler<HTMLInputElement>;
|
||||||
|
```
|
||||||
720
profiles/opencode/skill/solidjs/references/patterns.md
Normal file
720
profiles/opencode/skill/solidjs/references/patterns.md
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
# SolidJS Patterns & Best Practices
|
||||||
|
|
||||||
|
Common patterns, recipes, and best practices for SolidJS development.
|
||||||
|
|
||||||
|
## Component Patterns
|
||||||
|
|
||||||
|
### Controlled vs Uncontrolled Inputs
|
||||||
|
|
||||||
|
**Controlled:**
|
||||||
|
```tsx
|
||||||
|
function ControlledInput() {
|
||||||
|
const [value, setValue] = createSignal("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
value={value()}
|
||||||
|
onInput={(e) => setValue(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Uncontrolled with ref:**
|
||||||
|
```tsx
|
||||||
|
function UncontrolledInput() {
|
||||||
|
let inputRef: HTMLInputElement;
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
console.log(inputRef.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input ref={inputRef!} />
|
||||||
|
<button onClick={handleSubmit}>Submit</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compound Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const Tabs = {
|
||||||
|
Root: (props: ParentProps<{ defaultTab?: string }>) => {
|
||||||
|
const [activeTab, setActiveTab] = createSignal(props.defaultTab ?? "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
||||||
|
<div class="tabs">{props.children}</div>
|
||||||
|
</TabsContext.Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
List: (props: ParentProps) => (
|
||||||
|
<div class="tabs-list" role="tablist">{props.children}</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
Tab: (props: ParentProps<{ value: string }>) => {
|
||||||
|
const ctx = useTabsContext();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
role="tab"
|
||||||
|
aria-selected={ctx.activeTab() === props.value}
|
||||||
|
onClick={() => ctx.setActiveTab(props.value)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
Panel: (props: ParentProps<{ value: string }>) => {
|
||||||
|
const ctx = useTabsContext();
|
||||||
|
return (
|
||||||
|
<Show when={ctx.activeTab() === props.value}>
|
||||||
|
<div role="tabpanel">{props.children}</div>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Tabs.Root defaultTab="first">
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value="first">First</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="second">Second</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Panel value="first">First Content</Tabs.Panel>
|
||||||
|
<Tabs.Panel value="second">Second Content</Tabs.Panel>
|
||||||
|
</Tabs.Root>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Render Props
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function MouseTracker(props: {
|
||||||
|
children: (pos: { x: number; y: number }) => JSX.Element;
|
||||||
|
}) {
|
||||||
|
const [pos, setPos] = createSignal({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
|
||||||
|
window.addEventListener("mousemove", handler);
|
||||||
|
onCleanup(() => window.removeEventListener("mousemove", handler));
|
||||||
|
});
|
||||||
|
|
||||||
|
return <>{props.children(pos())}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<MouseTracker>
|
||||||
|
{(pos) => <div>Mouse: {pos.x}, {pos.y}</div>}
|
||||||
|
</MouseTracker>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Higher-Order Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function withAuth<P extends object>(Component: Component<P>) {
|
||||||
|
return (props: P) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={user()} fallback={<Redirect to="/login" />}>
|
||||||
|
<Component {...props} />
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProtectedDashboard = withAuth(Dashboard);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Polymorphic Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
type PolymorphicProps<E extends keyof JSX.IntrinsicElements> = {
|
||||||
|
as?: E;
|
||||||
|
} & JSX.IntrinsicElements[E];
|
||||||
|
|
||||||
|
function Box<E extends keyof JSX.IntrinsicElements = "div">(
|
||||||
|
props: PolymorphicProps<E>
|
||||||
|
) {
|
||||||
|
const [local, others] = splitProps(props as PolymorphicProps<"div">, ["as"]);
|
||||||
|
|
||||||
|
return <Dynamic component={local.as || "div"} {...others} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Box>Default div</Box>
|
||||||
|
<Box as="section">Section element</Box>
|
||||||
|
<Box as="button" onClick={handleClick}>Button</Box>
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Patterns
|
||||||
|
|
||||||
|
### Derived State with Multiple Sources
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function SearchResults() {
|
||||||
|
const [query, setQuery] = createSignal("");
|
||||||
|
const [filters, setFilters] = createSignal({ category: "all" });
|
||||||
|
|
||||||
|
const results = createMemo(() => {
|
||||||
|
const q = query().toLowerCase();
|
||||||
|
const f = filters();
|
||||||
|
|
||||||
|
return allItems()
|
||||||
|
.filter(item => item.name.toLowerCase().includes(q))
|
||||||
|
.filter(item => f.category === "all" || item.category === f.category);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <For each={results()}>{item => <Item item={item} />}</For>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Machine Pattern
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
type State = "idle" | "loading" | "success" | "error";
|
||||||
|
type Event = { type: "FETCH" } | { type: "SUCCESS"; data: any } | { type: "ERROR"; error: Error };
|
||||||
|
|
||||||
|
function createMachine(initial: State) {
|
||||||
|
const [state, setState] = createSignal<State>(initial);
|
||||||
|
const [data, setData] = createSignal<any>(null);
|
||||||
|
const [error, setError] = createSignal<Error | null>(null);
|
||||||
|
|
||||||
|
const send = (event: Event) => {
|
||||||
|
const current = state();
|
||||||
|
|
||||||
|
switch (current) {
|
||||||
|
case "idle":
|
||||||
|
if (event.type === "FETCH") setState("loading");
|
||||||
|
break;
|
||||||
|
case "loading":
|
||||||
|
if (event.type === "SUCCESS") {
|
||||||
|
setData(event.data);
|
||||||
|
setState("success");
|
||||||
|
} else if (event.type === "ERROR") {
|
||||||
|
setError(event.error);
|
||||||
|
setState("error");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { state, data, error, send };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optimistic Updates
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const [todos, setTodos] = createStore<Todo[]>([]);
|
||||||
|
|
||||||
|
async function deleteTodo(id: string) {
|
||||||
|
const original = [...unwrap(todos)];
|
||||||
|
|
||||||
|
// Optimistic remove
|
||||||
|
setTodos(todos => todos.filter(t => t.id !== id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.deleteTodo(id);
|
||||||
|
} catch {
|
||||||
|
// Rollback on error
|
||||||
|
setTodos(reconcile(original));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Undo/Redo
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function createHistory<T>(initial: T) {
|
||||||
|
const [past, setPast] = createSignal<T[]>([]);
|
||||||
|
const [present, setPresent] = createSignal<T>(initial);
|
||||||
|
const [future, setFuture] = createSignal<T[]>([]);
|
||||||
|
|
||||||
|
const canUndo = () => past().length > 0;
|
||||||
|
const canRedo = () => future().length > 0;
|
||||||
|
|
||||||
|
const set = (value: T | ((prev: T) => T)) => {
|
||||||
|
const newValue = typeof value === "function"
|
||||||
|
? (value as (prev: T) => T)(present())
|
||||||
|
: value;
|
||||||
|
|
||||||
|
setPast(p => [...p, present()]);
|
||||||
|
setPresent(newValue);
|
||||||
|
setFuture([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const undo = () => {
|
||||||
|
if (!canUndo()) return;
|
||||||
|
|
||||||
|
const previous = past()[past().length - 1];
|
||||||
|
setPast(p => p.slice(0, -1));
|
||||||
|
setFuture(f => [present(), ...f]);
|
||||||
|
setPresent(previous);
|
||||||
|
};
|
||||||
|
|
||||||
|
const redo = () => {
|
||||||
|
if (!canRedo()) return;
|
||||||
|
|
||||||
|
const next = future()[0];
|
||||||
|
setPast(p => [...p, present()]);
|
||||||
|
setFuture(f => f.slice(1));
|
||||||
|
setPresent(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { value: present, set, undo, redo, canUndo, canRedo };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Hooks/Primitives
|
||||||
|
|
||||||
|
### useLocalStorage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function createLocalStorage<T>(key: string, initialValue: T) {
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
const initial = stored ? JSON.parse(stored) : initialValue;
|
||||||
|
|
||||||
|
const [value, setValue] = createSignal<T>(initial);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value()));
|
||||||
|
});
|
||||||
|
|
||||||
|
return [value, setValue] as const;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useDebounce
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function createDebounce<T>(source: () => T, delay: number) {
|
||||||
|
const [debounced, setDebounced] = createSignal<T>(source());
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const value = source();
|
||||||
|
const timer = setTimeout(() => setDebounced(() => value), delay);
|
||||||
|
onCleanup(() => clearTimeout(timer));
|
||||||
|
});
|
||||||
|
|
||||||
|
return debounced;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const debouncedQuery = createDebounce(query, 300);
|
||||||
|
```
|
||||||
|
|
||||||
|
### useThrottle
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function createThrottle<T>(source: () => T, delay: number) {
|
||||||
|
const [throttled, setThrottled] = createSignal<T>(source());
|
||||||
|
let lastRun = 0;
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const value = source();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (now - lastRun >= delay) {
|
||||||
|
lastRun = now;
|
||||||
|
setThrottled(() => value);
|
||||||
|
} else {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
lastRun = Date.now();
|
||||||
|
setThrottled(() => value);
|
||||||
|
}, delay - (now - lastRun));
|
||||||
|
onCleanup(() => clearTimeout(timer));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return throttled;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useMediaQuery
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function createMediaQuery(query: string) {
|
||||||
|
const mql = window.matchMedia(query);
|
||||||
|
const [matches, setMatches] = createSignal(mql.matches);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
|
||||||
|
mql.addEventListener("change", handler);
|
||||||
|
onCleanup(() => mql.removeEventListener("change", handler));
|
||||||
|
});
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const isMobile = createMediaQuery("(max-width: 768px)");
|
||||||
|
```
|
||||||
|
|
||||||
|
### useClickOutside
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function createClickOutside(
|
||||||
|
ref: () => HTMLElement | undefined,
|
||||||
|
callback: () => void
|
||||||
|
) {
|
||||||
|
onMount(() => {
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
const el = ref();
|
||||||
|
if (el && !el.contains(e.target as Node)) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("click", handler);
|
||||||
|
onCleanup(() => document.removeEventListener("click", handler));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
let dropdownRef: HTMLDivElement;
|
||||||
|
createClickOutside(() => dropdownRef, () => setOpen(false));
|
||||||
|
```
|
||||||
|
|
||||||
|
### useIntersectionObserver
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function createIntersectionObserver(
|
||||||
|
ref: () => HTMLElement | undefined,
|
||||||
|
options?: IntersectionObserverInit
|
||||||
|
) {
|
||||||
|
const [isIntersecting, setIsIntersecting] = createSignal(false);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const el = ref();
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(([entry]) => {
|
||||||
|
setIsIntersecting(entry.isIntersecting);
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
observer.observe(el);
|
||||||
|
onCleanup(() => observer.disconnect());
|
||||||
|
});
|
||||||
|
|
||||||
|
return isIntersecting;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Patterns
|
||||||
|
|
||||||
|
### Form Validation
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function createForm<T extends Record<string, any>>(initial: T) {
|
||||||
|
const [values, setValues] = createStore<T>(initial);
|
||||||
|
const [errors, setErrors] = createStore<Partial<Record<keyof T, string>>>({});
|
||||||
|
const [touched, setTouched] = createStore<Partial<Record<keyof T, boolean>>>({});
|
||||||
|
|
||||||
|
const handleChange = (field: keyof T) => (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
setValues(field as any, target.value as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (field: keyof T) => () => {
|
||||||
|
setTouched(field as any, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (validators: Partial<Record<keyof T, (v: any) => string | undefined>>) => {
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
for (const [field, validator] of Object.entries(validators)) {
|
||||||
|
if (validator) {
|
||||||
|
const error = validator(values[field as keyof T]);
|
||||||
|
setErrors(field as any, error as any);
|
||||||
|
if (error) isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { values, errors, touched, handleChange, handleBlur, validate, setValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const form = createForm({ email: "", password: "" });
|
||||||
|
|
||||||
|
<input
|
||||||
|
value={form.values.email}
|
||||||
|
onInput={form.handleChange("email")}
|
||||||
|
onBlur={form.handleBlur("email")}
|
||||||
|
/>
|
||||||
|
<Show when={form.touched.email && form.errors.email}>
|
||||||
|
<span class="error">{form.errors.email}</span>
|
||||||
|
</Show>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field Array
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function createFieldArray<T>(initial: T[] = []) {
|
||||||
|
const [fields, setFields] = createStore<T[]>(initial);
|
||||||
|
|
||||||
|
const append = (value: T) => setFields(f => [...f, value]);
|
||||||
|
const remove = (index: number) => setFields(f => f.filter((_, i) => i !== index));
|
||||||
|
const update = (index: number, value: Partial<T>) => setFields(index, v => ({ ...v, ...value }));
|
||||||
|
const move = (from: number, to: number) => {
|
||||||
|
setFields(produce(f => {
|
||||||
|
const [item] = f.splice(from, 1);
|
||||||
|
f.splice(to, 0, item);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return { fields, append, remove, update, move };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Patterns
|
||||||
|
|
||||||
|
### Virtualized List
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function VirtualList<T>(props: {
|
||||||
|
items: T[];
|
||||||
|
itemHeight: number;
|
||||||
|
height: number;
|
||||||
|
renderItem: (item: T, index: number) => JSX.Element;
|
||||||
|
}) {
|
||||||
|
const [scrollTop, setScrollTop] = createSignal(0);
|
||||||
|
|
||||||
|
const startIndex = createMemo(() =>
|
||||||
|
Math.floor(scrollTop() / props.itemHeight)
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleCount = createMemo(() =>
|
||||||
|
Math.ceil(props.height / props.itemHeight) + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
const visibleItems = createMemo(() =>
|
||||||
|
props.items.slice(startIndex(), startIndex() + visibleCount())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ height: `${props.height}px`, overflow: "auto" }}
|
||||||
|
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
|
||||||
|
>
|
||||||
|
<div style={{ height: `${props.items.length * props.itemHeight}px`, position: "relative" }}>
|
||||||
|
<For each={visibleItems()}>
|
||||||
|
{(item, i) => (
|
||||||
|
<div style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: `${(startIndex() + i()) * props.itemHeight}px`,
|
||||||
|
height: `${props.itemHeight}px`
|
||||||
|
}}>
|
||||||
|
{props.renderItem(item, startIndex() + i())}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lazy Loading with Intersection Observer
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function LazyLoad(props: ParentProps<{ placeholder?: JSX.Element }>) {
|
||||||
|
let ref: HTMLDivElement;
|
||||||
|
const [isVisible, setIsVisible] = createSignal(false);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: "100px" }
|
||||||
|
);
|
||||||
|
observer.observe(ref);
|
||||||
|
onCleanup(() => observer.disconnect());
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref!}>
|
||||||
|
<Show when={isVisible()} fallback={props.placeholder}>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memoized Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// For expensive components that shouldn't re-render on parent updates
|
||||||
|
function MemoizedExpensiveList(props: { items: Item[] }) {
|
||||||
|
// Component only re-renders when items actually change
|
||||||
|
return (
|
||||||
|
<For each={props.items}>
|
||||||
|
{(item) => <ExpensiveItem item={item} />}
|
||||||
|
</For>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
### Component Testing
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { render, fireEvent, screen } from "@solidjs/testing-library";
|
||||||
|
|
||||||
|
test("Counter increments", async () => {
|
||||||
|
render(() => <Counter />);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button", { name: /increment/i });
|
||||||
|
expect(screen.getByText("Count: 0")).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(button);
|
||||||
|
expect(screen.getByText("Count: 1")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing with Context
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function renderWithContext(component: () => JSX.Element) {
|
||||||
|
return render(() => (
|
||||||
|
<ThemeProvider>
|
||||||
|
<AuthProvider>
|
||||||
|
{component()}
|
||||||
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Dashboard shows user", () => {
|
||||||
|
renderWithContext(() => <Dashboard />);
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Async Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { render, waitFor, screen } from "@solidjs/testing-library";
|
||||||
|
|
||||||
|
test("Loads user data", async () => {
|
||||||
|
render(() => <UserProfile userId="123" />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling Patterns
|
||||||
|
|
||||||
|
### Global Error Handler
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary
|
||||||
|
fallback={(err, reset) => (
|
||||||
|
<ErrorPage error={err} onRetry={reset} />
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Suspense fallback={<AppLoader />}>
|
||||||
|
<Router>
|
||||||
|
{/* Routes */}
|
||||||
|
</Router>
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Error Handling
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function DataComponent() {
|
||||||
|
const [data] = createResource(fetchData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={data.loading}>
|
||||||
|
<Loading />
|
||||||
|
</Match>
|
||||||
|
<Match when={data.error}>
|
||||||
|
<Error error={data.error} onRetry={() => refetch()} />
|
||||||
|
</Match>
|
||||||
|
<Match when={data()}>
|
||||||
|
{(data) => <Content data={data()} />}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessibility Patterns
|
||||||
|
|
||||||
|
### Focus Management
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Modal(props: ParentProps<{ isOpen: boolean; onClose: () => void }>) {
|
||||||
|
let dialogRef: HTMLDivElement;
|
||||||
|
let previousFocus: HTMLElement | null;
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.isOpen) {
|
||||||
|
previousFocus = document.activeElement as HTMLElement;
|
||||||
|
dialogRef.focus();
|
||||||
|
} else if (previousFocus) {
|
||||||
|
previousFocus.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={props.isOpen}>
|
||||||
|
<Portal>
|
||||||
|
<div
|
||||||
|
ref={dialogRef!}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
tabIndex={-1}
|
||||||
|
onKeyDown={(e) => e.key === "Escape" && props.onClose()}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Live Regions
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Notifications() {
|
||||||
|
const [message, setMessage] = createSignal("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="true"
|
||||||
|
class="sr-only"
|
||||||
|
>
|
||||||
|
{message()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user