Explore Blogs
Emulating SolidJS reactivity in React without signals

Fine-grained reactivity in React can optimize performance by reducing unnecessary component re-renders. This approach allows you to handle forms, data updates, and other interactive features efficiently, especially in high-performance applications.
Have you ever heard the phrase "Reactivity without re-renders" and wondered what magic powers Solid.js is using behind the scenes? Solid.js boasts ultra-performant, fine-grained reactivity that can update parts of your UI without triggering full component re-renders. But what if I told you that you can achieve a similar pattern in React — without ditching your React stack and without introducing Solid's signal system?
Yes, it's possible. No, you don't need to eject your app. This post will walk you through the idea of fine-grained reactivity and then step by step show how you can emulate Solid-like patterns in plain React using native patterns like refs, subscriptions, and custom hooks.
Whether you're just getting comfortable with React or you've been building apps for years, this guide will help you rethink how state updates and renders work under the hood — and how you can break the rules without breaking your app.
The problem with react renders
React's rendering model is simple on the surface: when state changes, the component re-renders. But this comes with overhead. Even with memoization, useCallback
, and useMemo
, we often find ourselves chasing performance bugs.
Let's see an example.
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<ExpensiveChild />
<button onClick={() => setCount(count + 1)}>{count}</button>
</>
);
}
function ExpensiveChild() {
console.log('Rendering ExpensiveChild');
return <div>I am expensive</div>;
}
Every time you click the button, React re-renders the whole Parent
, and that means ExpensiveChild
renders again — even though it didn't need to. We can use React.memo
, but now we're managing memoization manually and introducing complexity.
This isn't "fine-grained". In Solid.js, this wouldn't happen.
What SolidJS does differently
Solid uses a reactive system inspired by reactive primitives (like signals), not VDOM diffs. Each piece of state is a fine-grained reactive primitive. Changes to one signal only affect subscribers of that signal — no virtual DOM diffing, no component re-renders.
const count = createSignal(0);
createEffect(() => {
console.log(count()); // only re-runs when count changes
});
This reactivity is closer to MobX or Vue's reactive system, where updates are directly tied to the data, not the component lifecycle.
The good news? You can simulate something similar in React — without bringing in a signal system. It takes a bit more boilerplate, but you'll get precise control.
The concept of fine-grained reactivity in react
To replicate this in React, we need to break away from the setState-render cycle and instead use mutable shared state, combined with subscriptions that trigger updates only where needed.
Let's dive into an example.
Creating a reactive store
Let's build a tiny reactive store — just a plain JavaScript object with subscribers.
function createReactiveValue(initial) {
let value = initial;
const subscribers = new Set();
return {
get: () => value,
set: (next) => {
if (value === next) return;
value = next;
subscribers.forEach(fn => fn(value));
},
subscribe: (fn) => {
subscribers.add(fn);
return () => subscribers.delete(fn);
}
};
}
Usage:
const count = createReactiveValue(0);
const unsubscribe = count.subscribe(newVal => {
console.log("Value changed:", newVal);
});
count.set(1); // logs "Value changed: 1"
This is the backbone of reactivity: observable state and subscriptions. Now let's connect this to React.
Connecting to react without causing full renders
Here's where the magic happens. We want to update only the parts of the UI that care about the data — without forcing re-renders of parent components.
import { useSyncExternalStore } from 'react';
function useReactiveValue(reactive) {
return useSyncExternalStore(
reactive.subscribe,
reactive.get
);
}
Now we can use this reactive value inside a component:
function CounterDisplay({ count }) {
const value = useReactiveValue(count);
return <div>Count is {value}</div>;
}
And your parent component stays untouched, even if the count changes.
Updating reactively
Now wire it all together.
const count = createReactiveValue(0);
function App() {
return (
<div>
<CounterDisplay count={count} />
<button onClick={() => count.set(count.get() + 1)}>Increment</button>
</div>
);
}
Clicking the button updates only the CounterDisplay
, not the whole App
. We just achieved fine-grained reactivity.
No state in App
. No re-renders when count
changes. Just efficient updates.
Example: Reactively updating a list item
Let's say you have a list of 100 items, but only one item's name changes. With React state, the whole list may re-render. With fine-grained reactivity, only the changed item updates.
function createReactiveArray(initialArray) {
return initialArray.map(item => ({
id: item.id,
name: createReactiveValue(item.name)
}));
}
Then:
function ListItem({ item }) {
const name = useReactiveValue(item.name);
return <li>{name}</li>;
}
function ItemList({ items }) {
return <ul>
{items.map(item => <ListItem key={item.id} item={item} />)}
</ul>;
}
Only the affected ListItem
re-renders when a name changes.
Benefits of this pattern
- No unnecessary component renders
- Explicit control over what updates
- No dependency on external libraries or signals
- Great for performance-critical apps
Gotchas and tradeoffs
- Manual subscription management adds complexity
- Debugging becomes harder than the React state model
- React DevTools won't show changes in reactive state
- Works well with small shared state, but not ideal for deeply nested trees unless abstracted
You can use this pattern to build custom global stores, reactive form fields, or live UIs like dashboards.
Example: Using reactive form state without setState
Here's the complete example where we define the reactive form state and use it in a form component without using React's useState
or causing full component re-renders.
Define your reactive form state
const formState = {
name: createReactiveValue(""),
email: createReactiveValue(""),
};
Create a generic InputField
component
function InputField({ label, state }) {
const value = useReactiveValue(state);
return (
<div>
<label>{label}</label>
<input
value={value}
onChange={e => state.set(e.target.value)}
style={{ display: 'block', marginBottom: '1rem' }}
/>
</div>
);
}
Build the form component
function ReactiveForm() {
const handleSubmit = (e) => {
e.preventDefault();
alert(`Name: ${formState.name.get()}\nEmail: ${formState.email.get()}`);
};
return (
<form onSubmit={handleSubmit}>
<InputField
label="Name"
state={formState.name}
/>
<InputField
label="Email"
state={formState.email}
/>
<button type="submit">Submit</button>
</form>
);
}
Render in App
function App() {
return (
<div>
<h2>Reactive Form Example</h2>
<ReactiveForm />
</div>
);
}
Key takeaways
- Each input field only listens to its own reactive value.
- No component re-renders when other fields update.
- The form uses
formState.name.get()
andformState.email.get()
to access the current values at submission. - You completely avoid
useState
,useReducer
, or any form library, while maintaining full control over rendering and state.
Advanced reactive form state with validation and error handling
Now that you've seen how to build a basic form with reactive values, let's level up and make this structure practical for real-world apps.
We'll build a form system that mirrors Formik-style logic, but in our fine-grained reactivity style — no component re-renders, no useState
, and no external libraries.
Structure your form state like formik
We'll expand formState
to hold:
values
: reactive valueserrors
: reactive error messagestouched
: reactive flags for blur trackingvalidators
: validation functions for each field
function createFormField(initialValue, validateFn) {
return {
value: createReactiveValue(initialValue),
error: createReactiveValue(""),
touched: createReactiveValue(false),
validate: validateFn,
};
}
Then build the form state:
const formState = {
name: createFormField("", (v) =>
v.trim() === "" ? "Name is required" : ""
),
email: createFormField("", (v) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
? ""
: "Email is invalid"
),
};
Hook up field components with validation
function InputField({ label, field }) {
const value = useReactiveValue(field.value);
const error = useReactiveValue(field.error);
const touched = useReactiveValue(field.touched);
const handleChange = (e) => {
const next = e.target.value;
field.value.set(next);
const validation = field.validate(next);
field.error.set(validation);
};
const handleBlur = () => {
field.touched.set(true);
const validation = field.validate(field.value.get());
field.error.set(validation);
};
return (
<div style={{ marginBottom: "1rem" }}>
<label>{label}</label>
<input
value={value}
onChange={handleChange}
onBlur={handleBlur}
style={{ display: "block", width: "100%" }}
/>
{touched && error && (
<span style={{ color: "red", fontSize: "0.875rem" }}>{error}</span>
)}
</div>
);
}
Handle form submission with validation
function ReactiveForm() {
const getFormValues = () => {
return {
name: formState.name.value.get(),
email: formState.email.value.get(),
};
};
const validateForm = () => {
let isValid = true;
for (const key in formState) {
const field = formState[key];
const error = field.validate(field.value.get());
field.error.set(error);
field.touched.set(true);
if (error) isValid = false;
}
return isValid;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
const values = getFormValues();
alert(`Form submitted:\nName: ${values.name}\nEmail: ${values.email}`);
}
};
return (
<form onSubmit={handleSubmit}>
<InputField
label="Name"
field={formState.name}
/>
<InputField
label="Email"
field={formState.email}
/>
<button type="submit">Submit</button>
</form>
);
}
Optional extras like reset and dirty tracking
You can add initialValue
and track if a field has changed.
Update createFormField
:
function createFormField(initialValue, validateFn) {
const value = createReactiveValue(initialValue);
return {
value,
initialValue,
error: createReactiveValue(""),
touched: createReactiveValue(false),
dirty: createReactiveValue(false),
validate: validateFn,
};
}
Inside handleChange
:
field.dirty.set(next !== field.initialValue);
To reset the form:
function resetForm() {
for (const key in formState) {
const field = formState[key];
field.value.set(field.initialValue);
field.error.set("");
field.touched.set(false);
field.dirty.set(false);
}
}
Then you can add a reset button:
<button type="button" onClick={resetForm}>Reset</button>
When you should actually use fine-grained reactivity in real projects
There's no doubt this pattern is fun to build and helps you understand reactivity at a deeper level. But you're probably wondering…
Is this just a cool trick, or should I actually use it in production?
Let's break it down.
When fine-grained reactivity is a great fit
Ultra-high performance UI with lots of interactivity
If you're building dashboards, real-time apps, or admin interfaces with many inputs or live widgets — where every re-render matters — this approach is gold.
Example: A financial trading UI where dozens of inputs and charts update independently every few milliseconds. You don't want a change in one form field to re-render everything.
Embedded widgets or micro-frontend components
In complex platforms where different parts of the UI are isolated (think: micro frontends or embeddable components inside CMSs), reactive fields that work on their own without React context or prop drilling are a huge win.
Example: A comment box widget embedded inside a legacy CMS, where using useState
or Redux feels too heavy.
Reactive engines that need observability
If you're building an in-house design tool, a spreadsheet engine, or a state-based game engine — you'll love the way each piece of state reacts independently. It behaves more like a dependency graph than a tree of React components.
Example: A custom spreadsheet that updates one cell at a time without recalculating or re-rendering the entire sheet.
Low-rerender requirements in large lists
Forms inside tables, especially dynamic editable grids, benefit hugely from this model.
Example: A user table with 1000 rows, each with inline editable fields. Updating a field shouldn’t re-render 999 others.
When you should probably not use it
Let's be real — this isn't a silver bullet.
Simple CRUD Apps
If you're just building a basic blog, contact form, or CMS backend — you're better off with useState
, react-hook-form
, or Formik. They're easier, more mature, and well-documented.
You don't need laser-sharp reactivity if your app rerenders 5 times per second and nobody notices.
Team projects with junior developers
If you're working in a team, introducing custom reactive primitives means extra learning curve, and it may confuse others unless your team is on the same page.
In these cases, it's better to lean on mainstream tools your coworkers already know.
Heavy server-side rendering or static generation
React Server Components, Next.js, and similar tools are still deeply coupled to the React tree and its rendering model. Fine-grained client reactivity may fight the rendering strategy unless carefully scoped.
It's amazing after hydration, but not always helpful during SSR.
Is fine-grained reactivity production-ready?
- Yes — if you know why you're using it and your app benefits from it.
- No — if it's just for fun or over-engineering a simple problem.
Think of it like this:
Project type | Use this pattern? |
---|---|
Real-time dashboards | ✅ Yes |
Static marketing pages | ❌ No |
Data-heavy grid UIs | ✅ Yes |
Blog post editor | ❌ No |
Embedded component SDK | ✅ Yes |
Simple contact form | ❌ No |
You don't need signals to write reactive code. With a bit of plumbing, React can behave much like Solid.js under the hood. Fine-grained updates aren't about magic — they're about controlling what changes and when.
For beginners, this introduces a new way of thinking about state — outside of the usual useState
. For seasoned developers, this unlocks optimization strategies for real-time UIs and high-performance interfaces.
React gives us the building blocks. We just need to assemble them creatively.