Skip to content

Latest commit

 

History

History
225 lines (177 loc) · 6.41 KB

computed-properties.mdx

File metadata and controls

225 lines (177 loc) · 6.41 KB
title section description
Computed Properties
Advanced
Use object getters and setters

Computed Properties

In Valtio you can use object & class getters and setters to create computed properties.

⚠️ Note: Getters 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.

Simple object getter

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.

⚠️ Note: In 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
})

Object getter and setter

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

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.

State usage tracking with proxy-memoize

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.

Note about using this

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".