Ramonda

One per component, or one per item

Two decorators cache a value for you, and they look alike from the outside: both hold a result, both hand back the same thing until something they read has moved. People reach for the wrong one because the similarity is the visible part.

The difference is the key.

@compute@memoized
keyed bynothingits arguments
how many valuesone per componentone per argument, per component
you writea getter, or a method — with no argumentsa method that takes arguments and returns the value
you read it asas written — this.total or this.total()a call — this.rowConfig(id)
when it recomputesa signal its body read has changedthe same, and only that entry is dropped
when it is droppednever, while the component liveswhen a render stops asking for that argument

The question that decides it

Is there one of this value per component, or one per item?

That is the whole decision, and it has no grey area:

class Board extends Component {
  @state rows: RowItem[] = [];
  @state filter = "";

  // ONE per component: a single number derived from the list.
  @compute
  get total() {
    return this.rows.filter((row) => row.name.includes(this.filter)).length;
  }

  // ONE PER ROW: a different handler for each id.
  @memoized
  removeFor(id: string) {
    return () => {
      this.rows = this.rows.filter((row) => row.id !== id);
    };
  }
}

A @compute cannot do the second, and not because it would be slow: it belongs to the component, so it has exactly one slot. There is nowhere to put a value per row.

The compiler already knows

A @compute caches one value per component, so it has no key and nothing can pass it an argument. Both of its forms refuse one:

class Board extends Component {
  @state factor = 2;

  // ✗ refused in every build: one value per component means the argument is ignored
  // @compute times(n: number) { return n * this.factor }

  // ✓ arguments are what @memoized is keyed by
  @memoized
  times(n: number) {
    return n * this.factor;
  }
}

ramonda-check reports it before the build as compute-takes-no-arguments, and the framework throws when the class definition runs. The mistake this page exists for is the other direction: writing a fresh object per row, and reaching for neither.

@memoized caches a value, not only a handler

The commonest per-item value is a handler, which is why it is introduced with events. It is not limited to one. A row that needs a stable object has the same problem and the same answer:

class Board extends Component {
  @state rows: RowItem[] = [];

  @memoized
  configFor(id: string) {
    return { id, href: `/rows/${id}` };
  }

  render() {
    return <ul>{list(this.rows, (row) => <RowView cfg={this.configFor(row.id)} />)}</ul>;
  }
}

Written inline, that object is new on every render, so RowView re-renders every time and a development build reports it (RMD020, and RMD022 for the same thing in a hook's props callback). Their advice names @memoized for exactly this case, because nothing else reaches it: a @compute is one per component, and a field or a module constant cannot vary per row.

What they share

Both watch the signals their body read. Whatever a @compute getter or a @memoized builder reads while it runs is recorded, and a write to any of it invalidates what was built from it — a @compute recomputes, and @memoized drops that one entry so the next call builds it again.

This is the part that makes them feel like one thing. It is machinery they happen to share, not what tells them apart.

Both freeze what they captured. A value read before the return is closed into the result:

@memoized
removeFor(id: string) {
  const mode = this.mode;              // read while BUILDING
  return () => this.apply(mode, id);   // frozen into this handler
}

That is watched, so it is not stale — but it is worth knowing which reads count as the builder's. What the cache is allowed to remember has the detail.

The arguments have to be keyable

@memoized builds its key from the arguments, so they must be strings, numbers or booleans. An object has no stable form to build a key out of, and a development build throws rather than silently giving up the memoisation. Pass the id, or the index the list's mapper already hands you.

When neither

A value that never varies is not a cache problem. A field, or a module constant, says so more plainly and costs nothing:

const COLUMNS = ["name", "email", "role"];

Next