DEV Community

Cover image for A Hands-on Introduction to Fine-Grained Reactivity

A Hands-on Introduction to Fine-Grained Reactivity

Ryan Carniato on February 09, 2021

Reactive Programming has existed for decades but it seems to come in and out of fashion. In JavaScript frontends, it has been on the upswing again ...
Collapse
 
ajuni880 profile image
Junaid Ramzan

I understand the idea of reactive value, but how the updates on DOM are performed, does the framework subscribe to the changes via effects or some unexplained magic is happening?

Collapse
 
ryansolid profile image
Ryan Carniato

That's it. DOM rendering are effects. More precisely nested effects as to only retrigger the closest change.

Collapse
 
ajuni880 profile image
Junaid Ramzan

Thanks for quick reply Ryan! Is it possible to see this logic in solidjs source code?

Thread Thread
 
ivelaval profile image
Ivan Avila

You can check the SolidJs project repository. More accurately here: github.com/solidjs/solid/tree/main...

Collapse
 
axeldeblen profile image
Axel • Edited

Awesome read.
I feel like I could be missing something in the cleanup function - perhaps the nested nature is catching me but

function cleanup(running: Subscriber) {
  for (const dep of running.dependencies) {
    dep.delete(running);
  }

  running.dependencies.clear();
}
Enter fullscreen mode Exit fullscreen mode

Could this just be
running.dependencies.clear();

I guess we need to reach through and delete the references other dependencies might have to running?

Collapse
 
ryansolid profile image
Ryan Carniato

It needs to unregister from the other side. It is linked both ways. The computation needs to also remove itself from the signal. So the first loop is removing itself from the others, and then it clears its own list.

Collapse
 
drsensor profile image
૮༼⚆︿⚆༽つ

I've been wondering why people like representing a reactive state in [get,set]=data instead of S.data 🤔
Is it because React introduces it that way?

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.

Collapse
 
peerreynders profile image
peerreynders • Edited

Using Observer Pattern terminology the "signal" comes from the "subject".

By using "getState" the "observer" not only "gets the state" of the subject but also implicitly subscribes to updates to the subject's state. Similarly "setState" triggers the machinery necessary to update anybody who's interested in the updated state.

See Finding Fine-Grained Reactive Programming: How It Works

Collapse
 
zarabotaet profile image
Dima • Edited

Great article, Ryan! As always)🤓👏👍

Recently, I have used reactive like state manager effector.dev Do you heard anything about it? Your opinion would be extremely interesting!

Also, the author of effector creates a simple library npmjs.com/package/forest
around effector reactivity. It's not production-ready and for usage in personal projects, but it seemed to me that it has great potential...

It has a native async incremental rendering and well-directed dom API representation.

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

I've seen Effector, but hadn't seen Forest before. Effector is based on FRP concepts very similar to RxJS but with store based focus. Which is cool because in that context we can limit operations generally to map transformations and remove a lot of the "What 50 operators do I need to learn to get started?" issues typical observable libraries have.

In so it is really great way to created a directed graph with minimal API. It doesn't use things like proxies or dependency tracking since it's explicit. It supports the Observable standard too so you can hook it up to any place that can handle observables quite easily (gotta love how easy that is in Svelte).

Forest looks like a VDOM library created with Effector in mind. Which is pretty cool. But I think this is where you will start to see the motivation for things that Effector chooses not to do. The biggest benefit of auto-tracking like you find in fine-grained is that dependencies are constructed where they lie. Meaning that you can construct graphs without explicitly wiring things up. This is really great when trying to write inline expressions in templates or conditional logic. I always had this issue when trying to work with RxJS as a core change manager in a framework and there have been some projects to look at ways to alleviate this. Effector has the same limitation.

The thing is that looking at the way React or Svelte or Vue update, Forest can tell a pretty compelling tale. You can get a similar effect by driving things off this less framework integrated system. I mean put what you want in this box though really as most state management solutions more or less have the ability to drive a simple VDOM implementation on their own. The complexity comes in local state so who needs it? Not to start the debate again on local vs global state there are benefits to both approaches, but I do think that state management libraries like Effector that can work in a distributed way can do a good enough impression that you could make something pretty effective and pretty usable like this.

But if I was betting on the future, this isn't the sort of approach I'd be looking at on the framework side. State management, for sure.. But as you know I'm big on fine-grained and the reason is the granularity of update performance. Sure React, Vue, Svelte, don't get to leverage it in a way that makes it any better than what Forest is doing, but we shouldn't cut ourselves short there. The potential of well executed fine-grained reactivity has a much higher ceiling.

It is possible to do this with systems like Effector but it would be very verbose to create all the necessary subscriptions at that level and manage the dynamic nature of them. You'd have to do a lot of explicit work. Things perhaps you could analyze with a compiler, but until we got to a point of perfect analysis, something like proxies ensures we can do this sort of thing with no extra syntax and ultimate dynamicism.

Obviously this is all my opinion and I'm biased in this regard. But the performance that I've created with Solid is repeatable. We're already seeing it in early prototypes of future version of Marko. I imagine when things get rolling we may see more libraries like Vue or Svelte look at this work. Or not. After all the approaches I am using for fine-grained rendering in Solid have been around since at least 2016(I was not the first to experiment here). But where I currently stand nothing else is coming close in terms of streamlining DX and performance. Which is why I believe more people should be learning about fine-grained reactivity.

Collapse
 
zarabotaet profile image
Dima

Thank you for such a comprehensive answer!

Collapse
 
briancodes profile image
Brian

in order for the Signals to be read underneath the Effect

This is from the Derivations section

What does it mean by underneath the Effect here?

const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const fullName = () => {
  console.log("Creating/Updating fullName");
  return `${firstName()} ${lastName()}`
};

console.log("2. Create Reactions");
createEffect(() => console.log("My name is", fullName()));
createEffect(() => console.log("Your name is not", fullName()));

console.log("3. Set new firstName");
setFirstName("Jacob");
Enter fullscreen mode Exit fullscreen mode
Collapse
 
alijaya profile image
Ali Jaya Meilio

Ummm is this the expected behaviour on the last example?

If i write like this

batch(() => {
  setB(3);
  log(c()); // gives 4 instead of 6
  setC(2); // after this it will run the effect and will print 8 as expected
});
Enter fullscreen mode Exit fullscreen mode

So in the batch we can't read derivative?

Collapse
 
omril321 profile image
Omri Lavi

That's awesome, thank you.
I like how you explained things without being opinionated about specific libraries.

Collapse
 
artydev profile image
artydev

Great Ryan,
Thank you

Collapse
 
omenlog profile image
Omar E. Lopez

Hello @ryansolid , great article thanks for it.

Could you share some links for papers related to reactivity that you recommend as more deeper read ?

Collapse
 
ferhatavdic profile image
FerhatAvdic

The exact same patterns like hooks in react (useMemo, useCallback, useState, useEffect)

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

It's similar looking but mechanically very different. I've talked about this previously in Exploring the State of Reactivity Patterns in 2020.

This is part of why I want to teach people about this. React's model is one where the component runs over and over. To make my demo would have been a very different environment. Now some libraries do use this reactivity inside React and Vue is basically like if you put the 2 together. But purely React Hooks is different. Compare this React code:

React useInterval

To this Solid code:
Solid useInterval

Both of these actually function identically but this illustrates the difference in the Reactive update model. React uses refs to retain references between executions and has to be aware of stale callbacks. In the reactive case we need to ensure that props.delay doesn't execute early so we wrap it in a function but otherwise the logic is very similar to how you would write this without a framework.

Collapse
 
ferhatavdic profile image
FerhatAvdic • Edited

Why didnt you wrap the inline count function within a useCallback hook?
Also using a state object withing the set method may cause an infinite loop if used within a use effect so you need to use the previous value.

Apply both and u get this:

const incrementCount = useCallback(()=>setCount((prev)=>prev+1),[])

And why not put the callback in the useEffect dependency array?

The function would then look like this:

function useInterval(callback,delay){
useEffect(()=>{
let id = setInterval(callback,delay)
return () => clearInterval(id)
},[delay, callback])
}

But youre still right about the refs. React needs to have the refs explicitly mentioned in the array of dependencies to keep track of changes.

I wonder if it is possible to use Solid within react and im curious if it would make things easier and better.

Edit:
Your approach is great. I actually learned something for future reference. You wrote the code in a way that the next dev who is using the useInterval function doesnt have to think about wrapping their callback function within a useCallback. Thanks!

Thread Thread
 
ryansolid profile image
Ryan Carniato • Edited

Those are fair questions. I took this example directly from Dan Abramov's blog: overreacted.io/making-setinterval-...

I think his motivation was to solve this without using the function form of setState since he could have done that at the beginning of the article and moved on. In any case if you haven't read it, I highly recommend it.

I did make a react-solid-state library but it is basically like MobX. Pre-hooks it felt kind of cool, post-hooks I sort of lost interest in it. I thought React introducing its own primitives was a gamechanger and sure enough things like Recoil started showing up. React can never really leverage the benefits here in terms of execution performance and the DX is not amazing with the need for wrappers etc. There is probably a smarter way to approach it now but as I said limited benefits. Solid fully embraces fine-grained reactivity in a way other libraries don't and I've continued to focus there.

Collapse
 
harvey_triana_1a6e7819b32 profile image
Harvey Triana

About Blazor WebAssembly, github.com/harveytriana/SomethingR...