I'm trying to modify an element of a SolidJS store using the SetStoreFunction (setExpressions), so that the View for the store's values are updated, e.g. when text is entered into a text field.
I originally thought the problem had to do with calling the SetStoreFunction itself, but the console logging in getExpressionSetValue
shows that the setting is working. So the issue is with the updating of the View after the set takes place.
My expectation was that it would automatically be handled when using a store. I'm not sure if I'm using it wrong or what the problem is
Full Code:
import { createStore } from "solid-js/store";
import type { Component } from 'solid-js'
import { For } from 'solid-js';
import { TextField } from "@kobalte/core"
import { Button } from "@kobalte/core"
interface Expression {
id: number,
label: string,
value: string,
}
interface ExpressionViewProps extends Expression {
removeExpression: () => void
setValue: (value:string) => void
}
const ExpressionView: Component<ExpressionViewProps> = (props) => {
return (
<div>
<TextField.Root
value={props.value} //todo why doesn't this value update automatically?
onChange={props.setValue}>
<TextField.Label>
{props.label}
</TextField.Label>
<br/>
<TextField.Input />
<TextField.Description />
<TextField.ErrorMessage />
</TextField.Root>
<Button.Root class="button" onClick={props.removeExpression}>-</Button.Root>
</div>
)
}
const App: Component = () => {
const [expressions, setExpressions] = createStore<Expression[]>([])
let expressionId = 0
const addExpression = () => {
setExpressions(() => {
const id = ++expressionId
return [...expressions, {id: id, label: `Expression ${id}`, value: "inital value 0"}]
})
}
const getRemoveExpression = (id: number) => {
return () => {
setExpressions(() =>
expressions.filter(expression => expression.id != id)
)
}
}
const getExpressionSetValue = (id: number) => {
return (value: string) => {
console.log(value)
setExpressions(expression => expression.id == id, 'value', value)
console.log(expressions)
}
}
return (
<div>
<For each={expressions}>{(expression) => {
const expressionViewProps: ExpressionViewProps = {
...expression,
removeExpression: getRemoveExpression(expression.id),
setValue: getExpressionSetValue(expression.id),
}
return <ExpressionView {...expressionViewProps}/>
}}</For>
<Button.Root class="button" onClick={addExpression}>+</Button.Root>
</div>
)
}
export default App;
Update 8/8:
I tried switching to native UI elements instead of using Kobalte and now it works. E.g.:
const ExpressionView: Component<ExpressionViewProps> = (props) => {
return (
<div>
<input
value={props.value}
onInput={(e) => props.setValue(e.target.value)}
/>
<button type="button" onClick={() => props.removeExpression()}>-</button>
</div>
)
}
and
<button type="button" onClick={addExpression}>+</button>
I still don't understand what's going on here and would like to fix the Kobalte version, because I'd like to use Kobalte
I haven't tested this out, but I think the following should help you:
In general, there are two types of input components - "controlled" and "uncontrolled".
In solid, the behavior of the input element (and DOM elements in general) is uncontrolled. However, in kobalte, if you supply a value
prop, then the component becomes controlled. In your case, I believe the value prop passed to ExpressionView
never changes (I'll get to that in a second) so the display never changes either. Using a regular input doesn't have this issue, as uncontrolled mode works fine without needing the value prop to update.
Now, as for the reason the value passed to ExpressionView
is not updating, the problem is how you create the ExpressionViewProps
<For each={expressions}>{(expression) => {
const expressionViewProps: ExpressionViewProps = {
...expression,
removeExpression: getRemoveExpression(expression.id),
setValue: getExpressionSetValue(expression.id),
}
return <ExpressionView {...expressionViewProps}/>
}}</For>
In general, stores track where properties are accessed, and if the property is accessed inside a reactive scope, then that scope will rerun when the value in the store is updated. So, in your code, using ...expression
accessed every property on the expression
object, and assigns the key/value to the new object you're creating, but this isn't happening inside a reactive scope (the callback of For is not a reactive scope), so updates to the store won't cause the callback to rerun.
What I suggest instead is to do
return <ExpressionView removeExpression={getRemoveExpression(expression.id)} setValue={getExpressionSetValue(expression.id)} {...expression}/>
Because solid's compiler will analyze the JSX (the compiler only analyzes jsx) and see that {...expression}
is potentially reactive and create lazy property accesses (so the access will end up tracked where props.value
is used) instead of a spread.