DEV Community

Cover image for Thinking Locally with Signals

Thinking Locally with Signals

Ryan Carniato on October 13, 2023

As the creator of SolidJS, I was very influenced by React when designing the library. Despite what people might believe by looking at it, it wasn't...
Collapse
 
artxe2 profile image
Yeom suyun

In many more situations than we might think, programmers have more information than a highly optimized compiler.
Wouldn't it be enough for a framework to open up the possibility of optimization by providing options for programmers to leverage this information when they need it?
For example, if a function is "pure", the result of the function could be cached, but the decision of whether to cache the result should be entirely up to the programmer.

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

Yeah. In essence though you could say that is what React.memo, useMemo, and useCallback. The problem is it isn't really what people want to be thinking about. They ignore performance, then find out that some leaf component is inexplicably re-rendering 8 times on some small mutation. They have no idea how to trace it and they start slapping those on everywhere. Eventually they get it down to 2 and call it a day because they still are missing something. 6 months they come back and realize its running 4 times now because someone did something else in a parent component. And so on.

Models that remove the idea of "optimization mode" and just are more optimal to begin with have some benefits. React started looking into compilers, but you get this with Signals too just by their design.

Collapse
 
artxe2 profile image
Yeom suyun

I do not like React's memo function.
Memo is a function that reduces the cost of V-DOM, but I think frameworks like Solid and Svelte have proven that V-DOM itself is just pure overhead.
The implementation using signals is to say that there will be no performance problems in most cases even without additional optimization for excessive calls in the Limits to Locality of Thinking section.
But when I read the article again, it seems that the original content of the article is the same claim.

Collapse
 
aralroca profile image
Aral Roca

I would like to understand how the signals work to update the dom. The jsx-runtime transforms everything into DOM elements and if one has a Signal the update of the element is registered during the jsx-runtime ? And how is it solved if you use signals for conditional renders?

Collapse
 
ryansolid profile image
Ryan Carniato

In basic:

const elementsToInsert = createMemo(() => {
  return showSignal() ?
    createComponent(A, aProps) :
    createComponent(B, bProps)
})
Enter fullscreen mode Exit fullscreen mode

We transform everything to lazy evaluated expressions so we don't great the DOM elements up front. You can picture we wrap everything in functions.

I tried to explain this a couple years ago.. admin.indepth.dev/solidjs-reactivi...

I think this article isn't the clearest but I try to make it consumable.

Collapse
 
aralroca profile image
Aral Roca • Edited

I figure out reactivity with conditional renders only if are inside a node. I realized one thing that I don't get out of, and that is when there is reactivity mixed with static things. Example:

<div>
  <b>Hello </b>
  {signal() ? 'World' : <><i>and</i> bye</>}
</div>
Enter fullscreen mode Exit fullscreen mode

What would this JSX look like in HyperScript?

h('div', {}, [
  h('b', {}, 'Hello '),
  h(null, {}, () => signal() ? 'World' : [h('i', {}, 'and'), 'bye'])
])
Enter fullscreen mode Exit fullscreen mode

Something like this using h(null)?

I implemented it in this way, however, creation works fine because I can use a documentFragment to append and return, however modification doesn't work well because the editing the documentFragment doesn't update the real DOM... So I imagine that here is necessary to track the parentNode (initial div) and use it during the modification... Or how you solved this?

Thanks a lot for your help @ryansolid

Thread Thread
 
aralroca profile image
Aral Roca

I finally resolved changing the hyperscript children to array, this way I can execute in order knowing the parent:

h('div', {}, [
  ['b', {}, 'Hello '],
  ['', {}, () => signal() ? 'World' : ['i', {}, 'and'], 'bye']
])
Enter fullscreen mode Exit fullscreen mode
Collapse
 
aralroca profile image
Aral Roca

I have managed to implement a draft version to use jsx and signals in web-components. I am very grateful and the article you have passed me helped me to understand the basics of signals better. Thank you very much for the article.

Collapse
 
cheerabbits profile image
chee

for future readers that link is dead now, but the article is available here (with no attribution) angularindepth.com/posts/1289/soli...

or on archive.org here with no css web.archive.org/web/20221005075537...

Collapse
 
titob profile image
Tito • Edited

Getters are an antipattern, things become impossible to track down, the language becomes less powerful (no ability to use destructuring, not in functions arguments and not in objects, etc), things become unpredictable (merged effects = values that come from props are re-evaluated).

The exact code that was causing me problems with the merged effects + props getters is:

width={
  html.clientWidth > html.clientHeight
    ? html.clientHeight - 450
    : html.clientWidth - 450
}
Enter fullscreen mode Exit fullscreen mode

I never expected that code to run more than once. I was shocked.

An isSignal on this context doesn't make sense, it has no value to know if something is a signal (for the value that being a signal provides), in any case you will unwrap the value, context github.com/solidjs/signals/issues/8

About props, you can freeze them with Object.freeze(props)

Anyway, I do love solid and the ideas you came with, signals, enchanted by dom-expressions output, just feeling pity it crossed the line with the props getter stuff, but that's just my preference. Thanks Ryan!, great source of inspiration

Collapse
 
ryansolid profile image
Ryan Carniato

Yeah it definitely pushes against the language a bit, although using objects of functions for props is also awkward because shape changing... can't really spread test the existence of something in a reactive way. Ie.. track something that isn't there yet. Not having isSignal has been a blessing though so I doubt we'd ever expose that as an external API. Which is difficult because if we are building a core library that people might use directly hiding it might be challenging if it was included. So while there are a couple places I wouldn't mind internally leveraging the optimization it just can't exist out there so will have to see what can be done.

Collapse
 
dsaga profile image
Dusan Petkovic

Thanks for the article, still trying to wrap my mind around how signals work..

How does a global state fit with locally of thinking? any disadvantage or using signals as a global state?

Collapse
 
ryansolid profile image
Ryan Carniato

You are right in that is the 3rd boundary point into our component. I didn't talk about it much in the article. It is really a 3rd category as you have a bit of both qualities as it is a lot like props where you have data and explicit mutators coming in, but you don't get to define the interface(the global store does) similar to how it is when you consume child components.

If the interface is well defined then in a sense this is no different than any composition pattern in components you use. Like custom Hooks/Primitives. Whether you own the state or created doesn't impact the mechanics of the component.

const [count, setCount] = createSignal(0);
// vs global lookup via context
const [count, setCount] = useContext(CounterContext);
Enter fullscreen mode Exit fullscreen mode

However, since you do not know the side effects of your actions the recommendation for global state design it is generally encouraged to be as specific as possible with mutation API. For instance this is better:

const [count, { increment, decrement }] = useContext(CounterContext);
Enter fullscreen mode Exit fullscreen mode

In general Signals(or Solid's Stores which are nested Proxy Signals) are great for global state. Most global state management libraries are some sort of event emitters and this one plays first party with the framework. But given above I do recommend not just exposing the setters directly if possible, and design an explicit mutation API.

Collapse
 
dsaga profile image
Dusan Petkovic

Thanks!! will try out solid, interested to see how it works

Collapse
 
btakita profile image
Brian Takita

How is development on Solid 2.0 with global signals going? Looking forward to using signals, memos, & resources for global state management!

Collapse
 
mfp22 profile image
Mike Pearson • Edited

React has passed many values down over the years. Probably hundreds of trillions. And I hope we learn from all of them.

Btw, unidirectionality meant a lot more than just values being passed down. Flux was the unidirectional thing when they first described it, and React was described as a "declarative rendering framework." Declarative rendering with JSX is unidirectional, but it's kind of boring. What's interesting is the full cycle from event to DOM render. That's what MVC vs unidirectionality was about. Without something like Flux, React is an MVC framework. People forget the origins, because most apps require one data source and a few trivial state changes, so it doesn't matter. But React and Flux were designed in an environment that was much more complex than most applications: a chat app. The way most React apps are architected today is not much better than what came before React. It's event handlers controlling various states. JSX and components are the real improvement that stuck.

Collapse
 
mfp22 profile image
Mike Pearson

@ryansolid if you can watch my UtahJS talk and come away still not agreeing that React is an MVC framework, I'll give you the gift card they gave me for speaking. youtu.be/yCrrkBV4-K8?si=uxMbSQv-2q...