S
SolidJS7mo ago
halu

SOLVED: Trigger Side Effects on a Callbacks Dependency Updates

I'm connecting a live feed to solid and currently trying to apply a filter. I filter the feed as it comes in, but if the filter function changes or a reactive value under its scope updates, I need to re-filter from scratch. I'm not sure how to do this...
// 3rd Party Cache
let feed_cache = new Map<string, FeedItem>()

let [filter_user, set_filter_user] = createSignal<User|undefined>()
let [filter_fn, replace_filter] = createSignal((item: FeedItem) => {
// if filter user set, show just their messages. else, show everyones.
return filter_user() ? item.sender === filter_user().id : true
})

function live_filter(filter) {
const filter_feed = new ReactiveMap<string, InstanceType<T>>()

// 3rd Party Fn populates cache
Feed.onInsert((key:string, item: FeedItem)=>{
if (filter()(item)) {
filter_feed.set(key, item)
}
})

// pseudo-code
watchDeep(filter, ()=>{
// if someone runs "set_filter_user" or "replace_filter",
// this should run:
filter_feed.clear()
for (let [key, value] of feed_cache) {
if(filter()(value)) filter_feed.set(key, value)
}
})

return filter_feed
}

let filtered_feed = live_filter(filter_fn)
// 3rd Party Cache
let feed_cache = new Map<string, FeedItem>()

let [filter_user, set_filter_user] = createSignal<User|undefined>()
let [filter_fn, replace_filter] = createSignal((item: FeedItem) => {
// if filter user set, show just their messages. else, show everyones.
return filter_user() ? item.sender === filter_user().id : true
})

function live_filter(filter) {
const filter_feed = new ReactiveMap<string, InstanceType<T>>()

// 3rd Party Fn populates cache
Feed.onInsert((key:string, item: FeedItem)=>{
if (filter()(item)) {
filter_feed.set(key, item)
}
})

// pseudo-code
watchDeep(filter, ()=>{
// if someone runs "set_filter_user" or "replace_filter",
// this should run:
filter_feed.clear()
for (let [key, value] of feed_cache) {
if(filter()(value)) filter_feed.set(key, value)
}
})

return filter_feed
}

let filtered_feed = live_filter(filter_fn)
I'd prefer to not filter over the whole map for every insert, but idk how to detect those deep changes like "set_filter_user".
114 Replies
halu
haluOP7mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
halu
haluOP7mo ago
you can just jump to live_filter on line 200
halu
haluOP7mo ago
I can do this https://playground.solidjs.com/anonymous/f6e10d5e-869d-4432-88ee-d131d122a17a but now i'm just manually tracking deps
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
halu
haluOP7mo ago
worst case scenario i can dumb it down and memo all entries: https://playground.solidjs.com/anonymous/8f40836c-10d7-46d1-aaf9-ced2617ff7c6 but i don't really like it tho, requires post processing to filter and mem-foot print is larger than straight filtering.
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
peerreynders
peerreynders7mo ago
Is this about the ReactiveMap or are you actually only interested in the final UI. Because if this is only about the UI this seems overly complex. I'd dumb it down a lot. - For the UI just go with a store which holds an array of the entries that you want to display. - Create two operations against the store: - one that simply adds a single entry to the end of the array. - another that replaces everything inside the store. Given the nature of your use case it's probably not worth bothering with using reconcile as in most cases filtering will only rarely have any common items between before and after filtering. - Manage the feed history and the filtering at the edge of the reactive graph. There simply is no need for the entire history to live inside the reactive graph if you are not going to put it on the UI. - When a new entry arrives AND the filter setting will let it pass use the add single entry op to add it to the store (otherwise just add it to the history but don't push it into the reactive graph; the history has all entries, it is the source of truth; the store on the other hand only has those entries which need to be shown) - When the filter setting changes filter the cached history accordingly and use the replace everything op to replace the contents of the store. The only reactive element is the store (driving the DOM updates) everything else is just mundane code outside of the reactive graph. The only thing you are actually recreating are the arrays which hold the items. The items themselves are shared between the history and store so there is no duplication.
halu
haluOP7mo ago
I’m not worried about the UI so much as creating a very performant primitive. The feed code here is just a minimal example to demonstrate the trouble of detecting reactive changes within a callback (ie when we need to re-filter). The premise of all this is to create a sort of “virtual table” so people are not doing redundant, full-scan filters across their whole application. In reality, i imagine there to be multiple feeds and filters with incredibly volatile data (which is why i initially opted for Map) and this could all be in composables to drive heavier computations like physics engines and 3D-rendering. It’s possible the complexity i’ve created is less performant than a simpler solution, so I’ll have to benchmark it. i’ll try a store demo, add updates + deletions for all examples to simulate volatility, and figure out how to benchmark this ig… HEAD-CANNON; a callback in something like createMemo works as it can be immediately invoked to start tracking. My filter doesn’t work as it cant be invoked without an input. thus its hard to capture updates in an effect. in my last example, i bypassed this by tracking effects for each input on the map. however… i wonder if i can simply require an args.length===0 run condition to start tracking before we ever receive a feed item to filter? @peerreynders sorry to keep bothering you, but is this a possible approach? or does it run the risk of not encountering reactive variables due to some conditions and fail to track more generally? (I’m a little fuzzy on the details of tracking) maybe i can turn the onInsert value into a signal. then its pre-defined and i can ensure its tracked appropriately on init
glassy
glassy7mo ago
Hi! I'm kind of implementing something similar so I found reading your code interesting. I think the point peerreynders said to
Manage the feed history and the filtering at the edge of the reactive graph. There simply is no need for the entire history to live inside the reactive graph if you are not going to put it on the UI.
and some of the other points, seems to be what you are doing already As you have the feed_cache as the entire history and it isn't reactive but is just used to recreate the live_filter return value I think I understand your point about your 'filter not working' because it can't be invoked without an input as you are dong on(filter, .... The problem is the effect doesn't get re-triggered when the set_filter_user is called because that effect is listening to just changes to the filter itself.
// DOESNT WORK:
createEffect(
on(filter, () => { // <<<< the problem is this effect listens to just changes to the filter itself
// run if "set_filter_user" or "replace_filter"
console.log("refiltered!");
filter_feed.clear();
for (let [key, value] of feed_cache) {
if (filter()(value)) filter_feed.set(key, value);
}
}),
);
// DOESNT WORK:
createEffect(
on(filter, () => { // <<<< the problem is this effect listens to just changes to the filter itself
// run if "set_filter_user" or "replace_filter"
console.log("refiltered!");
filter_feed.clear();
for (let [key, value] of feed_cache) {
if (filter()(value)) filter_feed.set(key, value);
}
}),
);
Now ... I'm trying to figure out a solution. My first thought was to just remove the on(filter, part and just createEffect on the inner function ... but that doesn't seem to work or I'm doing something wrong.
glassy
glassy7mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
glassy
glassy7mo ago
Not sure if it is what you are aiming for. The reason why this wasn't working (and also the old code partly I guess) was that live_feed was (and is) creating a new ReactiveMap everytime it was being run. And the filtered variable that was receiving the output from live_feed wasn't reactive. And thus the <MapIter> which was listening to the previous instance of filtered wouldn't get any reactive update. So actually maybe another option is instead of creating a new ReactiveMap everytime, you could keep using the same instance and make use of it's reactivity. I've been trying to simplify the code.
halu
haluOP7mo ago
map was being weird so i switched it to store, https://playground.solidjs.com/anonymous/d640cc13-e62a-4a02-aab4-7211fff53cb1 i believe this approach causes aggressive re-filtering on inserts (cuz its a derived signal)
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
glassy
glassy7mo ago
Yeah I think the map was being weird too. I also changed to store and doing Object.keys. But in my version reactivity is lost, trying to figure out what happened.
halu
haluOP7mo ago
i don't think the <MapIter> expects the whole reactive object to be swapped on updates. thats just speculation tho
glassy
glassy7mo ago
Do you know how to completely clear a store? (Lol I'm still actually a noob)
halu
haluOP7mo ago
my guess would be setSomeStore({})
glassy
glassy7mo ago
Yeah, that was what I was doing at first. After experimentation, turns out since the set function merges whatever you give it with the existing store, then setSomeStore({}) actually does nothing lol.
halu
haluOP7mo ago
maybe setSomeStore(()=>{}) ?
halu
haluOP7mo ago
yea i bookmarked that last night too 😂
glassy
glassy7mo ago
So actually the way to clear a store (at least that I've found) is: setSomeStore(reconcile({})); Yeah, a guide like that one should actually be in the docs. (Right now I feel like the docs are written for geniuses lol)
glassy
glassy7mo ago
Alright, this is a cleaner version: https://playground.solidjs.com/anonymous/e90d34b0-6f41-48f4-8783-3e7a51e0836f I might clean it up a bit more, I think it is almost done.
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
halu
haluOP7mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
glassy
glassy7mo ago
OK here here is the cleaned up version with some explanation comments https://playground.solidjs.com/anonymous/39ff47e1-b544-4853-b41d-2eb022d00720
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
glassy
glassy7mo ago
ah, I see you want to have a method that takes the filter_fn. Otherwise yeah, my comments in there explain my adjustments. This was a great excercise for me. Now back to coding my own app, which using this experience should make it smoother 😄 But maybe @peerreynders will look at our code and say it is all the wrong way (lol)
glassy
glassy7mo ago
OK, here I made just a small tweak and put the effects back in live_filter https://playground.solidjs.com/anonymous/83435aa9-add6-404b-a57c-335462eb7825 So live_filter is kind of re-usable now
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
halu
haluOP7mo ago
i dont think onInsert actually needs to be wrapped. am i mistaken? i think the secret sauce of this whole thread is the dummy input to init tracking
halu
haluOP7mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
halu
haluOP7mo ago
Thanks a ton @glassy ! i'll prob chuck an init value on the end of the function just so you don't have to worry about down stream typing (*updated)
glassy
glassy7mo ago
I think the reason it needs to be wrapped is for when the filter itself gets replaced. (replace_filter is called.) I am trying to prove it but now I don't know how to call replace_filter with a new one lol. OK yeah, actually onInsert doesn't need to be in a createEffect because it is already calling filter()
glassy
glassy7mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
glassy
glassy7mo ago
to change the filter I found out I need to do replace_filter(() => newFilter); instead of just replace_filter(newFilter); the () => newFilter is needed because if you send a function to setSignal it expects the function to return the new value to be set.
halu
haluOP7mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
halu
haluOP7mo ago
i just replace immediately lemme play around with urs
glassy
glassy7mo ago
aha looking at your code again, I realized that actiually returning the filter_feed from inside the live_feed function doesn't mean it is created more than once cause actually live_feed is called only once. I misread the code at first. So your code makes it even more reusable.
halu
haluOP7mo ago
changed to filter by sender as content updates at regular intervals, so if you dont start on the right milisecond you wont see anything https://playground.solidjs.com/anonymous/c82b0e5a-fec1-4064-b14d-020f54a1f769
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
halu
haluOP7mo ago
this was all a great help, this was at the edge of my understanding of solid thx again for ur help!
glassy
glassy7mo ago
Yes, I learned a lot too.
halu
haluOP7mo ago
i wonder if theres a way to use the first insert to register the effect. then we can eliminate the need first the dummy
glassy
glassy7mo ago
I think maybe put the onInsert and the refilter code inside the same createEffect might do it
halu
haluOP7mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
glassy
glassy7mo ago
I wonder if it would track properly if the filter changes while the feed is still empty (ie. the new filter might depend on some signals that don't get triggered) hmm I guess the register in the oninsert would handle it I think the dummy version is simpler lol
halu
haluOP7mo ago
i havent tested this, so there could be mistakes. but, just a thought
// generic
function live_filter<T>(
filter: Accessor<(item: T) => boolean>,
onInsert: (cb:()=>void)=>void,
source_store: ()=>unknown
options: () => {
let store = new ReactiveMap<string, T>()
return {
store: ()=>store,
filter_set: store.set,
filter_clear: store.clear
}
}
) {
const { clear, set, filter_store } = options()
let init = false;

let RegisterEffect = (first_input: T) => {
let first_filter: boolean;
createEffect(() => {
if (!init) {
// dummy_input to init tracking
first_filter = filter()(first_input);
} else {
console.log("refiltered!");
filter_clear();
for (let [key, item] of source_store()) {
if (filter()(item)) filter_set(key, item);
}
}
});
init = true;
// I believe this is guaranteed as first createEffect call is sync.
return first_filter!;
};

// 3rd Party Fn populates cache
onInsert((key: string, item: T) => {
let is_filtered = init ? filter()(item) : RegisterEffect(item);
if (is_filtered) {
filter_set(key, item);
}
});

return filter_feed;
}

// default with map
live_filter(my_filter, onInsert, feed_cache)

// customized for solid store
live_filter(
my_filter,
onInsert,
feed_cache,
options: () => {
let [store, setStore] = createStore({})
return {
store: ()=>store,
filter_set: setStore,
filter_clear: ()=>setStore(reconcile({}))
}
}
)
// generic
function live_filter<T>(
filter: Accessor<(item: T) => boolean>,
onInsert: (cb:()=>void)=>void,
source_store: ()=>unknown
options: () => {
let store = new ReactiveMap<string, T>()
return {
store: ()=>store,
filter_set: store.set,
filter_clear: store.clear
}
}
) {
const { clear, set, filter_store } = options()
let init = false;

let RegisterEffect = (first_input: T) => {
let first_filter: boolean;
createEffect(() => {
if (!init) {
// dummy_input to init tracking
first_filter = filter()(first_input);
} else {
console.log("refiltered!");
filter_clear();
for (let [key, item] of source_store()) {
if (filter()(item)) filter_set(key, item);
}
}
});
init = true;
// I believe this is guaranteed as first createEffect call is sync.
return first_filter!;
};

// 3rd Party Fn populates cache
onInsert((key: string, item: T) => {
let is_filtered = init ? filter()(item) : RegisterEffect(item);
if (is_filtered) {
filter_set(key, item);
}
});

return filter_feed;
}

// default with map
live_filter(my_filter, onInsert, feed_cache)

// customized for solid store
live_filter(
my_filter,
onInsert,
feed_cache,
options: () => {
let [store, setStore] = createStore({})
return {
store: ()=>store,
filter_set: setStore,
filter_clear: ()=>setStore(reconcile({}))
}
}
)
glassy
glassy7mo ago
nice ... making it further generic
halu
haluOP7mo ago
i think this is correct as the effect hasn't been registered yet, nothing would happen
glassy
glassy7mo ago
I feel a lot more confident using stores now. I used to (like just yesterday) think createMutable was so much more easy to use and didn't understand the value of createStore. After reading that article above and playing with it today, it turns out actually pretty nice to use. And the fact that you can keep the setStore method private actually does allow unidirectional data flow. I also checked some of the most popular state management tools for other libraries, and actually stores + Solid is the simplest to use.
halu
haluOP7mo ago
i love mutable for recursion. when making a deep update in createStore via recursion, you have to append and pass keys to keep track of where you are. with createMutable, you can just traverse and directly mutate, its a good deal cleaner. there are also some times where you need to pass setter and getter together, its can be cleaner to just use a mutable in those cases too. those edge cases aside, stores are awesome. the versatility of the setter is crazy.
glassy
glassy7mo ago
I just wrote this last night and am starting to try to use it
// a base class for exposing store data while controlling all changes to be made only inside this class
export class StoreClass<T extends object> {
public readonly store: T;
protected setStore: ReturnType<typeof createStore<T>>[1];

constructor(initialState: T) {
const [store, setStore] = createStore<T>(initialState)
this.store = store;
this.setStore = setStore;
}
}

// then can define store classes

// this is the type for the store
interface MySomethingStore {
some: string
data: string
}

export class MySomethingState extends StoreClass<MySomethingStore> {
constructor() {
super({some: "initial", data: "data"})
}

operationThatChangesTheStore() {
this.setStore({some: "thing else"})
}
}

// then in components

// a reference to the store (possibly a central store)
const mySomethingState: MySomethingState = ...
const store = mySomethingState.store

// then in the jsx
<div>{store.some} {store.data}</div>
// a base class for exposing store data while controlling all changes to be made only inside this class
export class StoreClass<T extends object> {
public readonly store: T;
protected setStore: ReturnType<typeof createStore<T>>[1];

constructor(initialState: T) {
const [store, setStore] = createStore<T>(initialState)
this.store = store;
this.setStore = setStore;
}
}

// then can define store classes

// this is the type for the store
interface MySomethingStore {
some: string
data: string
}

export class MySomethingState extends StoreClass<MySomethingStore> {
constructor() {
super({some: "initial", data: "data"})
}

operationThatChangesTheStore() {
this.setStore({some: "thing else"})
}
}

// then in components

// a reference to the store (possibly a central store)
const mySomethingState: MySomethingState = ...
const store = mySomethingState.store

// then in the jsx
<div>{store.some} {store.data}</div>
Anyways it is a pattern I'm going to start using as it allows components to easily access the store for rendering, while keeping all changes to the store controlled inside the StoreClass (due to the fact that setStore is a protected member). Hmm, yeah recursion. Haven't gotten to that yet. I wonder if produce can be used with that.
halu
haluOP7mo ago
neat! encapsulating in classes can do a lot also be sure to check out the flux primitive and native getters: - https://primitives.solidjs.community/package/flux-store - https://www.solidjs.com/docs/latest/api#getters
Solid Primitives
A library of high-quality primitives that extend SolidJS reactivity
SolidJS
Solid is a purely reactive library. It was designed from the ground up with a reactive core. It's influenced by reactive principles developed by previous libraries.
halu
haluOP7mo ago
interesting i haven’t thought about that approach
glassy
glassy7mo ago
Thanks, will check it out Yeah, produce makes using stores almost like createMutable. Also the fact you can just setState({some: "partialState"}) and it will merge instead of replace means I figured out I will barely have to use setting with paths thing. If it is more than 1 level deep, just use produce. When I first read about stores, I thought setStore was crazy at first cause instead of normal javascript you have to use these weird paths. But produce makes it like normal javascript again. Ah didn't know about getters in stores, could be useful
halu
haluOP7mo ago
good to know thx
glassy
glassy7mo ago
Ah flux .. looks familar like I used to write code like that in another life lol. I think I prefer just using TypeScript classes like my StoreClass pattern.
halu
haluOP7mo ago
i really enjoyed the DX of pinia from vue https://pinia.vuejs.org/ the flux primitive is pretty similar. i know there are a bunch of agnostic state management libs that do similar stuff
Pinia 🍍
Intuitive, type safe, light and flexible Store for Vue
glassy
glassy7mo ago
Yeah, I used to use vue too. Vue 2 with class components. When Vue 3 and react hooks became the craze, I started to learn Solid instead
halu
haluOP7mo ago
Note to self, consider the situation that the cache is already populated. this may be the case for an initial get request that is then kept up to date by websockets
peerreynders
peerreynders7mo ago
I was only focused on the UI, not creating some new reactive mechanism. My KISS solution doesn't even need a store. https://playground.solidjs.com/anonymous/63664f46-20dc-42f5-8045-58029de0baa4
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
peerreynders
peerreynders7mo ago
That's what I created this thing for.
GitHub
solid-start-sse-chat/src/components/history-context/message-history...
Basic Chat demonstration with server-sent events (SSE) - peerreynders/solid-start-sse-chat
halu
haluOP7mo ago
This is a very interesting approach sees our reactive approach: "look what they need to mimic a fraction of my power" - peerreynders, probably 😆
peerreynders
peerreynders7mo ago
Nobody's responded to this yet. Also I have a small brain, so I have to accommodate for it in my code.
peerreynders
peerreynders7mo ago
Fits in My Head If you like that I've listed more here.
GOTO Conferences
YouTube
Kicking the Complexity Habit • Dan North • GOTO 2014
This presentation was recorded at GOTO Chicago 2014 http://gotochgo.com Dan North - Agile Troublemaker, Developer, Originator of BDD @daniel-terhorst-north ABSTRACT Without rigorous care and attention software quickly becomes messy and unmanageable. Even with the best intentions entropy and complexity are a fact of life in growing application...
halu
haluOP7mo ago
technology-hospice.md 😂
peerreynders
peerreynders7mo ago
The key idea being that replace-ability is much more powerful than re-usability.
Ditto
Ditto7mo ago
Is there a reconcile function for mutables? Can you prevent re-renders when replacing a deep mutable object?
peerreynders
peerreynders7mo ago
That's exactly why you use reconcile with a store-to ensure the referential stability of the items being rendered which in turn minimizes the render effort. I personally don't use mutables. Ryan cringes whenever he mentions them.
Ditto
Ditto7mo ago
Actually, I just saw this from the article linked above:
modifyMutable(
data,
reconcile({
colors: ['red', 'blue', 'orange', 'black'],
fruits: ['apple', 'banana', 'orange']
})
)
modifyMutable(
data,
reconcile({
colors: ['red', 'blue', 'orange', 'black'],
fruits: ['apple', 'banana', 'orange']
})
)
So this wouldn't prevent re-renders for unchanged values?
peerreynders
peerreynders7mo ago
Have a look at what is happening in the DOM (developer console, Elements view) on change. While it's not 100% reliable you should just see the new elements flash. I personally start with { merge: true }. Ryan has a distaste for mutables because they violate read/write segregation (and therefore don't fit the Flux view of the world)- they are simply a convenience that sacrifice RWS.
Ditto
Ditto7mo ago
So you're saying you can use reconcile with mutables, but they have this problem that they violate read/write segregation. Is that right? I've seen people echo the same criticism for mutables in this server and elsewhere, but I've also seen them being recommended for recursive components.
halu
haluOP7mo ago
that was more an appeal to DX and maintainability, im pretty sure its still fine grain tho (afaik) they may increase spaghetti if you abuse them, but i'd make the argument that RWS can also create spaghetti (ie recursion and passing setter/getter around) just pick the right tool for the job imo
Ditto
Ditto7mo ago
This is a bit old, but here is Ryan recommending createMutable for a "tree" component: https://github.com/solidjs/solid/discussions/499#discussioncomment-930195
GitHub
efficiently render tree structures · solidjs solid · Discussion #499
import { render, Dynamic } from "solid-js/web"; import { createSignal, createState, JSX, For, produce } from "solid-js"; type Node = { id: number; children: Node[]; }; type Node...
Ditto
Ditto7mo ago
Good to know!
halu
haluOP7mo ago
if it were outright bad it wouldnt be in the framework, and i dont think theres any plan to remove it for V2 either (scratch that https://discord.com/channels/722131463138705510/1245832940060147752/1246121125071687718, may be removed after all)
Discord
Discord - Group Chat That’s All Fun & Games
Discord is great for playing games and chilling with friends, or even building a worldwide community. Customize your own space to talk, play, and hang out.
Ditto
Ditto7mo ago
It seems there's a general attitude in the community to recommend outright avoiding createMutable, but at the same time, if you look for it, you can find people claiming it's the best way to solve a particular problem. Even the documentation warns against using createMutable without saying anything about how sometimes it's the right thing to use.
glassy
glassy7mo ago
Yeah, It depends on how you structure your code anyways. I'm sure using stores if you pass around the setStore method then it can easily be spaghetti too. And if you write organized code then createMutable can work too. But what clicked for me and made me see the value of stores is once I figured out that you can keep the setStore method private or keep it somewhere that no other code can access it, then you can control all changes to the store with good peace of mind. At the same time you can expose the store itself and components and other code can easily access the store.properties.whatever.data directly which means the components are still as easy to write as normal mutables. Then it is just the updating of the store values that is different, which I then discovered produce makes it as easy as mutables. Anyways, I used to use MobX and classy-solid and Vue 2's reactivity which all work in the same way as mutables and I (and tons of other people) were able to ship good production code. This pattern is what I like that stores can do. I could expose a mutable instead, but then anyone could modify it. Otherwise I could put the mutable as a private member, but then when I want to return data to components I would have to write getter methods / or clone the output if I wanted to make sure no other code changes the data in the mutable.
Ditto
Ditto7mo ago
Would you still prefer to use stores even with recursive components?
glassy
glassy7mo ago
I haven't tried that. OK let me think, you mean (I think I read somewhere) a Chat Message that can have children Chat Messages?
halu
haluOP7mo ago
yea using stores in some sort of composable way is great to avoid prop drilling, set global states, or create encapsulated store instances
but you'll prob still have minor frustrations when the components directly reflect the shape of deep data (like nested trees) (but again i havent tried produce for that yet)
Ditto
Ditto7mo ago
Tree View | Ark UI
A headless component library for building reusable, scalable design systems that works for a wide range of JS frameworks.
peerreynders
peerreynders7mo ago
seen them being recommended for recursive components.
It may well be that I haven't encountered a problem that needs them. But I've also observed that RWS is usually disliked by people who want "general reactivity"—these are same people who prefer the rendition of Preact Signals; an object with a getter/setter. In my view "general reactivity" is minefield of foot guns destined to give reactivity a bad name. Ryan's RWS philosophy on the other hand is one of responsible, mindful mutation. So when I plan the reactive portion of my code I always think of it as a uni-directional flow of change propagation: - starting at signal and store setters (perhaps createAsyncconsumers bound to cache points) - over various derived values and memos - to finally terminate at the various effects (which of course includes the JSX).
glassy
glassy7mo ago
For trees, rendering isn't an issue cause the component root can just take the store root. But doing modifications to it is the hard part I think. Gonna have to think about it cause trees are something I will need to implement in my app eventually too.
Ditto
Ditto7mo ago
I think that with stores you'd have to keep the path for each sub-component of the tree so you could pass that to setStore later when you wanted to update.
glassy
glassy7mo ago
I still think produce might work
setStore(produce(store => {
callRecursiveFunctionToDoSomethingOn(store)
}))
setStore(produce(store => {
callRecursiveFunctionToDoSomethingOn(store)
}))
And if starting at a deep point then
setStore(produce(store => {
const node = store[get][to][the][deep][path]
callRecursiveFunctionToDoSomethingOn(node)
}))
setStore(produce(store => {
const node = store[get][to][the][deep][path]
callRecursiveFunctionToDoSomethingOn(node)
}))
Ditto
Ditto7mo ago
🤔
glassy
glassy7mo ago
Each recursive component would have to keep record of it's path so you can start at right node when you want to do an operation on it.
Ditto
Ditto7mo ago
yeah on the other hand, with mutable you could just... do it?
glassy
glassy7mo ago
Yes, tradeoffs. lol exactly, just saw that I did ask about stores within stores: https://discord.com/channels/722131463138705510/722131463889223772/1246075106707636305 And it seems it is supported. So I'm thinking maybe that could be used for trees. But again, that might still be kind of more complicated.
Ditto
Ditto7mo ago
yeah, don't know if that is worth it just to keep RWS It would be cool if there was a way to "slice" parts of a store
glassy
glassy7mo ago
So it's true, createMutable might be the most straightforward way to implement trees. I might still try that produce approach above though when I get to it (lol)
Ditto
Ditto7mo ago
Then each sub-component would have it's own store, but they would only be "views" into a single store
halu
haluOP7mo ago
if you want to avoid the deep mutation problem in a tree, you can use nested references that point to a Map()/Object pseudocode...
// you can traverse structure from this:
{
a: {
b: {
c: {}
},
d: {}
}
}

// and read/write to these:
{
a: {/* node properties */}
b: {}
c: {}
d: {}
}
// you can traverse structure from this:
{
a: {
b: {
c: {}
},
d: {}
}
}

// and read/write to these:
{
a: {/* node properties */}
b: {}
c: {}
d: {}
}
you can also represent a tree as a list/map with whose nodes point to a parent or contain an array or children really just depends wut ur doing tbh
Ditto
Ditto7mo ago
Yeah, I saw someone suggest that before But then wouldn't you have to keep the state in two different shapes? I'm trying to think about how that would work
REEEEE
REEEEE7mo ago
just have them separate and store a children property that points to the other nodes in the tree
{
a: {children: ['b', 'd']}
b: {children: ['c']}
c: {}
d: {}
}
{
a: {children: ['b', 'd']}
b: {children: ['c']}
c: {}
d: {}
}
Ditto
Ditto7mo ago
Can you show how you'd render that?
halu
haluOP7mo ago
anything that is not a nested tree requires more book keeping
REEEEE
REEEEE7mo ago
Just keep the flat tree for reference in a context and you can do the recursive rendering normally but the components reference the flat tree to get their data
halu
haluOP7mo ago
for completeness... the inversion:
{
a: {parent: ''} // no parent is the root
b: {parent: 'a'}
c: {parent: 'b'}
d: {parent: 'a'}
}
{
a: {parent: ''} // no parent is the root
b: {parent: 'a'}
c: {parent: 'b'}
d: {parent: 'a'}
}
REEEEE
REEEEE7mo ago
const Child = (props: {id: string, children: string[]}) => {
const context = useContext(TreeContext)

return (
<div>
<For each={props.children}>
{childId => <Child id={childId} children={context.tree[childId].children} />}
</For>
</div>
)
}
const Child = (props: {id: string, children: string[]}) => {
const context = useContext(TreeContext)

return (
<div>
<For each={props.children}>
{childId => <Child id={childId} children={context.tree[childId].children} />}
</For>
</div>
)
}
Something like that you can even avoid passing the children as props if needed and just pass the id along with the other props
Ditto
Ditto7mo ago
That seems pretty cool! Do you think this is a better approach than using createMutable ?
REEEEE
REEEEE7mo ago
In my opinion it's almost always easier to use a flat tree rather than a recursive one You can use createStore and do your updates easily
Ditto
Ditto7mo ago
You'd have to make sure that each "node" has a unique id tho
REEEEE
REEEEE7mo ago
Another thing to note about createMutable is that it may be removed in Solid 2.0, although it will continue to exist in solid-primitives
halu
haluOP7mo ago
oh thats news to me
Ditto
Ditto7mo ago
Imagine I had a tree view component that shows list of People and a list of Animals
REEEEE
REEEEE7mo ago
True, this completely depends on your data but you can probably figure out a way to create a simple unique id
Ditto
Ditto7mo ago
you could have both a person and an animal with id 1 so you'd have to prefix that somehow person-1, animal-1 something like that
glassy
glassy7mo ago
very nice solution. but deleting an item is hard? maybe each node needs to point both to children and parent
Ditto
Ditto7mo ago
you'd probably just recreate the whole thing no?
glassy
glassy7mo ago
without link to parent then if I want to delete item 'c' I'd have to scan every item to look for its parent talking about this one I kind of skipped the discussion so sorry if I missed something, was just looking at that and thinking about it.
REEEEE
REEEEE7mo ago
yeah you can include a parent property too I was just simplifying the example
Ditto
Ditto7mo ago
Can you show how you'd delete a node that way?
REEEEE
REEEEE7mo ago
for each node, check if it's children has 'c'
for each node, check if it's children has 'c'
if nodes can have multiple parents, then you'll need a different solution for identifying children/parents
Ditto
Ditto7mo ago
No, I meant with the parent prop You'd just remove 'c' from the parent's children?
REEEEE
REEEEE7mo ago
look up the parent, remove 'c' from the parent's children, and delete 'c' yup
Ditto
Ditto7mo ago
gotcha thanks!
glassy
glassy7mo ago
then with both parent and children have to be careful they don't lose sync. maybe only serialize one and generate the other on load so at least the persisted data doesn't become broken even with saving just one, could still have inconsistent data (eg. a non existing child or a non existing parent). and that is the downside of not storing it as a straight tree. however, in the end if it comes from a database, then the database stores it in the same way probably the database just has a 'parent' column but could be other ways. hierarchical storage in relational database is always something I have to read up on the different ways to do it everytime so in any case, I guess having the in-memory store mirror how it's stored in the database would be a good idea
halu
haluOP7mo ago
^ there are million tradeoffs... look up times, mutation costs, book keeping and denormalizing data, memory footprint, data volatility, shape of data and updates (may not have control), DX, etc. really just depends on the situation and requirements what is best. Sorry to revive, just cross-linking an update in case anyone should find themselves here in the future https://discord.com/channels/722131463138705510/1252318635989405818
Want results from more Discord servers?
Add your server