leva
leva copied to clipboard
Using with state management
Hello, First, Leva looks really great. I was in the process of rolling my own type of solution before I saw a post on Reddit. Your approach is MUCH cleaner and smarter than my attempt so I'm very intrigued at getting Leva integrated into my app.
My question is about state management/sync, specifically with something like Redux/RTK. Since it seems the each controls state is relatively coupled, what would be the correct way to use values that come from the apps 'global' state somewhere, while having the changes get passed down correctly to Leva's controls?
One approached I've tried is a useEffect on the value I want to "watch" from a control and then just dispatch the updated value back to my store. Another is to use the new onChange callback to do the same thing. But I'll be honest, both of those don't feel right...
I know I might be missing something obvious and any insight on how to approach this would be great.
Hi! So binding with another state is not something that we've considered from the ground up, so you feeling wrong might be normal.
As you've noted, leva provides the onChange a callback that doesn't trigger a render, so that means that you'd have to use your store values to make the component react to changes.
leva also provides a set function (undocumented I believe) that is returned when using the function api.
const [values, set] = useControls(() => inputs)
You could also use that function to react to your store changes and update leva accordingly.
If this doesn't help or still feels wrong, please provide what you think would be a good api for your usecase and we'll see what we can do!
I'm in a similar situation and I'm curious to know if anyone has come up with any nice recipes for this problem. I have global state using zustand and it would be cool to hook it into leva!
Ok, not to turn this into a blog but I recently started messing with Zustand (@funwithtriangles it's pretty neat!) and here's what I've done so far to get state flowing.
Let's say I've got a function that returns a <Leva> and we use useStore to hook into our state. petName is something coming from outside this function and not being changed with Leva, while petColor is being changed by Leva and the new color being sent back to Zustand.
The Zustand portion would look like this:
const {
petName,
petColor,
update // this is to relay my changes to zustand
} = useStore()
With the Leva portion looking like this:
const [{ }, set] = useControls(
() => ({
name: {
label: 'Pet name',
value: petName,
onChange: (c) => {
set({name: petName})
},
},
color: {
label: "Pet color",
value: petColor,
onChange: (c) => {
update('petColor', c)
}
}
}),
[petName, petColor]
);
I'm sure this is nothing earth shattering but it's helped me understand the flow of data to Zustand, Leva and back again.
Ok, not to turn this into a blog but I recently started messing with Zustand (@funwithtriangles it's pretty neat!) and here's what I've done so far to get state flowing.
Let's say I've got a function that returns a
<Leva>and we useuseStoreto hook into our state.petNameis something coming from outside this function and not being changed with Leva, whilepetColoris being changed by Leva and the new color being sent back to Zustand.The Zustand portion would look like this:
const { petName, petColor, update // this is to relay my changes to zustand } = useStore()With the Leva portion looking like this:
const [{ }, set] = useControls( () => ({ name: { label: 'Pet name', value: petName, onChange: (c) => { set({name: petName}) }, }, color: { label: "Pet color", value: petColor, onChange: (c) => { update('petColor', c) } } }), [petName, petColor] );I'm sure this is nothing earth shattering but it's helped me understand the flow of data to Zustand, Leva and back again.
From what I can tell, there're a few mistakes in the code, and I think the proper version should be:
const [, set] = useControls(
() => ({
name: {
label: 'Pet name',
value: petName,
onChange: (c) => {
set({name: c})
},
},
color: {
label: "Pet color",
value: petColor,
onChange: (c) => {
update('petColor', c)
}
}
}),
[update]
);
petName and petColor are states from outside but are only used to initialize the leva controls (value: xxx), so they can be removed from the depend list (you can think of them as values sent to useState() to initialized the state in React. No matter how many times the component rerenders, they only get used once). And the update function created by the store will be revoked every time the petColor in the leva panel changes, so update should be kept in the depend list to avoid any possible stale bahaviours.
BTW, I want to bring up several more possible caveats someone might find helpful when using Leva's controlled inputs:
1. Leva's controlled inputs aren't the same as controlled components in React
You can use controlled inputs in Leva as described in Advanced: Controlled Inputs
To change values in Leva store from the outside, use the function API by passing
() => schematouseControls.
The set and onChange API may let you think they share the same logic as controlled component in React. However, the set is merely an exposed function for users to update the input values outside of Leva controls, but internally Leva can still update its input values. So everytime you toggle the inputs in Leva, set is implicitly executed for those inputs and you can neither intercept nor prevent it in onChange, which also means your are not required to manully set their values in onChange. In most cases this won't be a problem (you almost always want inputs to be reactive to your toggles), but keep it in mind.
2. Split useControls to avoid unecessary rerenders or solve interdependence deadlocks
Let's use the example above:
const [, set] = useControls(
() => ({
name: {
label: 'Pet name',
value: petName,
onChange: (c) => {
set({name: c})
},
},
color: {
label: "Pet color",
value: petColor,
onChange: (c) => {
update('petColor', c)
}
}
}),
[update]
);
We can see that name has nothing to do with the dependency update, but every time update updates, the name input will re-render and trigger its onChange. To avoid unnecessary re-renders, we can always split up the useControls:
const [, setPetName] = useControls(() => ({
name: {
label: "Pet name",
value: petName,
onChange: (c) => {
setPetName({ name: c });
},
},
}));
useControls(
() => ({
color: {
label: "Pet color",
value: petColor,
onChange: (c) => {
update("petColor", c);
},
},
}),
[update]
);
I'm not sure I understand why anyone would use the set function inside onChange. This is redundant. If you need reactivity with onChange, you can set the transient option to false (this option might be renamed to reactive in the future).
Hmm, I'd like to update some values used in multiple components, and am unsure about the most performant and elegant way to do this, that's quick to implement on future projects also. I am currently fumbling around with using zustand for this, and then updating the values there, when they are updated in Leva... am I missing some obvious solution to this?