SolidJS pain points and pitfalls (part 2)
In this article I will continue the conversation about non-obvious aspects of SolidJS that those who have been using SolidJS for a long time, or those who are just starting to get acquainted with SolidJS may encounter. My previous article is here.
I don’t want to say that the following features of SolidJS are bad, I want to highlight them, and the reader will judge for himself whether he likes them or not.
Input Events
Let’s consider simple example:
const Component = () => {
const [checked, setChecked] = createSignal(false);
return <input type=”checkbox” checked={checked()} />
}
React developers will expect that the user will not be able to change the state of the input, since the code clearly states that the state of the checkbox should be false
, and we do not update this state anywhere. However, the actual behavior may seem a little surprising: the user will be able to change the state of the checkbox. Why is this happening? Let’s take a look at the MDN documentation to see how the checked
attribute actually works:
checked
: A boolean attribute indicating whether this checkbox is checked by default (when the page loads). It does not indicate whether this checkbox is currently checked: if the checkbox’s state is changed, this content attribute does not reflect the change.
Since Solid doesn’t have a virtual DOM, it can’t “prevent” a checkbox from changing. React implements additional checks and circuits that make it seem like the data controls the display, but in reality, it doesn’t.
The same goes for other types of inputs:
const Component = () => {
const [value, setValue] = createSignal("Static string");
return <input type="text" value={value()} />
}
Here, nothing prevents the user from changing the contents of the input, even though the value is set by the signal and does not change anywhere. However, if you update the signal’s value, the checkbox (and other inputs) is synchronized with that value. Keep this in mind.
Input values
Let’s continue talking about text fields. Frameworks have taught us that if we pass undefined to a text input, the input will be empty. Hold my beer!
// Look ma! There's "undefined" string inside text input
<input type="text" value={undefined} />
Probably someone might think: “Oh, Solid is so bad! They can’t fix the simplest things.” However, the reality is that this is valid behavior. If you do the same with VanillaJS, you will get the same result:
// We'll see "undefined" string inside our input
textInput.value = undefined;
The point is that React spoils (misleads?) developers because it implements additional checks that do not work as the specification dictates.
To avoid trouble, you can do this:
// No "undefined" inside input
<input type="text" value={value() ?? ""} />
Just remember that for inputs: view != f(state)
Eternal stores
There might be a situation when you need to make a store from another store. For instance, we have initial data for rendering item list (like video list), and when we click on some item, we want to download more data (extended data about the video) about the item in order to change some of them (edit the video):
const [videoList, setVideoList] = createStore([/* some items here*/]);
// Somewhere in the component
<For each={videoList}>
{video => <VideoEditModal video={video} />}
</For>
// Video modal
const VideoModal = () => {
const [extendedVideoData, setExtendedVideoData] = createStore(props.video);
// Here might be fetching some data and calling setExtendedVideoData
// You might use createResource for it with createDeepSignal as storage
// see https://www.solidjs.com/docs/latest/api#createresource
return ...
}
First thing you might notice is that we make a store from a Proxy object. Missed it? We’ll talk about it later, but for Solid it’s fine: createStore(props.video)
is legit. The point is that when you change the extendedVideoData
, videoList
object will also be updated! Even if you do the following trick:
const [extendedVideoData, setExtendedVideoData] = createStore(
unwrap(props.video)
);
The stores will be connected even in this case! Why it’s happening? When you make a store Solid will add some hidden props to the object, and even when you unwrap
the proxy, the result value will still have these properties:
const [count, setCount] = createStore({data: 1});
console.log(unwrap(count));
/**
You'll see this in the console
1. {data: 1}
1. data: 1
2. Symbol(solid-proxy): undefined
3. Symbol(store-node): undefined
4. [[Prototype]]: Object
*/
The same is applicable to the parts of the store. I’d say that this can be quite useful sometimes, because you need to keep in sync the information of derived store and original store.
IMPORTANT NOTE HERE: Only extend or modify your dependent stores with data but not remove the data, because it’ll affect the initial store data, and you may end up with confusing situations (but of course, you can do whatever you want, just keep this in mind).
Sometimes you need to separate the stores, so how to achieve that? Well, it’s quite easy with structuredClone
:
const [videoList, setVideoList] = createStore([/* some items here*/]);
// Somewhere else
const [extendedVideoData, setExtendedVideoData] = createStore(
structuredClone(unwrap(props.video))
);
Now these two stores will be completely separated from each other.
Earlier I mentioned that for Solid it’s ok to make store either from a Proxy
or from an object
like:
// video - is a proxy here
// These two will work as expected
const [extendedVideoData, setExtendedVideoData] = createStore(props.video);
// or
const [extendedVideoData, setExtendedVideoData] = createStore(
unwrap(props.video)
);
During the time when your codebase becomes large, I think it’s a good practice to name your stores with $
in order to see what’s really happens in your code, because eventually you may forget the data flow you have. $
may help you to debug your app easier:
const [$videoList, setVideoList] = createStore([/* some items here*/]);
// Somewhere in the component
<For each={$videoList}>
{$video => <VideoEditModal video={$video} />}
</For>
// Video modal
const VideoModal = () => {
const [$extendedVideoData, setExtendedVideoData] = createStore(props.video);
return ...
}
Now it’s clear when you use proxy and when you use plain objects. I also recommend to name with $
every thing which is a proxy (except props of course):
const [$videoList, setVideoList] = createStore([/* some items here*/]);
// signal returns a proxy
// currentVideoId is a signal
const $derivedList = () => $videoList.filter(
$video => $video.id !== currentVideoId()
);
This technique helped me to see the things I missed.
Dangerous (but cool) store setter
Let’s continue on stores. Solid has a nice syntax for stores, which allows to granularly set the field like this:
const [state, setState] = createStore({
counter: 2,
list: [
{ id: 23, title: 'Birds' },
{ id: 27, title: 'Fish' }
]
});
// Nice syntax!
setState('list', 2, 'read', true);
However, this syntax is also dangerous, and I’d recommend avoiding it, because it’s not fully typed. Let’s consider the following example:
function Component() {
const [state, setState] = createStore<{ value: { field: string } | null }>({
value: { field: "Some value" },
});
const setRandomValue = () =>
Math.random() > 0.5
// Set the field value granularly
// No TS error here
? setState("value", "field", "12")
// Oops, danger here!
: setState({ value: null });
return (
<button type="button" onClick={setRandomValue}>
{String(state.value?.field)}
</button>
);
}
After clicking several times you’ll encounter an error. The reason for that is this granular syntax doesn’t consider null
values, so I’d recommend using produce
instead.
Well, that’s it for today. Share in the comments, what tricky things you’ve noticed while working with Solid!