S
SolidJS3mo ago
Stijn

Trying to understand stores in a reactive scope

I've started rewriting my code to use stores instead of lots of signals, and I'm not sure if I understand how they work when passed into a reactive context. This is my store:
export type ExplorerState = {
clips: ClipTreeItem[];
};

export type ClipTreeItem = {
id: string;
name: string;
};

export const [explorer, setExplorer] = createStore<ExplorerState>({
clips: [],
});
export type ExplorerState = {
clips: ClipTreeItem[];
};

export type ClipTreeItem = {
id: string;
name: string;
};

export const [explorer, setExplorer] = createStore<ExplorerState>({
clips: [],
});
I then have a ClipTree component to which I pass all clips:
<ClipTree rows={explorer.clips} />
<ClipTree rows={explorer.clips} />
Lastly, my store gets updated when receiving a callback (from Tauri) like so.
setExplorer(
"clips",
(item: ClipTreeItem) => item.id === id,
"name",
change.name
);
setExplorer(
"clips",
(item: ClipTreeItem) => item.id === id,
"name",
change.name
);
This does not update one of the clips in my clip tree component. I expected that if you update a specific clip with path syntax, any component that has all clips passed into it as a prop in a reactive context ({}) would update. Am I understanding incorrectly how this works? Do I need to write my <ClipTree> in a different way?
7 Replies
peerreynders
peerreynders3mo ago
This works as expected: https://playground.solidjs.com/anonymous/e679ac59-1b39-4d50-a80c-7af5fdcf669b
import { render } from 'solid-js/web';
import { For, createSignal } from 'solid-js';
import { createStore } from 'solid-js/store';

type ClipTreeItem = {
id: string;
name: string;
};

type ExplorerState = {
clips: Array<ClipTreeItem>;
};

function ClipTree(props: { rows: Array<ClipTreeItem> }) {
return (
<ul>
<For each={props.rows}>
{(item, index) => (
<li>
{index()} [{item.id}, {item.name}]
</li>
)}
</For>
</ul>
);
}

function App() {
const [explorer, setExplorer] = createStore<ExplorerState>({
clips: [
{
id: '0',
name: 'zero',
},
{
id: '1',
name: 'one',
},
{
id: '2',
name: 'two',
},
],
});

setTimeout(() => {
const change = {
id: '1',
name: 'ONE',
};

// https://docs.solidjs.com/concepts/stores#path-syntax-flexibility
// - inside 'clips'
// - for any item where predicate is true
// - select 'name' of item
// - update 'name' to change.name
setExplorer(
'clips',
(item: ClipTreeItem) => item.id === change.id,
'name',
change.name
);
}, 2000);

return <ClipTree rows={explorer.clips} />;
}

render(() => <App />, document.getElementById('app')!);
import { render } from 'solid-js/web';
import { For, createSignal } from 'solid-js';
import { createStore } from 'solid-js/store';

type ClipTreeItem = {
id: string;
name: string;
};

type ExplorerState = {
clips: Array<ClipTreeItem>;
};

function ClipTree(props: { rows: Array<ClipTreeItem> }) {
return (
<ul>
<For each={props.rows}>
{(item, index) => (
<li>
{index()} [{item.id}, {item.name}]
</li>
)}
</For>
</ul>
);
}

function App() {
const [explorer, setExplorer] = createStore<ExplorerState>({
clips: [
{
id: '0',
name: 'zero',
},
{
id: '1',
name: 'one',
},
{
id: '2',
name: 'two',
},
],
});

setTimeout(() => {
const change = {
id: '1',
name: 'ONE',
};

// https://docs.solidjs.com/concepts/stores#path-syntax-flexibility
// - inside 'clips'
// - for any item where predicate is true
// - select 'name' of item
// - update 'name' to change.name
setExplorer(
'clips',
(item: ClipTreeItem) => item.id === change.id,
'name',
change.name
);
}, 2000);

return <ClipTree rows={explorer.clips} />;
}

render(() => <App />, document.getElementById('app')!);
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
peerreynders
peerreynders3mo ago
This does not update one of the clips in my clip tree component.
That specific setter update seems to target a specific clip with a matching id and updates its name. So I don't know what “does not update one of the clips” means in this context; that would only make sense if there are multiple clips with the same id in the clips array.
any component that has all clips passed into it as a prop in a reactive context ({}) would update.
You are not disclosing what you are actually doing inside of the ClipTreecomponent. Given just
<ClipTree rows={explorer.clips}>
<ClipTree rows={explorer.clips}>
all we know is that explorer.clips is passed into the component. What your component reacts to depends entirely on what it accesses. In the context of the example that I gave
<li>{index()} [{item.id}, {item.name}]</li>
<li>{index()} [{item.id}, {item.name}]</li>
each item's name is accessed, so the component will react when the name of that one item is changed.
Stijn
StijnOP3mo ago
Hey @peerreynders, thank you for taking the time to answer. I'm relatively new to solid, so it's much appreciated 🙂
So I don't know what “does not update one of the clips” means in this context; that would only make sense if there are multiple clips with the same id in the clips array.
Yes, I meant that I have a list of multiple clips (each with a unique ID), but I don't see any changes when mutating the store. What I'm doing internally is pretty much like what you show in the playground, and there it clearly works. This leaves me wondering where the difference lies, so I'm diving a bit deeper.
peerreynders
peerreynders3mo ago
One thing to look for is that you don't break reactivity, e.g.:
function ClipTree(props: { rows: Array<ClipTreeItem> }) {
// Don't do this; destructuring breaks reactivity
const { rows } = props;
return (
<ul>
<For each={rows}>
{(item, index) => (
<li>
{index()} [{item.id}, {item.name}]
</li>
)}
</For>
</ul>
);
}
function ClipTree(props: { rows: Array<ClipTreeItem> }) {
// Don't do this; destructuring breaks reactivity
const { rows } = props;
return (
<ul>
<For each={rows}>
{(item, index) => (
<li>
{index()} [{item.id}, {item.name}]
</li>
)}
</For>
</ul>
);
}
Stijn
StijnOP3mo ago
Ah, no, I don't destructure, but I didn't know that, so thank you 🙂 Here's the (stripped down) version of the component
interface ClipTreeProps {
rows: ClipTreeItem[];
}

export type ClipTreeItem = {
id: string;
name: string;
};

const ClipTree = (props: ClipTreeProps) => {
return (
<div class="clip-tree">
<For each={props.rows}>
{(row) => (
<ClipTreeRow
text={row.name}
/>
)}
</For>
</div>
);
}

interface ClipTreeRowProps {
text: string;
}

const ClipTreeRow = (props: ClipTreeRowProps) => {
return (
<LabelIconRow text={props.text} editable={true} icon={props.root && <AiFillStar class="root-star" />} onRename={props.onRename} />
);
}
interface ClipTreeProps {
rows: ClipTreeItem[];
}

export type ClipTreeItem = {
id: string;
name: string;
};

const ClipTree = (props: ClipTreeProps) => {
return (
<div class="clip-tree">
<For each={props.rows}>
{(row) => (
<ClipTreeRow
text={row.name}
/>
)}
</For>
</div>
);
}

interface ClipTreeRowProps {
text: string;
}

const ClipTreeRow = (props: ClipTreeRowProps) => {
return (
<LabelIconRow text={props.text} editable={true} icon={props.root && <AiFillStar class="root-star" />} onRename={props.onRename} />
);
}
LabelIconRow eventually puts the text into a <span></span> Really gotta get to bed now, but I will investigate further in the morning. At least it's good knowing that it's supposed to work, so I know to search for where the reactivity breaks down
peerreynders
peerreynders3mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
Stijn
StijnOP3mo ago
Hmmm, if I do this it does work, so either I'm doing providing the path incorrectly, or that triggers some more fine-grained reactivity I'm not handling correctly in the component.
const clips = explorer.clips.map((clip) => {
if (clip.id === id) {
return { ...clip, name: change.name };
}
return clip;
});
setExplorer({ clips });
const clips = explorer.clips.map((clip) => {
if (clip.id === id) {
return { ...clip, name: change.name };
}
return clip;
});
setExplorer({ clips });
vs
setExplorer(
"clips",
(clip: ClipTreeItem) => clip.id == id,
"name",
change.name
);
setExplorer(
"clips",
(clip: ClipTreeItem) => clip.id == id,
"name",
change.name
);
Ah, I found the culprit. It's that LabelIconRow, which does something internally that breaks the reactivity. Not sure what yet, I'll figure it out, but if I change its internals to literally {props.text} everything works as I expected Thank you very much for your help, @peerreynders. You pointed me in the right direction and I learned something along the way (about destructuring for example) 👍

Did you find this page helpful?