Replacement for `this` in cells?

In Observable Javascript, this refers to the previous state of a cell. This is very handy, because this can store the state of a simulation (both the visible state in DOM nodes and hidden data as well), and each time the cell is executed the simulation can advance by one time step. This made it easy to update only those parts of the simulation that changed while leaving everything else alone.

Here is the base of how I was using it:

https://observablehq.com/@akrawitz/simulation-controls

I was, of course, building on the shoulders of giants, as shown here:

https://observablehq.com/@mbostock/pause-a-generator

And here:

https://observablehq.com/@tmcw/untitled/5

As far as I can tell, this doesn’t work the same way in new plain JavaScript/TypeScript cells. What would be the canonical replacement?

The closest analogy is probably Mutable, and that reminds me I need to write some more documentation… you can see the Framework docs on mutables for now.

The behavior of this in Observable JavaScript is another form of mutable state, and I think Mutable is an improvement over this because it makes it more explicit and more flexible. You can also think of Mutable as similar to React’s useState hook.

Adapting one of your simulation control examples, I’d first define the simulation state as a mutable, and then provide a getter (for peeking “non-reactively” at the state) and likewise a setter. The setter is important because it allows another cell (a cell other than the one that declared the mutable) to mutate the state.

const state = Mutable({cycle: 0});
const getState = () => state.value;
const setState = (s) => (state.value = s);
const epochLength = 20;

Then your simulation code can run in another cell:

{
  conSkip;
  let state = getState();
  if (conSkip.reset) {
    state = setState({cycle: 0});
  } else {
    state = setState({cycle: state.cycle + 1});
    if (conSkip.skip && (state.cycle % epochLength) === 0) {
      conSkip.stopSkip();
    }
  }
}

Notebook: https://observablehq.com/d/1b4a0e1ad4bdc170

Another approach you could consider is using event listeners rather than reactive references (“reactions” to the input) to update your simulation state. That’s potentially a little simpler since you can use a single cell to define the simulation state and how it updates, rather than needing two cells.

To do this, you first display your input instead of using view (or do both, but you need to be able to reference the input in order to call addEventListener):

const conSkipInput = display(controls({skip: true, speed: 75}));

Then here’s your simulation state definition:

const state = Mutable({cycle: 0});
const epochLength = 20;

conSkipInput.addEventListener("input", (event) => {
  const conSkip = event.currentTarget.value;
  if (conSkip.reset) {
    state.value = {cycle: 0};
  } else {
    state.value = {cycle: state.value.cycle + 1};
    if (conSkip.skip && (state.value.cycle % epochLength) === 0) {
      conSkip.stopSkip();
    }
  }
});

(Note that this leaks an event listener if you re-run the state cell, so be careful when editing. If you want to clean that up, you can either use the invalidation promise to remove the event listener, or assign to conSkipInput.oninput instead of using conSkipInput.addEventListener.)

Notebook: https://observablehq.com/d/f3c1b2d06555fc77