Skip to content

Instantly share code, notes, and snippets.

@FlameWolf
Created August 11, 2026 11:11
Show Gist options
  • Select an option

  • Save FlameWolf/8f25449eafdf0a719f5d2be6fee44bb9 to your computer and use it in GitHub Desktop.

Select an option

Save FlameWolf/8f25449eafdf0a719f5d2be6fee44bb9 to your computer and use it in GitHub Desktop.
Differences in How Vue and Solid Handle Non-Primitive Reactive Values

Differences in How Vue and Solid Handle Non-Primitive Reactive Values

If you ever find yourself porting your front-end code from Vue to Solid, or vice versa, here's one thing that might get you into deep trouble: they handle non-primitive reactive references (created using ref in Vue and createSignal in Solid) in a completely different manner. To demonstrate, here is a Vue component that contains a reactive array:

<script setup lang="ts">
	import { ref, watch } from "vue";

	const separator = ", ";
	const arrayRef = ref([1, 2, 3]); /* Keep an eye on this */
	const updated = ref<Date>();

	function updateArray() {
		const copy = arrayRef.value;
		copy.push(...copy.slice(-3).map(x => x + 3));
	}

	watch(
		arrayRef,
		() => {
			updated.value = new Date();
		},
		{ deep: true }
	);
</script>
<template>
	<h2>Reactive Array Behaviour Demo</h2>
	<p>The value of <code>arrayRef</code> is: {{ arrayRef.join(separator) }}</p>
	<button class="btn btn-primary" @click="updateArray">Update</button>
	<hr/>
	<template v-if="updated">
		<p><code>arrayRef</code> updated at: {{ updated.toISOString() }}</p>
		<hr/>
	</template>
</template>

What happens when you render this component in your app and click the Update button? The original reference (arrayRef) will be updated with each click, and the timestamp at the bottom will change. This happens because arrayRef.value is a reactive proxy of the source array. Therefore, copy points to the same reactive proxy, and calling push() on it is tracked by Vue's reactivity system.

Here is the same code ported to Solid:

import { createEffect, createSignal, on, Show } from "solid-js";
import type { JSX, VoidProps } from "solid-js";

export default function Dummy(props: VoidProps): JSX.Element {
	const separator = ", ";
	const [arraySig, setArraySig] = createSignal([1, 2, 3]); /* Keep an eye on this */
	const [updated, setUpdated] = createSignal<Date>();

	function updateArray() {
		const copy = arraySig();
		copy.push(...copy.slice(-3).map(x => x + 3));
	}

	createEffect(
		on(
			arraySig,
			() => {
				setUpdated(new Date());
			},
			{ defer: true }
		)
	);

	return (
		<>
			<h2>Reactive Array Behaviour Demo</h2>
			<p>The value of <code>arraySig</code> is: {arraySig().join(separator)}</p>
			<button class="btn btn-primary" onClick={updateArray}>Update</button>
			<hr/>
			<Show when={updated()} keyed={true}>
				{lastUpdated => (
					<>
						<p><code>arraySig</code> updated at: {lastUpdated.toISOString()}</p>
						<hr/>
					</>
				)}
			</Show>
		</>
	);
}

In this case, clicking Update mutates the array, but the UI does not update. arraySig returns the array currently stored in the signal to the variable copy inside the function updateArray. However, mutating that array directly does not notify Solid's reactive system because setArraySig was not called. As a result, computations depending on arraySig are not rerun. Any subsequent calls to arraySig after updateArray will give you the updated array while the UI gives you absolutely no indication that the undelying array was updated. What fun!

(The behavior differs slightly with createStore. A signal created with createSignal can hold an ordinary mutable array, so direct mutation changes the stored array but does not notify the reactive system. A store, by contrast, exposes its state through readonly reactive proxies and is designed to be updated only through its store setter. Simply put, createStore won't let you modify copy directly, unlike createSignal.)


This difference impacts another important area besides UI updates: serialization. Vue reactive objects are proxies, and such proxies cannot be directly structured-cloned. Consequently, passing a reactive proxy to APIs that use the structured clone algorithm, including IndexedDB operations, can result in a DataCloneError. (To make things worse, Vue's toRaw() is shallow; it unwraps only the proxy passed directly to it. It does not recursively traverse an object and unwrap every nested reactive proxy. Consequently, if the argument itself is a plain object, toRaw() simply returns that object, even if it contains reactive proxies at deeper levels.)

Summary

Vue makes objects stored in a normal ref() deeply reactive. Reading arrayRef.value returns a reactive proxy, so mutating the array through methods such as push() is tracked.

Solid's createSignal() does not deeply proxy the object stored in it. Reading arraySig() returns the stored array itself. Directly mutating that array changes the object, but it does not notify the signal's subscribers. To trigger reactive updates, the new value must be passed through the signal setter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment