Closed trusktr closed 5 years ago
Oh wait, I missed the Signals
doc. EDIT: nvm, it doesn't show using inline signals. Continuing reading...
Interesting, in the Mutability doc, you basically said it yourself:
It really comes down to the fact that in most systems once you exit a certain boundary/domain you are better off/only able to pass data by messages rather than by reference. And at that point all the performance gained by mutability is essential lost.
When to using Immutable State:
- Places where frequent partial updates aren't expected
This one, I will have animations on various props of various elements at any given time, which means lots of frequent partial updates.
Just finished reading the docs. It is awesome, good job on those! It'd be sweet to add the plain S.js signal usage to the Signals page. :)
Let me see if I can gather how to do it from reading ko.jsx and mobx-jsx.
If I understood correctly, I need to make a file similar to https://github.com/ryansolid/mobx-jsx/blob/master/src/index.js, using S.js, and should be good to go.
Or does Solid already have it built in?
EDIT: looks like it is built in already, the line
export const r = createRuntime({wrap: S.makeComputationNode});
The last question is, what does it look like?
This is a Surplus example:
const className = S.data("foo"),
div = <div className={className()}/>;
Judging from the babel-plugin-jsx-dom-expressions doc for createRuntime
, I would pass the data without calling it, like follows?
const className = S.data("foo"),
div = <div className={className}/>;
and then the runtime made with createRuntime
will call it instead? Or do I still need to call it?
Yep, that's it. Just forked the solid-todomvc example and tested it, the parens are not required.
So to modify the TodoMVC header to show seconds since opening the app, it looks like this:
const seconds = S.value(0)
setInterval(() => seconds(seconds() + 1), 1000)
const TodoHeader = ({ addTodo }) =>
<header class='header'>
<h1>todos {seconds}</h1>
...
</header>
Awesome! It's so niiiiiiiice!
It sounds like you answered most of your own questions. The State objects just wrap S.data structures so at it's core everything is S() and S.data(). The renderer isn't specific for using the State object. So as I suggested in the other issue coming up with a new State representation isn't terribly hard. I've been working on 2, (State and ImmutableState). The primary one is Mutable under the hood so it works like a nested tree of S signals ala MobX. It's the one in the benchmark and has great performance. It should work well for your scenario.
The Immutable one similar to ImmerJS in that while it constantly clones and updates it still updates under the same clock mutably (ie it clones the first time then mutates along the same change propagation and then when when settles notifies out). So as far as immutable goes it's still reasonably performant but I haven't focused on it less recently.
The State object is mostly for control flow and my thinking a simpler way for people used to React like interfaces to enter this world of fine grained changes. I sort of purposefully skimmed it a bit in the docs to reduce confusion but I think I could put an example in the Signals section.
As for the parens required, for attributes they should be required as it's possible to pass functions to props so I wouldn't expect the className example above to work. For inserted content between the tags (seconds examples) that should work because returned functions are considered computations. S.js has no isObservable or the like so it's necessary.
I don't feel like I've properly explained this yet still. I think it comes down to the fact that Signals are just functions. So that means it's an S.data etc or any function that contains the execution of an S.data. In so functions need to be handled specially as we can't make any assumptions about them. Nothing makes them a Signal. This holds true for the render tree, operators, pretty much anything around this. It's only the fact there is a computation higher up in the execution stack that means they behave in a certain way.
Someone new to this paradigm might go overkill on making computations. If you only care about d2 these are almost equivalent except example 1 does slightly more work:
const s = S.data();
// 1
const d1 = S(() => s()),
d2 = S(d1);
// 2
const d1 = () => s(),
d2 = S(d1)
What this does mean is you generally have to be pretty conscious of what you are doing under certain scenarios. Like while in your seconds example it works you actually created an extra unnecessary computation by doing it that way. My experience with Knockout over the years which allows the same sort of direct referencing in binding is when to include and when not to include the parens ends up being an endless source small bugs. New devs miss it or someone refactors something adding more conditions to the binding now they have to add the parens but miss it and it always evals as true in a conditional etc.. not important things. I actually like the consistency of always including the parens.
The interesting challenge here in general is that at compiler time we don't know if it is a function being passed in and it isn't until runtime we can make the resolutions. Surplus actually optimizes around this which is why you can't not include the parens, since it looks for them as a decision to wrap the expressions in a function. My Babel plugin instead uses outer double parens (( ))
to indicate no wrap. I want a better way but I haven't found one yet.
Another place where things get interesting is dealing with stores and nested data. Signals are setup for tracking Immutable Data. There is a single getter and setter and once set you can not mutate the value and have it track. However it is inefficient data structures and trees to always track through everything so you nest Signals in Signals etc. It means when moving data from the Store or Service layer you have to map over it. Sometimes this happens in rendering, but other times you need specific context references. This problem is always awkward since automating it usually has a cost and serializing it is always a consideration when trying to pass around the data to places not aware of Signals. The second you try to interface with model data you will hit this problem. I disliked how heavy this was and since Proxies are dynamic I can actually only wrap properties in Signals that are being listened to. It's not an every time I set overhead but rather a small overhead just on initial read. And best of all you don't have those concerns.
From my perspective a lot of specifics when dealing with Signals directly. So the State object and Solid in general is just a wrapper so those sort of details are not a consideration starting out. You can use Solid without being aware of Signals so it's very intentional to expose only the baseline stuff from S directly through the library. I was looking to providing a React-ish experience for fine grained. Where you start simple and grow into it. The only piece of Signals that really expose themselves is the fact that inorder to run operators you need to write selectors which is an idea familiar to the React community. Essentially by writing a function that accesses state properties you get a Signal without writing anything with S. ex..
// Psuedo Signal, AKA a Selector
const s = () => state.firstName + ' ' + state.lastName;
So I'm not sure if it's misleading but I wrote the documentation in a way that the end user would not be aware of Signals even though that is the underpinnings. I'm very familiar with fine grained libraries and I truly believe when you get into the harder problems and optimizations it is a superior representation to say React's lifecycle functions but they are a lot at first as anyone starting with like RxJS will attest to. So it's a bit ambitious, all the performance and power of fine grained yet a very gradual learning curve.
I realized re-reading the original post their may been confusion. Solid State object is mutable. It just has an immutable interface. While there is slight overhead from proxies it's actually ideal for nested trees. It saves you from having to manually map everything with Signals which can be tedious. Signals themselves are immutable atoms. Solid State takes away the concern of having to construct the structure of Signals your self while letting you leverage stuff like destructuring. I've removed the Immutable State experiment since I wasn't really progressing it and it was flawed.
Thanks for all that explanation @ryansolid! I'm still working on other stuff over at http://github.com/trusktr/infamous, but I'm planning to make some example declarative app examples with it soon, and I'd like to use Solid for some of them.
in your seconds example it works you actually created an extra unnecessary computation by doing it that way.
Mind showing how the example would be not adding the extra computation?
It saves you from having to manually map everything with Signals which can be tedious
Can you make a small example of having to "map everything with Signals", vs using the "mutable" state object?
You can scratch that first comment about the extra computation given the way that I've changed the code now. Previously all expressions were wrapped in functions by the compiler which were turned into computations. And if it returned a function in a content insert (not for an attribute where a function is a perfectly valid value to set) like in your example that would create another nested computation. It would work since that's intended, but I've since removed that logic to be explicit. So now nothing is wrapped by default and you need to supply a function to make it get wrapped in a computation. Because it's still a bit verbose for none signals like the state object to write {() => state.value}
I've introduced a syntax of using outer wrapping parens so that you can write {( state.value )}
instead. That being said attributes still behave different given that functions are valid values so I'd like to standardize it a bit better, but that still needs some consideration given the concerns I express above.
As for the wrapping. It is pretty common case, you can even see it in the JS Frameworks Benchmark. Most of the time you are going to get blobs of JSON data from a server. Whether they are normalized or not at some point you going to want to denormalize them to display them in the view. So say you have a format of for a Blog Post:
{
id: 1,
title: 'Some Title',
content: '...'
createdAt: '2018-04-05'
author: {
id: 234,
profileUrl: ''
firstName: 'John',
lastName: 'Smith'
}
comments: [{
id: 56,
content: '...',
author: {
id: 4007,
profileUrl: ''
firstName: 'Janet',
lastName: 'Jameson'
}
}]
}
Let's say the user who happens to be the commentor on a particular blog post clicks in the corner and changes their Profile Pic, but you want to make sure the picture that is beside their comment updates. It won't update unless that nested profileUrl is a Signal.
With a library like React some change to the Store is going to trigger either a full re-render or using some sort of MapStateToProps only the relevant components will re-render. From there essentially mapping happens as you call your render functions as you update the url reference on the img tag in the VDOM as it re-evaluates everything.
With KVO(Kev Value Observables, what Signals are) for this to update you will need to navigate to the specific signal to update. But since Comments are dynamic too they need to be Signals as well or they won't appear. Now it's possible you have these Signals Globally and normalized so it isn't as big of a deal but generally when you end up is with Signals that contain mapped versions of the data with signals for values that contain more mapped objects with Signals. Like you'd put the Post in a Signal, and then make a Signal for the Comments array, and then for each Comment you make a Signal for every field that could update, and the same for each field on the Author. In the end you have a completely different nested parallel data structure the View is working with. To your benefit the changes will be pinpoint and not cause re-evaluation of anything else. But you need to write toJS methods ever to get Raw data back out and the process of pulling out values is a chain of post().comments().author().firstName()
. You will find this pattern over and over again with KVO. It's big part of MobX design decisions and the choices made here.
I find people new to these patterns tend to overwrap everything in Signals. But it's usually not as necessary to be so fine grained and you can get away re-evaluating certain things so there is room for optimization here. It's just unfortunate if you need to tweak your data structure as you make those decisions. Like if you decide to make something dynamic later. Part of that is put the owness of mapping on the components which is generally a pattern I like but it definitely is more work. With the State Objects while it can't quite be optimized it basically only creates Signals for values you are reading in a Computed Context. None of this is a consideration and you get to handle it just like a POJO. The touch point is usually just calling my reconcile method on the immutable data source (Global Store/Server data) you wish to do fine grained change on.
Thanks for the description at https://github.com/ryansolid/solid/issues/2. I was going to make another issue, the topic of which you talked about there.
My use case for custom elements (and libs like this one) is to make element for declaratively describing 3D scenes.
When animating things, I want to try to keep memory from growing as much as possible. I was concerned about the immutable state changes leaving the garbage collector with many objects to collect after updating properties every frame of an animation and thus making the graphics stutter.
(imagine graphically rich web games)
In the other thread you mentioned
Interesting! That makes me curious about using solid with S.js and not using the state API. Can you make a small example snippet of using S.js signals instead of the state API?