算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
frontend-solidjs-patterns【设计与创意】Use this skill when the user says 'SolidJS pattern', 'SolidJS form', 'SolidJS data fetching', 'SolidJS component pattern', 'SolidJS animation'. This skill enforces: createResource for async data with Suspense, controlled forms via signals, component composition via JSX children as functions, and transitions/animation with createTransition and CSS. Re…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Apply production patterns to SolidJS applications: async data with createResource, controlled forms with signals and Zod, component composition patterns, and animation.
Exact user phrases: "SolidJS pattern", "SolidJS form", "SolidJS data fetching", "SolidJS component pattern", "SolidJS animation".
Before activating, verify:
No file output. Produces code patterns for data fetching, forms, composition, and animation.
Code: show resource, form, and composition examples. No imports beyond SolidJS APIs.
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
Code: 15 lines per example.
| Approach | Trade-off | When to Use |
|---|---|---|
| createResource + Suspense | Simple async data, loading states | API data fetching |
| createResource with mutate | Optimistic updates, cache control | Dashboard, live data |
| Controlled form with signals | Full control over validation | Complex forms |
| Children as functions (render props) | Flexible composition | List, Table, Dropdown |
| Slot pattern via props | Explicit API | Card, Layout, Modal |
| createTransition | Pending state for deferred updates | Tab switching, navigation |
Is data needed on page load?
├── Yes -> createResource in component
│ ├── Depends on signal? -> Pass signal as source
│ └── Static -> Pass constant source
└── No -> fetch on interaction (event handler)
How many fields?
├── 1-3 fields -> createSignal per field
├── 4-10 fields -> createStore for the form object
└── 10+ fields or nested -> createStore + field arrays
Does the component need to customize rendering?
├── Yes -> Children as functions or render props
└── No -> Regular props + children
Does the component have optional sections?
├── Yes -> Slot pattern (header, footer, sidebar)
└── No -> Single children slot
function ProductList() {
const [products] = createResource(() => '/api/products', fetchProducts)
return (
<Suspense fallback={<ProductSkeleton />}>
<ErrorBoundary fallback={<div>Failed to load products</div>}>
<For each={products()}>
{(product) => <ProductCard product={product} />}
</For>
</ErrorBoundary>
</Suspense>
)
}
function UserPosts() {
const params = useParams()
const userId = () => params.id
const [user] = createResource(userId, fetchUser)
const [posts] = createResource(user, (u) => u ? fetchPosts(u.id) : [])
return (
<Suspense fallback={<div>Loading...</div>}>
<h1>{user()?.name}'s Posts</h1>
<For each={posts()}>{(post) => <PostCard post={post} />}</For>
</Suspense>
)
}
function LikeButton(props: { postId: string }) {
const [liked, setLiked] = createSignal(false)
const toggleLike = async () => {
const previous = liked()
setLiked(!liked()) // optimistic update
try {
await api.toggleLike(props.postId)
} catch {
setLiked(previous) // rollback
}
}
return <button onClick={toggleLike}>{liked() ? 'Unlike' : 'Like'}</button>
}
function OrderForm() {
const [form, setForm] = createStore({
customer: { name: '', email: '' },
items: [] as { productId: string; quantity: number }[],
shipping: { address: '', city: '', zip: '' },
errors: {} as Record<string, string>,
})
const addItem = () => setForm('items', items => [...items, { productId: '', quantity: 1 }])
const removeItem = (i: number) => setForm('items', items => items.filter((_, idx) => idx !== i))
const updateItem = (i: number, field: string, value: any) => setForm('items', i, field, value)
const total = createMemo(() => form.items.reduce((sum, i) => sum + i.quantity * 10, 0))
const handleSubmit = async (e: Event) => {
e.preventDefault()
const result = orderSchema.safeParse(form)
if (!result.success) {
setForm('errors', result.error.flatten().fieldErrors)
return
}
await api.createOrder(result.data)
}
return (
<form onSubmit={handleSubmit}>
<input value={form.customer.name} onInput={(e) => setForm('customer', 'name', e.currentTarget.value)} />
<For each={form.items}>{(item, i) => (
<div>
<input value={item.productId} onInput={(e) => updateItem(i(), 'productId', e.currentTarget.value)} />
<input type="number" value={item.quantity} onInput={(e) => updateItem(i(), 'quantity', Number(e.currentTarget.value))} />
<button type="button" onClick={() => removeItem(i())}>Remove</button>
</div>
)}</For>
<button type="button" onClick={addItem}>Add Item</button>
<p>Total: {total()}</p>
<button type="submit">Submit Order</button>
</form>
)
}
function DataList<T>(props: {
each: T[]
children: (item: T, index: () => number) => JSX.Element
}) {
return (
<ul>
<For each={props.each}>
{(item, i) => <li>{props.children(item, i)}</li>}
</For>
</ul>
)
}
// Usage
<DataList each={users()}>
{(user, i) => (
<div>
<span>{i() + 1}.</span>
<span>{user.name}</span>
</div>
)}
</DataList>
function Card(props: {
header?: JSX.Element
footer?: JSX.Element
children: JSX.Element
}) {
return (
<div class="card">
{props.header && <div class="card-header">{props.header}</div>}
<div class="card-body">{props.children}</div>
{props.footer && <div class="card-footer">{props.footer}</div>}
</div>
)
}
function Search() {
const [query, setQuery] = createSignal('')
const [debouncedQuery, setDebounced] = createSignal('')
createEffect(() => {
const timer = setTimeout(() => setDebounced(query()), 300)
onCleanup(() => clearTimeout(timer))
})
const [results] = createResource(debouncedQuery, searchApi)
return (
<div>
<input value={query()} onInput={(e) => setQuery(e.currentTarget.value)} placeholder="Search..." />
<Suspense fallback={<div>Searching...</div>}>
<For each={results()}>{(item) => <div>{item.title}</div>}</For>
</Suspense>
</div>
)
}
const [isOpen, setIsOpen] = createSignal(false)
const [selectedTab, setSelectedTab] = createSignal('info')
const [filters, setFilters] = createStore({
search: '',
category: null as string | null,
priceRange: { min: 0, max: 1000 },
sortBy: 'date' as 'date' | 'price' | 'name',
})
setFilters('search', 'widget')
setFilters('priceRange', 'max', 500)
const [data, { mutate, refetch }] = createResource(source, fetcher)
mutate(optimisticData) // update locally without refetch
refetch() // force server re-fetch
const AppContext = createContext<{ theme: () => string; user: () => User | null }>()
// Provider wraps app, consumer uses useContext(AppContext)
SolidJS updates only the DOM nodes that depend on changed signals. A list with 10,000 items updating one item updates exactly one DOM text node — not the list, not the parent.
Memos only recompute when their dependencies change. Use createMemo for expensive computations:
const processed = createMemo(() => heavyComputation(items()))
Use untrack to read a signal without creating a dependency:
createEffect(() => {
console.log(untrack(() => count())) // logs but doesn't subscribe
})
const Dashboard = lazy(() => import('./pages/Dashboard'))
// Dashboard code only loads when navigated to
SolidJS batches updates automatically within event handlers and effects. No manual batching needed.
npm run build
# Output in dist/assets/ — JS chunks, CSS, HTML
npx vite build --analyze
# or add rollup-plugin-visualizer to vite.config.ts
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js",
"target": "ESNext",
"module": "ESNext"
}
}
import { createSignal, createMemo, createEffect } from 'solid-js'
import { describe, it, expect } from 'vitest'
describe('signal logic', () => {
it('computed values update correctly', () => {
const [count, setCount] = createSignal(0)
const doubled = createMemo(() => count() * 2)
expect(doubled()).toBe(0)
setCount(3)
expect(doubled()).toBe(6)
})
})
import { render, screen, fireEvent } from 'solid-testing-library'
import { describe, it, expect } from 'vitest'
import Counter from './Counter'
describe('Counter', () => {
it('renders and increments', () => {
render(() => <Counter />)
const button = screen.getByRole('button')
expect(button).toHaveTextContent('0')
fireEvent.click(button)
expect(button).toHaveTextContent('1')
})
})
import { test, expect } from '@playwright/test'
test('search flow', async ({ page }) => {
await page.goto('/products')
await page.fill('[placeholder="Search..."]', 'widget')
await page.waitForResponse(/api\/search/)
await expect(page.locator('.product-card')).toHaveCount(3)
})
import { server } from '../mocks/server'
import { render, screen } from 'solid-testing-library'
import ProductList from './ProductList'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('shows products from API', async () => {
render(() => <ProductList />)
await screen.findByText('Widget')
expect(screen.getByText('$10')).toBeDefined()
})
// React useState + useEffect + useMemo
const [user, setUser] = useState(null)
const [posts, setPosts] = useState([])
useEffect(() => { fetchUser(id).then(setUser) }, [id])
useEffect(() => { if (user) fetchPosts(user.id).then(setPosts) }, [user])
const displayName = useMemo(() => user?.name.toUpperCase(), [user])
// SolidJS
const [user] = createResource(() => id, fetchUser)
const [posts] = createResource(user, (u) => u ? fetchPosts(u.id) : [])
const displayName = createMemo(() => user()?.name.toUpperCase())
// Vue: data() + computed + watch
export default {
data: () => ({ count: 0 }),
computed: { doubled: () => this.count * 2 },
watch: { count(val) { console.log(val) } },
}
// Solid: createSignal + createMemo + createEffect
const [count, setCount] = createSignal(0)
const doubled = createMemo(() => count() * 2)
createEffect(() => console.log(count()))
// Svelte
let count = $state(0)
let doubled = $derived(count * 2)
$effect(() => console.log(count))
// Solid
const [count, setCount] = createSignal(0)
const doubled = createMemo(() => count() * 2)
createEffect(() => console.log(count()))
// Anti-pattern
createEffect(async () => {
const data = await fetch(`/api/users/${id()}`).then(r => r.json())
setUsers(data)
})
// Correct
const [users] = createResource(() => id(), fetchUsers)
// Anti-pattern: direct mutation on store ref
const state = createStore({ count: 0 }) // returns [state, setState]
state.count = 5 // doesn't trigger update
// Correct
const [state, setState] = createStore({ count: 0 })
setState('count', 5)
// Anti-pattern: effect for derived display logic
createEffect(() => {
document.title = `Count: ${count()}`
})
// If this is the only thing that needs count, it's fine.
// But consider: can you inline the expression?
Components run once — signals at top level only.
Resources can throw. Wrap resource consumers in ErrorBoundary for production robustness.
count() not countcreateSignal for primitives, createStore for objectscreateResource for all async data — never fetch in effectscreateMemo for derived values — never createEffectuntrack() to read without subscribing| Aspect | SolidJS | React | Vue |
|---|---|---|---|
| Reactivity | Fine-grained signals | VDOM diff | Proxy-based |
| Component | Once, not re-run | Every render | Once (setup), re-run (template) |
| Async | createResource | TanStack Query | useAsyncData |
| Forms | Manual signals | React Hook Form | v-model |
| Bundle | ~8KB | ~45KB | ~30KB |
| SSR | SolidStart | Next.js | Nuxt |
npm run dev — HMR dev servernpm run build — production buildnpx solid-devtools — CLI reactivity inspectornpm create solid — project scaffoldingNo artifact produced. Next skill: frontend-universal-testing for unit/integration tests in SolidJS. Carry forward: resource patterns, form validation approach, composition patterns.