title | section | description |
---|---|---|
Computed Properties |
Advanced |
Use object getters and setters |
In Valtio you can use object & class getters and setters to create computed properties.
ℹ️ NoteGetters in JavaScript are a more advanced feature of the language, so Valtio recommends using them with caution. That said, if you are a more advanced JavaScript programmer, they should work as you expect; see the "Note about using
this
" section below.
const state = proxy({
count: 1,
get doubled() {
return this.count * 2
},
})
console.log(state.doubled) // 2
// Getter calls on the snapshot work as expected
const snap = snapshot(state)
console.log(snap.doubled) // 2
// When count changes in the state proxy
state.count = 10
// Then snapshot's computed property does not change
console.log(snap.doubled) // 2
When you call state.doubled
on the state
proxy, it is not cached, and will be re-calculated on every call (if you must cache this result, see the section below on proxy-memoize
).
However, when you make a snapshot, calls to snap.doubled
are effectively cached, because the value of an object getter is copied during the snapshot process.
ℹ️ NoteIn the current implementation a computed property should only reference **_sibling*** properties, otherwise you'll encounter weird bugs. For example:
const user = proxy({
name: 'John',
// OK - can reference sibling props via `this`
get greetingEn() {
return 'Hello ' + this.name
},
})
const state = proxy({
// could be nested
user: {
name: 'John',
// OK - can reference sibling props via `this`
get greetingEn() {
return 'Hello ' + this.name
},
},
})
const state = proxy({
user: {
name: 'John',
},
greetings: {
// WRONG - `this` points to `state.greetings`.
get greetingEn() {
return 'Hello ' + this.user.name
},
},
})
const user = proxy({
name: 'John',
})
const greetings = proxy({
// WRONG - `this` points to `greetings`.
get greetingEn() {
return 'Hello ' + this.name
},
})
A workaround to it is to attach the related object as a property.
const user = proxy({
name: 'John',
})
const greetings = proxy({
user, // attach the `user` proxy object
// OK - can reference user props via `this`
get greetingEn() {
return 'Hello ' + this.user.name
},
})
Another method would be to create a separate proxy and
synchronize with subscribe
.
const user = proxy({
name: 'John',
})
const greetings = proxy({
greetingEn: 'Hello ' + user.name,
})
subscribe(user, () => {
greetings.greetingEn = 'Hello ' + user.name
})
Or with watch
.
const user = proxy({
name: 'John',
})
const greetings = proxy({})
watch((get) => {
greetings.greetingEn = 'Hello ' + get(user).name
})
Setters are also supported:
const state = proxy({
count: 1,
get doubled() {
return state.count * 2
},
set doubled(newValue) {
state.count = newValue / 2
},
})
// Setter calls on the state work as expected
state.doubled = 4
console.log(state.count) // 2
// Getter calls on the snapshot work as expected
const snap = snapshot(state)
console.log(snap.doubled) // 4
// And setter calls on the snapshot fail as expected
// compile error: Cannot assign to 'doubled' because it is a read-only property.
// runtime error: TypeError: Cannot assign to read only property 'doubled' of object '#<Object>'
snap.doubled = 2
As with getters, setter calls within set doubled
(i.e. this.count = newValue / 2
) are themselves invoked against the state
proxy, so the new count
value will be correctly updated (and subscribers/snapshots notified of the new change).
If you make a snapshot, all properties become readonly, so snap.doubled = 2
will be a compile error, and will also fail at runtime because snapshot
objects are frozen.
Class getters and setters work effectively like object getters and setters:
class Counter {
count = 1
get doubled() {
return this.count * 2
}
set doubled(newValue) {
this.count = newValue / 2
}
}
const state = proxy(new Counter())
const snap = snapshot(state)
// Changing the state works as expected
state.doubled = 4
console.log(state.count) // 2
// And the snapshot value doesn't change
console.log(snap.doubled) // 2
Similar to object getters, class getters on the state
proxy are not cached.
However, unlike object getters, class getters on the snapshot
object are not cached, and are re-evaulated on every access to snap.doubled
. As mentioned in the snapshot
docs, this should be fine because the expectation is that getters are as cheap to evaluate as they would be to cache.
Also unlike object setters on snapshots (which immediately fail at runtime when called), class setters on snapshots will technically start evaluating, but any mutations they do internally (i.e. this.count = newValue / 2
) will then fail at runtime because this
will be a snapshot instance and the snapshots are frozen by Object.freeze
.
If you need to cache getter results even for the state
proxy itself, you can use Valtio's sister project proxy-memoize.
proxy-memoize
uses a similar usage-based tracking approach as Valtio's snapshot
function, so it will only re-calculate the getter if fields accessed by the getter logic have actually changed.
import { memoize } from 'proxy-memoize'
const memoizedDoubled = memoize((snap) => snap.count * 2)
const state = proxy({
count: 1,
text: 'hello',
get doubled() {
return memoizedDoubled(snapshot(state))
},
})
With this implementation, when text
property changes (but count
has not), the memoized function won't be re-executed.
You can use this
inside of getters and setters, however you should be familiar with how JS this
works: basically that this
is whatever object you invoked the call against.
So if you call state.doubled
, then this
will be the state
proxy.
And if you call snap.doubled
, then this
will be the snapshot object (except for object getters & setters, where the current value is copied during the snapshot process, so object getters & setters are never invoked on snapshots).
Despite this nuance, you should be able to use this
as you would expect, and things will "just work".