A Vite plugin that automatically transforms props destructuring in SolidJS components to use mergeProps and splitProps, preserving reactivity.
In SolidJS, destructuring props directly breaks reactivity because it converts reactive getters into static values:
// ❌ Breaks reactivity
function Component({ name, count }) {
return (
<div>
{name}: {count}
</div>
)
}The correct approach is to use splitProps and mergeProps:
// ✅ Maintains reactivity
import { splitProps } from 'solid-js'
function Component(_props) {
const [{ name, count }] = splitProps(_props, ['name', 'count'])
// ...
}This plugin performs that transformation automatically.
- ✨ Automatically transforms destructured props to
splitProps/mergeProps - 🎯 Handles default values using
mergeProps - 🔄 Preserves spread parameters with
splitProps - 📦 Auto-imports
mergePropsandsplitPropsfrom 'solid-js' - ⚡ Skips non-component functions
bun add -D vite-plugin-solid-undestructureimport solidUndestructure from './plugins/solid-undestructure'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solidUndestructure(), solid() /* other plugins */]
})// Before
function Greeting({ name, age }) {
return (
<div>
Hello {name}, you are {age} years old
</div>
)
}
// After
function Greeting(_props) {
return (
<div>
Hello {_props.name}, you are {_props.age} years old
</div>
)
}// Before
function Button({ label = 'Click me', disabled = false }) {
return <button disabled={disabled}>{label}</button>
}
// After
import { mergeProps } from 'solid-js'
function Button(_props) {
const _merged = mergeProps({ label: 'Click me', disabled: false }, _props)
return <button disabled={_merged.disabled}>{_merged.label}</button>
}// Before
function Card({ title, description, ...props }) {
return (
<div {...props}>
<h2>{title}</h2>
<p>{description}</p>
</div>
)
}
// After
import { splitProps } from 'solid-js'
function Card(_props) {
const [, props] = splitProps(_props, ['title', 'description'])
return (
<div {...props}>
<h2>{_props.title}</h2>
<p>{_props.description}</p>
</div>
)
}// Before
import { For } from 'solid-js'
function TestComponent({
name = 'World',
count = 0,
avatar = '/default.png',
items,
nested: { a, b },
...props
}: {
name?: string
count?: number
avatar?: string
items: string[]
nested: { a: number; b: number }
class?: string
onClick?: () => void
}) {
return (
<div {...props}>
<p>{props.class}</p>
<pre>{a}</pre>
<pre>{b}</pre>
<img src={avatar} alt={name} />
<h1>Hello {name}!</h1>
<p>Count: {count}</p>
<ul>
<For each={items}>{(item) => <li>{item}</li>}</For>
</ul>
</div>
)
}
// After
import { For, mergeProps, splitProps } from 'solid-js'
function TestComponent(_props) {
const _merged = mergeProps({ name: 'World', count: 0, avatar: '/default.png' }, _props)
const [, props] = splitProps(_merged, ['name', 'count', 'avatar', 'items', 'nested'])
return (
<div {...props}>
<p>{props.class}</p>
<pre>{_merged.nested.a}</pre>
<pre>{_merged.nested.b}</pre>
<img src={_merged.avatar} alt={_merged.name} />
<h1>Hello {_merged.name}!</h1>
<p>Count: {_merged.count}</p>
<ul>
<For each={_merged.items}>{(item) => <li>{item}</li>}</For>
</ul>
</div>
)
}- Parse — Uses
@babel/parserto parse TypeScript/JSX files into an AST - Detect — Identifies functions with destructured props that return JSX
- Transform — Rewrites destructuring into
mergeProps/splitPropscalls and replaces all references to destructured identifiers with property accesses on the merged/props object - Import — Adds necessary imports from
solid-jsif not already present - Generate — Outputs transformed code with source maps
- Only transforms functions that return JSX (regular functions are left untouched)
- Requires the first parameter to be an object pattern (destructuring)
- Skips files in
node_modules
bun testSince eslint-plugin-solid's solid/reactivity rule doesn't know about this plugin, it will flag destructured props as non-reactive. The bundled ESLint processor fixes this by teaching the rule about destructured props.
Install eslint-plugin-solid:
bun add -D eslint-plugin-solidAdd the processor to your ESLint config:
// eslint.config.js
import solid from 'eslint-plugin-solid'
import solidUndestructure from 'vite-plugin-solid-undestructure/eslint'
export default [
solidUndestructure.configs['flat/recommended'],
solid.configs['flat/typescript'],
{
rules: {
'solid/no-destructure': 'off'
}
}
]The processor transparently rewrites destructured props into props.X member expressions before the linter runs, so the existing solid/reactivity rule can correctly identify untracked reactive usages. Error messages are adjusted to reference the original destructured name.
// Without the processor, solid/reactivity ignores `size` (not recognized as reactive)
// With the processor, it correctly warns:
function ExampleComponent({ size }: { size: 'sm' | 'lg' }) {
const dimensions =
// ↓ The reactive variable 'size' should be used within JSX, a tracked scope
// (like createEffect), or inside an event handler. [solid/reactivity]
size === 'sm' ? { width: 4, height: 4 } : { width: 8, height: 8 }
// Correct usages that don't cause warnings:
// const dimensions = () => size === 'sm' ? ...
// const dimensions = createMemo(() => size === 'sm' ? ...)
return <img src="..." alt="..." {...dimensions()} />
}