DEV Community

Discussion on: A Hands-on Introduction to Fine-Grained Reactivity

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

Admittedly I was staring at this problem for years before I ever saw React Hooks, but I knew in 2 seconds tuples were what I always wanted.

In JavaScript we've had a few versions. There is the single getter/setter function:

const s = createSignal(0);
// read a value
console.log(s()); // 0

// set a value
s(5);
console.log(s()); //5
Enter fullscreen mode Exit fullscreen mode

This one is probably the most awkward as a certain point you end up with.. is it a signal? is it a function? is it writable? If it is should I? What if I pass it into a function directly to track, and that function later starts calling with arguments. I've experience all of this first hand. KnockoutJS popularized this approach but it becomes pretty ambiguous once you leave local scope. Maybe it's because we aren't in a typed language, but this is about the most error prone approach there is.

const s = createSignal(0);
// read a value
console.log(s.get()); // 0

// set a value
s.set(5);
console.log(s.get()); //5
Enter fullscreen mode Exit fullscreen mode

This is how it works in MobX, and Svelte 2.. and probably where I would have ended up if I had never seen React Hooks. This isn't bad but it's a little verbose. You could always split them off though:

const { get, set } = createSignal(0);
Enter fullscreen mode Exit fullscreen mode

But multiple signals you end up doing a lot of aliasing:

const { get: s, set: setS } = createSignal(0);
Enter fullscreen mode Exit fullscreen mode

Vue using a simple .value getter is a 3rd option but it also has verbosity and unlike the separate .get .set you can't just destucture it as it uses assignment semantics to set. Of course you could always wrap each part in a function:

const ref = createSignal(0);
const s = () => ref.value;
const set = v => ref.value = v;
Enter fullscreen mode Exit fullscreen mode

After looking at all these none of them were preferable. The thing with tuples are you can name them exactly as you want. They have explicit meaning. A read is always just a function. Even with proxies and what not or derived expressions if wrap it in a thunk it's a signal. Like fullName in the first Derivation example above.

In general it makes it easier to visually see and talk about in terms of composition and makes it very easy to maintain this same [get, set] signature. This makes it not only the best teaching API but also in my opinion the just the best API for this.

Consider this useReducer composition:

const useReducer = (reducer, state) => {
  const [getState, setState] = createSignal(state);
  const [getAction, dispatch] = createSignal();
  createEffect(() => {
    const action = getAction();
    if (!action) return;
    state = reducer(state, action);
    setState(state);
  });
  return [getState, dispatch];
};
Enter fullscreen mode Exit fullscreen mode

Look at how you wire the the signals together to create derived behavior. It's just clean. Other patterns are not nearly as much.

EDIT: I am aware this specific example implementable without using the Effect and having the the dispatch just call the state reducer directly. But that is sort of besides the point. I wanted to show how you can easily wire these Signals together.

Collapse
 
drsensor profile image
૮༼⚆︿⚆༽つ

Do you know any framework/libs that use this signature

interface Signal<State> {
  () => State
  set(val: State): void
}
function createSignal<T>(s: T): Signal<T>
Enter fullscreen mode Exit fullscreen mode

which the usage is something like

const state = createSignal(0)
// read a value
console.log(state()) // 0

// set a value
state.set(5)
console.log(state()) // 5
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
ryansolid profile image
Ryan Carniato

Actually no. The single function and the separate get/set are by far the most common. The tuple that I use is super uncommon in JavaScript in reactive libraries except maybe some newer ones in React.

This was one of the ones I was super leaning towards the month before hooks came out. I was worried it was too clever using the function as the object to hang it off of. We used to use this pattern in KnockoutJS a reasonable amount of the time to add augmented properties. Generally, it was for attaching other observables and was called sub-observables. I found while teaching that to newcomers they found it weird.

What I liked was it was minimal. It only creates the 2 functions. Syntaxtually it kept the common case easy (get) and let you be explicit on sets. No weird doubling up, and about as minimal syntax. You would still name the variable as you like.

I'm not sure if I'd have landed on it over the MobX style but now looking back at it I would think I wouldn't have hesitated on that API if I hadn't seen hooks. I only came to respect the read/write split after I started using it. As I started playing with composition patterns and reading more academic papers.