S
SolidJS8mo ago
Morphy

Reactive field inside objects of an array

import {
Accessor,
ParentComponent,
Setter,
createContext,
createEffect,
createSignal
} from 'solid-js'

class Cart {
get: Accessor<Array<CartItem>>
private set: Setter<Array<CartItem>>
constructor(items = []) {
;[this.get, this.set] = createSignal<Array<CartItem>>(items)
}

add(id: number) {
let item = this.get().find((item) => item.id === id)

if (item === undefined) {
this.set((prev) => [...prev, new CartItem(id)])
} else {
throw Error('item already exists')
}
}
remove(id: number) {
this.set((prev) =>
prev.toSpliced(
prev.findIndex((item) => item.id === id),
1
)
)
}
}

class CartItem {
id: number
count: number

constructor(id: number) {
this.id = id
this.count = 1
}

increment() {
this.count++
}
decrement() {
if (this.count > 1) {
this.count--
} else {
throw Error(`count is ${this.count}, can't decrement more`)
}
}
}

export const cartContext = createContext(new Cart())

export const Provider: ParentComponent = (props) => {
const cart = new Cart()
createEffect(() => {
console.log(cart.get()) // I want this to update when I update `CartItem`'s count
})
return <cartContext.Provider value={cart}>{props.children}</cartContext.Provider>
}
import {
Accessor,
ParentComponent,
Setter,
createContext,
createEffect,
createSignal
} from 'solid-js'

class Cart {
get: Accessor<Array<CartItem>>
private set: Setter<Array<CartItem>>
constructor(items = []) {
;[this.get, this.set] = createSignal<Array<CartItem>>(items)
}

add(id: number) {
let item = this.get().find((item) => item.id === id)

if (item === undefined) {
this.set((prev) => [...prev, new CartItem(id)])
} else {
throw Error('item already exists')
}
}
remove(id: number) {
this.set((prev) =>
prev.toSpliced(
prev.findIndex((item) => item.id === id),
1
)
)
}
}

class CartItem {
id: number
count: number

constructor(id: number) {
this.id = id
this.count = 1
}

increment() {
this.count++
}
decrement() {
if (this.count > 1) {
this.count--
} else {
throw Error(`count is ${this.count}, can't decrement more`)
}
}
}

export const cartContext = createContext(new Cart())

export const Provider: ParentComponent = (props) => {
const cart = new Cart()
createEffect(() => {
console.log(cart.get()) // I want this to update when I update `CartItem`'s count
})
return <cartContext.Provider value={cart}>{props.children}</cartContext.Provider>
}
How do I achieve this (line 62)? Basically I want a reactive field inside objects of an array which update the array itself when they change
10 Replies
jer3m01
jer3m018mo ago
How do you update CartItem's count?
Morphy
MorphyOP8mo ago
with increment and decrement
jer3m01
jer3m018mo ago
You could pass Cart to CartItem and call .set((prev) => [...prev]) on increment/dec
Morphy
MorphyOP8mo ago
Which I don't want to do. The goal here is to have an intuitive api for Cart and CartItem
jer3m01
jer3m018mo ago
Signals are only reactive with reference updates and createStore doesn't make classes reactive. You might be able to use createStore for storing the array but I'm not familiar enough with it to help.
Maciek50322
Maciek503228mo ago
Only reactive thing here is the Cart, because only it has signal. It is only reactive to change of whole array of CartItem. If you want to have reactivity on the CartItem's count, then it should also be a signal (you already have increment and decrement that can be wrappers to update count as a signal). However, then it would be only reactive to the count only if you get it within some reactive context (like createEffect). So cart.get() still wouldn't react to increment / decrement this, but cart.get()[i].getCount() would and to the particular item (of index i) in list. The cart.get() would still trigger the effect if the whole array changes. There's also another way, less granular, to "update" whole Cart's array, when the CartItem's count changes. In this case CartItem should have the concept of ownership - it should know in which Cart it's contained in. Then, when count changes, trigger the "update" on whole array. Or maybe just the callback when count changes, there are probably many more ways. With callback it would be something like that:
class CartItem {
id: number
count: number
onUpdate: () => void

constructor(id: number, onUpdate: () => void) {
this.id = id
this.count = 1
this.onUpdate = onUpdate
}

increment() {
this.count++
this.onUpdate()
}
decrement() {
if (this.count > 1) {
this.count--
this.onUpdate()
} else {
throw Error(`count is ${this.count}, can't decrement more`)
}
}
}
class CartItem {
id: number
count: number
onUpdate: () => void

constructor(id: number, onUpdate: () => void) {
this.id = id
this.count = 1
this.onUpdate = onUpdate
}

increment() {
this.count++
this.onUpdate()
}
decrement() {
if (this.count > 1) {
this.count--
this.onUpdate()
} else {
throw Error(`count is ${this.count}, can't decrement more`)
}
}
}
And the onUpdate callback should trigger the change of array, so that it triggers other updates. It can be done with copy new CartItem(id, () => { set(prev => [...prev]) }) or just reassignment, when signal's option is createSignal<Array<CartItem>>(items, { equals: false }) - just set(prev => prev) would trigger update The first way should be more efficient, because when you change only one item's count, only dependents on this single item's count are updated. The second will trigger updates for things that depend on the whole array, so when you change one item's count, dependents on this item's count as well as others are updated. Not to say that it's not viable solution, so just use whatever meets your needs. And using createStore should have same effect as the first way (basically every property is signal), with just proxy overhead
Morphy
MorphyOP8mo ago
The first way is actually what is intended here In a cart You show items individually and its enough to just react to changes of that item and not update the array itself. we only update the array when length of it changes which makes sense
peerreynders
peerreynders8mo ago
The goal here is to have an intuitive api for Cart and CartItem
“Intuitive” perhaps in an imperative context; but here you are working in a reactive context where read/write segregation is valued. Think of it as query/command separation where the store delivers a read-only “query” object. The SetStoreFunction is then used as the building block for creating specialized, custom tailored function closures (the “commands”) that are only passed to those parts of the program which absolutely need write access (and with only just-enough write access). It's about granting (sane) mutability more responsibly.
martinfowler.com
bliki: Command Query Separation
a bliki entry for Command Query Separation
Jessica Joy Kerr (@jessitron) on X
GOTO was evil because we asked, "how did I get to this point of execution?" Mutability leaves us with, "how did I get to this state?"
Twitter
GitHub
solid/packages/solid/store/src/store.ts at 9082a7bc6f965f8e3a97821b...
A declarative, efficient, and flexible JavaScript library for building user interfaces. - solidjs/solid
Morphy
MorphyOP8mo ago
Yes but even if I use setStore as a primitive I have to wrap both Cart and CartItem into another type and do some weird stuff that complicates the code unnecessarily. Now that I thought about it more I realized that I don't need to update the Cart itself when count changes in any of the items
peerreynders
peerreynders8mo ago
A store is about data. A class instance collocates data and (possibly mutable) behaviour. What is the incentive to use a class in the first place? Have a look at solid-todomvc—the TodoStore items are editable; it's not that different from a Cart.
Want results from more Discord servers?
Add your server