What's the Real Difference Between SolidJS 2.0 and React's Reactivity Model?
I spent three hours debugging why my React component kept re-rendering. I had useMemo, useCallback, and React.memo sprinkled everywhere. Still, one keystroke in an input field triggered a cascade of unnecessary renders. Then I tried SolidJS and realized: the problem wasn’t my code. It was React’s fundamental reactivity model.
The React Problem: Component Re-renders
In React, when state changes, the entire component function re-executes. This is by design:
function Counter() { const [count, setCount] = useState(0); const [name, setName] = useState('React');
// This console.log runs on EVERY state change // Even when only 'name' changes console.log('Component re-rendered');
return ( <div> <h1>Count: {count}</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> );}Type in the input, and React:
- Calls
setNamewith the new value - Re-runs the entire
Counterfunction - Creates a new virtual DOM tree
- Diffs old vs new virtual DOM
- Patches the actual DOM
Every. Single. Keystroke.
For small apps, this is fine. But I worked on a dashboard with 50+ components sharing state. A single dropdown selection triggered re-renders across the entire tree. My performance profiling looked like a Christmas tree of wasted cycles.
The Mental Burden of Optimization
React forces you to think about optimization constantly:
const ExpensiveList = memo(({ items }) => { // Only re-renders if items reference changes return items.map(item => <Item key={item.id} {...item} />);});
function Dashboard() { const [filter, setFilter] = useState(''); const [data, setData] = useState([]);
// Must memoize to prevent ExpensiveList re-renders const filteredItems = useMemo(() => { return data.filter(item => item.name.includes(filter)); }, [data, filter]);
// Must useCallback to maintain reference stability const handleItemClick = useCallback((id) => { console.log('Clicked:', id); }, []);
return ( <ExpensiveList items={filteredItems} onItemClick={handleItemClick} /> );}I found myself spending more time optimizing re-renders than writing actual features. This is the React tax: you pay it in mental overhead and debugging time.
SolidJS’s Solution: Fine-Grained Reactivity
SolidJS takes a fundamentally different approach. Components run once. Signals track dependencies automatically. Only the specific DOM nodes that changed update.
function Counter() { const [count, setCount] = createSignal(0); const [name, setName] = createSignal('SolidJS');
// This runs ONCE on mount, never again console.log('Component rendered once');
return ( <div> {/* Only this h1 updates when count changes */} <h1>Count: {count()}</h1> {/* Only this input updates when name changes */} <input value={name()} onInput={e => setName(e.target.value)} /> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> );}Type in the input, and SolidJS:
- Calls
setNamewith the new value - Updates ONLY the input’s value attribute
- Done. No virtual DOM. No diffing. No re-renders.
The count() expression in the h1 doesn’t even run. SolidJS tracks that the h1 depends on count, but since count didn’t change, it skips that DOM update entirely.
How Signals Actually Work
Signals are the core primitive. They’re like useState but smarter:
function UserProfile() { const [user, setUser] = createSignal({ name: 'Alice', age: 30 }); const [theme, setTheme] = createSignal('dark');
// This creates a derived signal - automatically tracks user const displayName = createMemo(() => { return user().name.toUpperCase(); });
createEffect(() => { // This effect runs when theme() or user() changes // SolidJS tracks dependencies automatically document.body.className = theme(); console.log('User:', user().name); });
return ( <div> {/* Only re-renders when displayName changes */} <h1>{displayName()}</h1> {/* Only re-renders when theme changes */} <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}> Toggle Theme </button> </div> );}No dependency arrays. No useCallback. No memo. SolidJS tracks what each DOM node depends on and updates only what’s necessary.
The Architecture Difference
Here’s the fundamental difference in how updates flow:
React Update Flow:State Change -> Component Re-render -> Virtual DOM Diff -> DOM Patch
SolidJS Update Flow:Signal Change -> Direct DOM UpdateReact’s approach adds layers. Virtual DOM diffing is fast, but it’s not free. In complex apps, those layers compound.
Where React’s Model Breaks Down
I hit the React wall when building a real-time data table with 500 rows. Each row had editable cells. The state management was:
function DataTable() { const [rows, setRows] = useState(/* 500 rows */);
const updateCell = (rowId, column, value) => { setRows(prev => prev.map(row => row.id === rowId ? { ...row, [column]: value } : row )); };
return rows.map(row => ( <Row key={row.id} row={row} onUpdate={updateCell} /> ));}
const Row = memo(({ row, onUpdate }) => { // Even with memo, this re-renders when ANY row changes // because the onUpdate callback reference changes return ( <tr> {Object.entries(row).map(([key, value]) => ( <Cell key={key} value={value} onChange={(v) => onUpdate(row.id, key, v)} /> ))} </tr> );});The onUpdate callback reference changed on every parent render, breaking memo. I needed useCallback, but useCallback needed setRows in its dependency array, which… you get the idea.
SolidJS Handles This Naturally
The same problem in SolidJS:
function DataTable() { const [rows, setRows] = createStore(/* 500 rows */);
return ( <table> <For each={rows}> {(row) => ( <Row row={row} onUpdate={(column, value) => setRows(row.id, column, value) } /> )} </For> </table> );}
function Row({ row, onUpdate }) { // This runs once per row, never re-runs return ( <tr> <For each={Object.keys(row)}> {(key) => ( <Cell value={row[key]} onChange={(v) => onUpdate(key, v)} /> )} </For> </tr> );}SolidJS’s For component is keyed by default. Each row renders once. Cell updates don’t cascade. The onUpdate callback is stable because it references setRows directly, not wrapped in a state setter.
Effect Handling: Manual vs Automatic
This is where the mental models diverge most.
React: Manual Dependency Tracking
function SearchResults() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]);
useEffect(() => { fetchResults(query).then(setResults); }, [query]); // Don't forget this array!
useEffect(() => { document.title = `Searching: ${query}`; }, [query]); // Same dependency, different effect
return <ResultsList results={results} />;}Miss a dependency? Stale data. Include too many? Unnecessary runs. The React team even built an ESLint rule to catch this, because it’s so error-prone.
SolidJS: Automatic Dependency Tracking
function SearchResults() { const [query, setQuery] = createSignal(''); const [results, setResults] = createSignal([]);
createEffect(() => { fetchResults(query()).then(setResults); // SolidJS automatically tracks query() as a dependency });
createEffect(() => { document.title = `Searching: ${query()}`; // Same automatic tracking });
return <ResultsList results={results()} />;}SolidJS tracks which signals each effect reads. When query() changes, both effects run. No manual arrays. No stale closures.
The Trade-offs Nobody Talks About
SolidJS sounds perfect. It’s not. I learned this the hard way.
Ecosystem Gap
React has 10+ years of solutions. SolidJS doesn’t. When I needed:
- Form library: React has React Hook Form, Formik, Final Form. SolidJS has?- UI components: React has Radix, MUI, Chakra. SolidJS has Solid-UI, but smaller ecosystem.- Auth: React has NextAuth, Auth0 SDKs. SolidJS requires custom solutions.- Testing: React Testing Library is mature. SolidJS Testing Library exists but less documented.For a personal project? I could work around this. For a team project at work? The ecosystem gap became a real blocker.
Mental Model Shift
React’s model is: “What should the UI look like given this state?” SolidJS’s model is: “Where does this piece of data flow to the UI?”
This seems subtle, but it changes how you structure code:
// React: Declarative, state-drivenfunction TodoList() { const [todos, setTodos] = useState([]); const [filter, setFilter] = useState('all');
// Derived state computed on each render const filteredTodos = todos.filter(t => filter === 'all' || t.status === filter );
return <List items={filteredTodos} />;}// SolidJS: Reactive, dependency-drivenfunction TodoList() { const [todos, setTodos] = createSignal([]); const [filter, setFilter] = createSignal('all');
// Memoized signal that automatically updates const filteredTodos = createMemo(() => todos().filter(t => filter() === 'all' || t.status === filter()) );
return <List items={filteredTodos()} />;}In React, I think in renders. In SolidJS, I think in data flow. Unlearning React patterns took me weeks.
The Debugging Difference
React debugging: “Why is this component re-rendering?”
- Check props with React DevTools
- Verify memo dependencies
- Use
why-did-you-renderlibrary - Add console.logs in render
SolidJS debugging: “Why is this effect running?”
- Check signal dependencies
- Verify reactive scope
- Use SolidJS DevTools (less mature)
- Usually simpler: fewer moving parts
I spent less time debugging reactivity in SolidJS, but more time learning the mental model.
When SolidJS Wins
After six months with SolidJS, I found clear winners:
1. Complex dashboards with frequent updates
React’s re-render tax compounds. Every dropdown, every filter, every cell edit triggers virtual DOM work. SolidJS handles thousands of reactive updates per second because it skips the intermediate layers.
2. Real-time collaborative apps
When multiple users edit simultaneously, React’s re-renders become a bottleneck. SolidJS’s surgical updates handle concurrent changes gracefully.
3. Performance-critical UIs
Animation-heavy interfaces, data visualizations, interactive editors—these need 60fps. React can do it with careful optimization. SolidJS does it by default.
When React Still Makes Sense
I still use React for:
1. Team projects
If I’m the only one who knows SolidJS, I’m a single point of failure. React’s hiring pool is enormous.
2. Ecosystem-dependent projects
Need a specific library? React probably has three options. SolidJS might have none.
3. Quick prototypes
For a throwaway prototype, React’s familiarity wins. I can scaffold faster in React because I’ve done it hundreds of times.
The Verdict
SolidJS’s fine-grained reactivity eliminates the virtual DOM overhead and mental model complexity of React’s re-render approach. But the ecosystem gap and mental model shift are real costs.
For new projects where performance matters and I control the stack? SolidJS 2.0 is my choice.
For team projects at work? React remains pragmatic—its maturity and talent pool outweigh the reactivity benefits.
The framework war isn’t about which is “better.” It’s about matching the tool to the problem. Now that I understand both models, I choose based on constraints, not hype.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 SolidJS 2.0 Beta Release Discussion
- 👨💻 SolidJS Official Documentation
- 👨💻 React Re-rendering Deep Dive
- 👨💻 Fine-Grained Reactivity Explained
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments