I'm not sure I understood this article. I think we're talking about different things. The responses you wrote don't correspond to what I meant to say. I'll try to clarify.
The problem that React "fixes" is inconsistent UI between initialization and updates. Before React, you would typically write some logic for initialization of some UI, and write a separate piece of logic that would update that UI to match the latest state. Like you did with the imperative manual button example. Of course, the problem with this is that they get out of sync.
The key insight is that the user doesn't care if the component has just appeared or if it updated. The end result is supposed to be the same. So we want to somehow get to the "end result" based on the current information. For React, this means re-running your component function and figuring out how to update the UI to match that. For Solid, this means re-running the things embedded in your template.
So far so good—but not quite.
The interesting question to me is where do you place the rendering logic. The kind of logic that governs what your component displays. Your example omits any realistic logic, so let's consider this React component:
// Working version in ReactfunctionVideoList({videos,emptyHeading}){constcount=videos.length;letheading=emptyHeading;if (count>0){constnoun=count>1?'Videos':'Video';heading=count+''+noun;}return<h1>{heading}</h1>
}
The big idea in React is that you should be able to write code like this.
You should be able to write rendering logic that takes some data, makes some decisions, formats some inputs, and then embeds them into the template. And that logic should not describe initialization only — it should run on updates too!
The user doesn't care whether it's initialization or update. The user just wants to see the latest state — whatever it is — reflected on the screen. That's what React "fixed": it made rendering logic reactive by default. Whenever you write rendering logic in React, you can be confident that it won't get out of sync with the data you pass.
Solid follows a different strategy. Only "holes" in the template are reactive. As a result, code like this doesn't work in Solid (even if I access stuff via props.something):
// Broken version in SolidfunctionVideoList(props){constcount=props.videos.length;// never updates in Solid letheading=props.emptyHeading;// never updates in Solidif (count>0){constnoun=count>1?"Videos":"Video";heading=count+""+noun;}return<h1>{heading()}</h1>
}
Yes, I get the point, it's not supposed to work in Solid because things execute once. Since the linter warns about this code, it's fair to say that this is not a valid example for me to complain about.
What is the canonical way to structure this code instead? From my understanding (I might be wrong), the idiomatic Solid code would be:
// Working version in SolidfunctionVideoList(props){constcount=()=>props.videos.length;constheading=()=>{if (count()>0){constnoun=count()>1?"Videos":"Video";returncount()+""+noun;}else{returnemptyHeading;}}return<h1>{heading()}</h1>
}
This reads pretty nicely, but note that with Solid, I've had to restructure the code around each value instead of relying on the control flow of the outer function.
Suppose I wanted to add more rendering logic into count > 0 condition (for example, to determine some other variable). In React, I would put it inside the if statement I already have:
// Working version in ReactfunctionVideoList({videos,emptyHeading}){constcount=videos.length;letheading=emptyHeading;letsomethingElse=42;// can add something hereif (count>0){constnoun=count>1?'Videos':'Video';heading=count+''+noun;somethingElse=someOtherStuff();// can add something here too}return (<><h1>{heading}</h1>
<h2>{somethingElse}</h2>
</>
);}
In Solid, I don't know how to do this other than to duplicate the if statement again inside a new const somethingElse = () => { /* my logic here */ } binding:
// Working version in SolidfunctionVideoList(props){constcount=()=>props.videos.length;constheading=()=>{if (count()>0){// can't add the logic here :(constnoun=count()>1?"Videos":"Video";returncount()+""+noun;}else{returnemptyHeading;}}constsomethingElse=()=>{if (count()>0){// let's put this here i guessreturnsomeOtherStuff();}else{return42;}});return (<><h1>{heading()}</h1>
<h2>{somethingElse()}</h2>
</>
);}
I get that there are ways to compress this specific example to be shorter, but my point is that you can't rely on top-level control flow for any of your rendering logic. You have to group things by individual values. (Or you could return multiple things from heading and rename it to model or something.) Another solution in Solid is using DSL like <When>. The downside of that is now you can't easily move the computation back into JS when you want to read its result inside an event handler — for example, to create similar branching logic.
That is the point I was trying to make.
In Solid, only your template (and things explicitly referenced from it) re-executes. So putting rendering logic into the top-level component body is a mistake. You can't use top-level control flow or read values at the top level because that would undo the "fix": your initialization logic would diverge from your update logic. The linter flags it.
In React, all your rendering logic is your "template". This lets you use if statements and control flow without regrouping your code around every value you render. This also ensures that the user always sees fresh values. That's what I meant by React not "missing" updates. React doesn't let you write rendering logic that leaves initialization and updates out of sync.
The benefit of the Solid approach is that you can avoid re-executing parts of that logic because you've structured the code around the values (rather than around the control flow). Similar to how you have to restructure your code around the values when you optimize it with useMemo. But for us, this isn't the desirable end state.
With the compiler, the goal is to be able to write code without regrouping it:
// Working version in React (again)functionVideoList({videos,emptyHeading}){constcount=videos.length;letheading=emptyHeading;letsomethingElse=42;if (count>0){constnoun=count>1?'Videos':'Video';heading=count+''+noun;somethingElse=someOtherStuff();}return (<><h1>{heading}</h1>
<h2>{somethingElse}</h2>
</>
);}
And then the compiler would figure out the "groups" so that it has comparable performance to useMemo or something even more fine-grained. That's the hard part, but hopefully this helps explain the vision.
Coming back to your final code example. Remove useMemo, and the React code still works. Would the Knockout example still work if you did not mark this as pureComputed()? This seems a bit similar to how Solid examples require you to wrap things.
The beauty of React is that making things "computed" is an optimization, not a requirement. An optimization we can eventually put under the hood. React does not require you to write rendering logic inside-out just to get things to update. In React, everything is reactive by default. That's what I was trying to convey — perhaps poorly.
Apologies if something is incorrect in my post. It's a bit hard to write these responses impromptu. But I figured I should since my tweets were pulled here. :) Appreciate the conversation.
React approach: let's write imperative code, calling it pure functions, which are actually not pure at all, since we will use hooks to store state and emulate object decomposition, without mentioning the word "class", and by default we will have a bunch of re-renderers for every sneeze, launching all these imperative completeness calculations to form a new tree of objects, which we will compare deeply with the previous version to find out what needs to change one text node in the corner of the screen, and let developers from all over the world struggle with these re-renders by placing even more hooks, providing them with manually placed dependencies, and wonder why re-renders still happen when they don't need to, but now they don't happen, the code needs to.
The Solid approach: Let's just write reactive invariants and not think about dependencies, re-renders, optimizations, but still get an effective update of the state graph, but for each of the 100 state changes per animation frame, we will synchronously update the entire application in general, even if some states are not needed for rendering now, and the results of intermediate the renderer will not be able to physically see the renderers.
Well, I don't even know who to choose.. Don't you want to think about a declarative description of the composition of components and data flows between them, without manual state managing?
This is the full application code (see in the beta of sandbox which build UI through reflect components declarations), self - sufficient, but controlled, with l10n, detailed style/behavior/composition customizations, error boundaries everywhere, suspense, automatic waiting indicators, a lot of extension points/slots/props, inversion of control, tested dependencies, virtual rendering etc. In 6 lines of code! Yes, there is a two-way data flow. And no, it doesn't cause any problems at all, since the state is stored only in one place and is used through delegates, and not copied to a bunch of props and states.
Here I was telling how to fix most of the problems of React and JSX, if I was suddenly forced to develop on this, but no one heard me. Fortunately, I don't need to mimic HTML in order to use and customize the components as my heart desires.
Guys, developers from all over the world are listening to you. Why don't you move the industry somewhere in this direction, instead competing in how to write trivial logic in the most non-trivial way and condemning them to write dozens of times more code in real applications?
Jobs will not disappear anywhere, but work tasks will become much more interesting. Instead of spending 3 months screwing the spinner (the real story from GitLab), you could at the same time completely rewrite the output of the commit code so that it does not slow down.
I appreciate you taking the time to write such a thorough response. I misunderstood your grief with setup + reactivity approach I thought it was due to immutability. I never would associate it with update correctness as once you write things in a derived way you don't tend to have those problems.
You've definitely making me think of scenarios where derived data might take more thought to write. I don't think anyone would want code written like that in the example if avoidable. And when it does happens it happens.
The crux of it is that in some cases it is easier to group around control flow rather than data flow. Data flow is a must for memoization which is why this same refactor occurs today in React if you want to use useMemo. You have the same dilema of where to put count > 0 because you can't conditionally wrap Hooks. In Solid you always write as if the data is derived so you don't refactor to optimize.
The refactor story is add a one liner for somethingElse and move on. Or extract out count() > 0 if you really want to.
The thing is any of the logic (other than early return, although I could show what an early return would map to if you like) you could just hoist into a function or memo. So you could just take your React code and now it's in the pure space. It's like a React component inside a Solid one.
As I said our community is full of trolls. You should recognize the wrapped code in this memo from your useInterval article.
gotta love how easy it is to make a counter work using Solid hooks 😍
18:13 PM - 25 Jan 2023
Back to the example if you were being lazy you could do this as well but I don't think this is where you'd want to end up:
functionVideoList(props){conststate=createMemo(()=>{// the react component bodyconstcount=props.videos.length;letheading=props.emptyHeading;letsomethingElse=42;// can add something hereif(count>0){constnoun=count>1?'Videos':'Video';heading=count+''+noun;somethingElse=someOtherStuff();// can add something here too}return{heading,somethingElse}});return(<><h1>{state().heading}</h1>
<h2>{state().somethingElse}</h2>
</>
);}
I think the goal of the compiler is really cool. I'm excited to see you guys get there and honestly I'm inspired to go further down this path myself. I wrote about it a while ago but you guys make me want to look at again.
EDIT: The more I look at this though isn't the data derived writing just clearer than the control flow version anyway. It's definitely more declarative feeling and more refactorable. Like let me port my Solid version to one without closures.
This makes me wonder.. if you are going to be smart on re-execution with React Forget are you going to forbade things like console.logs or other side effects in the main component body to prevent people from observing that the code doesn't exactly run top down? The compiler sounds good but I imagine traceability gets even harder.
Yeah, so with your final example we're essentially back to React-land.
I.e. if my entire function body is there, it always re-executes. But I thought Solid was supposed to help me be more granular! :) So we don't get the benefits of Solid here and also the syntax is more clunky (an extra indent level). Especially if aside from heading we're gonna need a bunch of other things. My JSX can't just access those values from the closure scope. Weren't we inside of a function?
If I try to make it granular again by splitting it up, then as you said it starts to look a lot like React with useMemo's. You still have to think about how to regroup things (and whether to split them up or unify them again) each time you change something. I guess it's par for the course in traditional reactive approaches, but after using React this feels like a step back. Why does everything need to be wrapped? Why can't we use the top level function body for its purpose?
That's what we're hoping to solve. Write plain logic, write it at the top level, and let the compiler figure out how to group it. Is this doable? We don't know yet whether our approach is completely solid, but we hope to share an update soon.
Regarding Hooks, note React doesn't place restrictions on your rendering logic. The restriction is on calling React built-in functions. Although in principle, if we were compiling code, we could remove this restriction, I don't know if it makes sense to. Say you want to useState inside an if. What is this supposed to mean? Does it mean this state gets reset whenever the condition flips back to false? Or does it mean that the state persists? It's a bit similar to saying you want to conditionally define a method on a class. Even if you could, it's just not clear what you actually mean by that. The rules of Hooks are restrictive but in most cases I'd say they help do the right thing and have clear semantics (the lifetime is always tied to the surrounding component). And importantly, they don't tell you how to write your code — everything after can have arbitrary control flow.
Right. My last example was just showing the bail out so to speak. This is how a lot React ports look like at first. I just wanted to show you could do things both ways. You return all the fields you need and you sacrifice some granular execution so it works. You could also nest Memos and not lose granularity too.
And yes once you go to useMemo breaking it out we are in the same boat. But what I'm getting at is if you start by writing things as if they are useMemo (whether they memo or not) I'm not sure how much you are bothered by this. I suppose there might be some duplication. I don't think it fundamentally impacts correctness if you are thinking in data. It definitely pushes you towards writing data as being derived at which point being a function or not is sort of whatever.
And like there are other things that you aren't worried about. Because like things like useCallback etc don't exist. Things like memoizing functions are much less common. Like communication between Effects and stable references ...memoizing components, these are all not concepts. Instead you have a model where you feel like you control and know exactly what updates. I'm sure we could pick out more slightly more awkward scenarios for each but to what end.
I don't really agree this is a clear step backwards. But my React experience on the job is much more limited than yours. I wrote and supported a React Native app for 3 years, and only have about 1 year experience doing React on the web(same company), doing a full rewrite of an private social media application (like Instagram for schools). I am not unfamiliar with developing React apps.
When my team moved from Knockout to React Hooks they were really confused. They did a lot of things wrong. They thought they were guarding executions and were getting re-renders. They figured it out but the overall sentiment was, Hooks weren't what I was expecting. They are fine. I thought we'd get some big win here, but maybe we should have just given Solid a try (this was 4 years ago so I didn't recommend we use Solid yet). So to me it is very much a matter of perspective.
Aside I have no issue with Hooks I think their design makes sense. I've never felt Hook rules were restrictive other than not being able to nest them within themselves. I think stale closures are confusing as complexity grows, ie.. if you need to useRef for something other than a DOM node you've hit a point that goes beyond where most comfort is. I only mentioned the rules from the perspective that our useMemo examples would be nearly identical. Unless we are doing some fancy stuff.
Yeah that’s pretty interesting. I haven’t thought about it from this perspective. To me Hooks are very functional in the classical sense. They’re a universe apart from Knockout style, so I was surprised by your mention of trading away the pure model. Hooks do not trade it away, they’re the clearest representation of that model. But I can see now how superficially they might look like some Knockout-style code. That might explain why people sometimes really struggle with them. Guess that’s similar to Solid code looking deceptively Reacty — when it’s really not. That’s a useful insight.
The examples above are very very simple. They are complete beginner examples, and don't really show where things get either a lot more complex, or way simpler. When you really use both React and Solid, then you'll see which is simpler as app requirements grow.
Here's just one simple example with pure Solid that I challenge anyone to write in pure React without importing additional libraries and with the same simplicity:
This was an interesting challenge, as I could see lots of ways of building this in React depending on which parts of the above code were considered critical.
The big difference with the above Solid code is that this moves the state handling into a top level React component so that React will re-render our components when the state changes.
We also need to wrap the setInterval call in a useEffect in order to kick off the interval from a React component.
You don't technically even need the top level App component or the state...
constOne=({count})=>{return<div>value in One: {count}</div>}constTwo=({count})=>{return<div>value in Two: {count*2}</div>}constroot=React.createRoot(document.getElementById("app"));letcount=0;setInterval(()=>{count++;root.render(<><Onecount={count}/><Twocount={count}/></>,);},500);
This code looks very simple and concise. But this is incorrect code, since the timer running will never stop. It's a time bomb. If you write this code correctly with the timer reset when removing the component, then the code will turn out to be not so concise at all. And I am silent about the fact that this code ticks not 2 times per second, but at unpredictable intervals of time, which introduces a progressive systematic error. To avoid this, you need to measure the time that has elapsed since the timer started.
In $moll there is a special store ticking with a given accuracy for this. Example:
Don’t mind. I’ve worked at Meta and I can count on one hand the number of times I saw people using let in component rendering code (not inside a callback). I always request a refactor during code reviews when I see it. Dan was just using contrived examples to make his point.
I fell in love with React because you write beautiful, simple, clear, declarative code. However once your webapp grows you start using memos, bringing state as close to children as possible, useCallback, aka restructuring your code, I wish I didn't have to resort to that.
FWIW even if I was using React and someone submitted that code, I'd tell them to restructure to make it look more like the Solid verison
It was extremely hard for me to understand what the nested if statement mutating variables outside of it was doing - it's the type of thing I always try to avoid unless I'm forced into - which is pretty rare.
So you can view Solid forcing you to regroup things as a problem - or you can view it as a feature that teaches you how to write more readable code. And if you added a memo const hasMany = createMemo(() => videos.length > 0) you'd be making it more readable and performant at the same time
The trade-off being expressed here is in React's model all styles of programming make sense given the render function approach. And in the Solid world a more specific style is required (one that I think is better in the majority of situations)
Both React and Solid limit what people can do, and to the extent that we like other things about both respective designs, we’ll probably both find a way to rationalize it. I do find it difficult to believe that someone might start designing with "I want features like local variables or conditional local mutation not to work". Yes, it's tolerable and arguments could be made about how that's better code anyway (unless it's one of those cases where it's actually super annoying because you have to restructure a bunch of stuff and can't use the tools already built into the language). But one could of course make a similar argument about Hooks being conditional, or gotchas related to Effects. And we'd say, well, yeah this is confusing, but it forces you to express the intent more clearly, or say to confront some edge cases early. And so, with some practice, it leads to better code too. The thing about better is it's pretty subjective. That's the fun of it, I guess.
I think what is swaying me so far is not necessarily the ergonomics, but efficiency. Intuitively, React to me feels like an "immediate mode" UI, where each time a node in the tree needs to re-render, all children process their hooks to determine what effects and derivations need to reprocess. Solid's approach on the other hand resembles "retained" UI architectures, where a component function describes data dependencies and returns a node to add to a tree. In fact, the usage of the VDOM in React makes sense in this light, since most immediate rendering architectures also rely on shadow state for efficiency. Both approaches are valid, to be sure, with different sets of tradeoffs, but as mentioned, I gravitate a bit more to the Solid approach to doing things. My personal bias is likely my experience as a graphics programmer operating in a primarily C++ world though.
I wonder if I am using some different kind of React than Dan is :)))
"The user doesn't care whether it's initialization or update. " --So why am I keeping thinking of "should I need useCallback here?"
"That's what I meant by React not "missing" updates. React doesn't let you write rendering logic that leaves initialization and updates out of sync. " --So I have to fill the annoying dep arrays again and again to achieve this.
;(((
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
I'm not sure I understood this article. I think we're talking about different things. The responses you wrote don't correspond to what I meant to say. I'll try to clarify.
The problem that React "fixes" is inconsistent UI between initialization and updates. Before React, you would typically write some logic for initialization of some UI, and write a separate piece of logic that would update that UI to match the latest state. Like you did with the imperative manual button example. Of course, the problem with this is that they get out of sync.
The key insight is that the user doesn't care if the component has just appeared or if it updated. The end result is supposed to be the same. So we want to somehow get to the "end result" based on the current information. For React, this means re-running your component function and figuring out how to update the UI to match that. For Solid, this means re-running the things embedded in your template.
So far so good—but not quite.
The interesting question to me is where do you place the rendering logic. The kind of logic that governs what your component displays. Your example omits any realistic logic, so let's consider this React component:
The big idea in React is that you should be able to write code like this.
You should be able to write rendering logic that takes some data, makes some decisions, formats some inputs, and then embeds them into the template. And that logic should not describe initialization only — it should run on updates too!
The user doesn't care whether it's initialization or update. The user just wants to see the latest state — whatever it is — reflected on the screen. That's what React "fixed": it made rendering logic reactive by default. Whenever you write rendering logic in React, you can be confident that it won't get out of sync with the data you pass.
Solid follows a different strategy. Only "holes" in the template are reactive. As a result, code like this doesn't work in Solid (even if I access stuff via
props.something
):Yes, I get the point, it's not supposed to work in Solid because things execute once. Since the linter warns about this code, it's fair to say that this is not a valid example for me to complain about.
What is the canonical way to structure this code instead? From my understanding (I might be wrong), the idiomatic Solid code would be:
This reads pretty nicely, but note that with Solid, I've had to restructure the code around each value instead of relying on the control flow of the outer function.
Suppose I wanted to add more rendering logic into
count > 0
condition (for example, to determine some other variable). In React, I would put it inside theif
statement I already have:In Solid, I don't know how to do this other than to duplicate the
if
statement again inside a newconst somethingElse = () => { /* my logic here */ }
binding:I get that there are ways to compress this specific example to be shorter, but my point is that you can't rely on top-level control flow for any of your rendering logic. You have to group things by individual values. (Or you could return multiple things from
heading
and rename it tomodel
or something.) Another solution in Solid is using DSL like<When>
. The downside of that is now you can't easily move the computation back into JS when you want to read its result inside an event handler — for example, to create similar branching logic.That is the point I was trying to make.
In Solid, only your template (and things explicitly referenced from it) re-executes. So putting rendering logic into the top-level component body is a mistake. You can't use top-level control flow or read values at the top level because that would undo the "fix": your initialization logic would diverge from your update logic. The linter flags it.
In React, all your rendering logic is your "template". This lets you use
if
statements and control flow without regrouping your code around every value you render. This also ensures that the user always sees fresh values. That's what I meant by React not "missing" updates. React doesn't let you write rendering logic that leaves initialization and updates out of sync.The benefit of the Solid approach is that you can avoid re-executing parts of that logic because you've structured the code around the values (rather than around the control flow). Similar to how you have to restructure your code around the values when you optimize it with
useMemo
. But for us, this isn't the desirable end state.With the compiler, the goal is to be able to write code without regrouping it:
And then the compiler would figure out the "groups" so that it has comparable performance to
useMemo
or something even more fine-grained. That's the hard part, but hopefully this helps explain the vision.Coming back to your final code example. Remove
useMemo
, and the React code still works. Would the Knockout example still work if you did not mark this aspureComputed()
? This seems a bit similar to how Solid examples require you to wrap things.The beauty of React is that making things "computed" is an optimization, not a requirement. An optimization we can eventually put under the hood. React does not require you to write rendering logic inside-out just to get things to update. In React, everything is reactive by default. That's what I was trying to convey — perhaps poorly.
Apologies if something is incorrect in my post. It's a bit hard to write these responses impromptu. But I figured I should since my tweets were pulled here. :) Appreciate the conversation.
React approach: let's write imperative code, calling it pure functions, which are actually not pure at all, since we will use hooks to store state and emulate object decomposition, without mentioning the word "class", and by default we will have a bunch of re-renderers for every sneeze, launching all these imperative completeness calculations to form a new tree of objects, which we will compare deeply with the previous version to find out what needs to change one text node in the corner of the screen, and let developers from all over the world struggle with these re-renders by placing even more hooks, providing them with manually placed dependencies, and wonder why re-renders still happen when they don't need to, but now they don't happen, the code needs to.
The Solid approach: Let's just write reactive invariants and not think about dependencies, re-renders, optimizations, but still get an effective update of the state graph, but for each of the 100 state changes per animation frame, we will synchronously update the entire application in general, even if some states are not needed for rendering now, and the results of intermediate the renderer will not be able to physically see the renderers.
Well, I don't even know who to choose.. Don't you want to think about a declarative description of the composition of components and data flows between them, without manual state managing?
This is the full application code (see in the beta of sandbox which build UI through reflect components declarations), self - sufficient, but controlled, with l10n, detailed style/behavior/composition customizations, error boundaries everywhere, suspense, automatic waiting indicators, a lot of extension points/slots/props, inversion of control, tested dependencies, virtual rendering etc. In 6 lines of code! Yes, there is a two-way data flow. And no, it doesn't cause any problems at all, since the state is stored only in one place and is used through delegates, and not copied to a bunch of props and states.
Here I was telling how to fix most of the problems of React and JSX, if I was suddenly forced to develop on this, but no one heard me. Fortunately, I don't need to mimic HTML in order to use and customize the components as my heart desires.
Guys, developers from all over the world are listening to you. Why don't you move the industry somewhere in this direction, instead competing in how to write trivial logic in the most non-trivial way and condemning them to write dozens of times more code in real applications?
The more imperative code we write, the more jobs we create) Hey, why don't you take a walk down the road to nowhere, buddy))
Jobs will not disappear anywhere, but work tasks will become much more interesting. Instead of spending 3 months screwing the spinner (the real story from GitLab), you could at the same time completely rewrite the output of the commit code so that it does not slow down.
Your answer sounds like the words of a fool. Is that on purpose?
Here you can compare different syntaxes.
"referential transparency" that functional programming languages respect.
I appreciate you taking the time to write such a thorough response. I misunderstood your grief with setup + reactivity approach I thought it was due to immutability. I never would associate it with update correctness as once you write things in a derived way you don't tend to have those problems.
You've definitely making me think of scenarios where derived data might take more thought to write. I don't think anyone would want code written like that in the example if avoidable. And when it does happens it happens.
The crux of it is that in some cases it is easier to group around control flow rather than data flow. Data flow is a must for memoization which is why this same refactor occurs today in React if you want to use
useMemo
. You have the same dilema of where to putcount > 0
because you can't conditionally wrap Hooks. In Solid you always write as if the data is derived so you don't refactor to optimize.I'd probably write this and not worry about it:
The refactor story is add a one liner for
somethingElse
and move on. Or extract outcount() > 0
if you really want to.The thing is any of the logic (other than early return, although I could show what an early return would map to if you like) you could just hoist into a function or memo. So you could just take your React code and now it's in the pure space. It's like a React component inside a Solid one.
As I said our community is full of trolls. You should recognize the wrapped code in this memo from your
useInterval
article.Back to the example if you were being lazy you could do this as well but I don't think this is where you'd want to end up:
I think the goal of the compiler is really cool. I'm excited to see you guys get there and honestly I'm inspired to go further down this path myself. I wrote about it a while ago but you guys make me want to look at again.
EDIT: The more I look at this though isn't the data derived writing just clearer than the control flow version anyway. It's definitely more declarative feeling and more refactorable. Like let me port my Solid version to one without closures.
This makes me wonder.. if you are going to be smart on re-execution with React Forget are you going to forbade things like
console.logs
or other side effects in the main component body to prevent people from observing that the code doesn't exactly run top down? The compiler sounds good but I imagine traceability gets even harder.Yeah, so with your final example we're essentially back to React-land.
I.e. if my entire function body is there, it always re-executes. But I thought Solid was supposed to help me be more granular! :) So we don't get the benefits of Solid here and also the syntax is more clunky (an extra indent level). Especially if aside from
heading
we're gonna need a bunch of other things. My JSX can't just access those values from the closure scope. Weren't we inside of a function?If I try to make it granular again by splitting it up, then as you said it starts to look a lot like React with
useMemo
's. You still have to think about how to regroup things (and whether to split them up or unify them again) each time you change something. I guess it's par for the course in traditional reactive approaches, but after using React this feels like a step back. Why does everything need to be wrapped? Why can't we use the top level function body for its purpose?That's what we're hoping to solve. Write plain logic, write it at the top level, and let the compiler figure out how to group it. Is this doable? We don't know yet whether our approach is completely solid, but we hope to share an update soon.
Regarding Hooks, note React doesn't place restrictions on your rendering logic. The restriction is on calling React built-in functions. Although in principle, if we were compiling code, we could remove this restriction, I don't know if it makes sense to. Say you want to
useState
inside anif
. What is this supposed to mean? Does it mean this state gets reset whenever the condition flips back tofalse
? Or does it mean that the state persists? It's a bit similar to saying you want to conditionally define a method on a class. Even if you could, it's just not clear what you actually mean by that. The rules of Hooks are restrictive but in most cases I'd say they help do the right thing and have clear semantics (the lifetime is always tied to the surrounding component). And importantly, they don't tell you how to write your code — everything after can have arbitrary control flow.Right. My last example was just showing the bail out so to speak. This is how a lot React ports look like at first. I just wanted to show you could do things both ways. You return all the fields you need and you sacrifice some granular execution so it works. You could also nest Memos and not lose granularity too.
And yes once you go to useMemo breaking it out we are in the same boat. But what I'm getting at is if you start by writing things as if they are useMemo (whether they memo or not) I'm not sure how much you are bothered by this. I suppose there might be some duplication. I don't think it fundamentally impacts correctness if you are thinking in data. It definitely pushes you towards writing data as being derived at which point being a function or not is sort of whatever.
And like there are other things that you aren't worried about. Because like things like
useCallback
etc don't exist. Things like memoizing functions are much less common. Like communication between Effects and stable references ...memoizing components, these are all not concepts. Instead you have a model where you feel like you control and know exactly what updates. I'm sure we could pick out more slightly more awkward scenarios for each but to what end.I don't really agree this is a clear step backwards. But my React experience on the job is much more limited than yours. I wrote and supported a React Native app for 3 years, and only have about 1 year experience doing React on the web(same company), doing a full rewrite of an private social media application (like Instagram for schools). I am not unfamiliar with developing React apps.
When my team moved from Knockout to React Hooks they were really confused. They did a lot of things wrong. They thought they were guarding executions and were getting re-renders. They figured it out but the overall sentiment was, Hooks weren't what I was expecting. They are fine. I thought we'd get some big win here, but maybe we should have just given Solid a try (this was 4 years ago so I didn't recommend we use Solid yet). So to me it is very much a matter of perspective.
Aside I have no issue with Hooks I think their design makes sense. I've never felt Hook rules were restrictive other than not being able to nest them within themselves. I think stale closures are confusing as complexity grows, ie.. if you need to
useRef
for something other than a DOM node you've hit a point that goes beyond where most comfort is. I only mentioned the rules from the perspective that ouruseMemo
examples would be nearly identical. Unless we are doing some fancy stuff.Yeah that’s pretty interesting. I haven’t thought about it from this perspective. To me Hooks are very functional in the classical sense. They’re a universe apart from Knockout style, so I was surprised by your mention of trading away the pure model. Hooks do not trade it away, they’re the clearest representation of that model. But I can see now how superficially they might look like some Knockout-style code. That might explain why people sometimes really struggle with them. Guess that’s similar to Solid code looking deceptively Reacty — when it’s really not. That’s a useful insight.
The examples above are very very simple. They are complete beginner examples, and don't really show where things get either a lot more complex, or way simpler. When you really use both React and Solid, then you'll see which is simpler as app requirements grow.
Here's just one simple example with pure Solid that I challenge anyone to write in pure React without importing additional libraries and with the same simplicity:
Solid playground example
Example on CodePen with no build tools:
This was an interesting challenge, as I could see lots of ways of building this in React depending on which parts of the above code were considered critical.
The most natural way I would write it is:
codepen.io/karlokeeffe/pen/vYzxPEX
The big difference with the above Solid code is that this moves the state handling into a top level React component so that React will re-render our components when the state changes.
We also need to wrap the
setInterval
call in auseEffect
in order to kick off the interval from a React component.You don't technically even need the top level App component or the state...
This thread is crazy.
Impure solidjs components being rebuilt using pure react components.
Let's turn this into a different challenge: write the code and the tests for each of these in solid and react
This code looks very simple and concise. But this is incorrect code, since the timer running will never stop. It's a time bomb. If you write this code correctly with the timer reset when removing the component, then the code will turn out to be not so concise at all. And I am silent about the fact that this code ticks not 2 times per second, but at unpredictable intervals of time, which introduces a progressive systematic error. To avoid this, you need to measure the time that has elapsed since the timer started.
In $moll there is a special store ticking with a given accuracy for this. Example:
Don’t mind. I’ve worked at Meta and I can count on one hand the number of times I saw people using let in component rendering code (not inside a callback). I always request a refactor during code reviews when I see it. Dan was just using contrived examples to make his point.
I fell in love with React because you write beautiful, simple, clear, declarative code. However once your webapp grows you start using memos, bringing state as close to children as possible, useCallback, aka restructuring your code, I wish I didn't have to resort to that.
FWIW even if I was using React and someone submitted that code, I'd tell them to restructure to make it look more like the Solid verison
It was extremely hard for me to understand what the nested if statement mutating variables outside of it was doing - it's the type of thing I always try to avoid unless I'm forced into - which is pretty rare.
So you can view Solid forcing you to regroup things as a problem - or you can view it as a feature that teaches you how to write more readable code. And if you added a memo
const hasMany = createMemo(() => videos.length > 0)
you'd be making it more readable and performant at the same timeThe trade-off being expressed here is in React's model all styles of programming make sense given the render function approach. And in the Solid world a more specific style is required (one that I think is better in the majority of situations)
Both React and Solid limit what people can do, and to the extent that we like other things about both respective designs, we’ll probably both find a way to rationalize it. I do find it difficult to believe that someone might start designing with "I want features like local variables or conditional local mutation not to work". Yes, it's tolerable and arguments could be made about how that's better code anyway (unless it's one of those cases where it's actually super annoying because you have to restructure a bunch of stuff and can't use the tools already built into the language). But one could of course make a similar argument about Hooks being conditional, or gotchas related to Effects. And we'd say, well, yeah this is confusing, but it forces you to express the intent more clearly, or say to confront some edge cases early. And so, with some practice, it leads to better code too. The thing about better is it's pretty subjective. That's the fun of it, I guess.
I think what is swaying me so far is not necessarily the ergonomics, but efficiency. Intuitively, React to me feels like an "immediate mode" UI, where each time a node in the tree needs to re-render, all children process their hooks to determine what effects and derivations need to reprocess. Solid's approach on the other hand resembles "retained" UI architectures, where a component function describes data dependencies and returns a node to add to a tree. In fact, the usage of the VDOM in React makes sense in this light, since most immediate rendering architectures also rely on shadow state for efficiency. Both approaches are valid, to be sure, with different sets of tradeoffs, but as mentioned, I gravitate a bit more to the Solid approach to doing things. My personal bias is likely my experience as a graphics programmer operating in a primarily C++ world though.
I wonder if I am using some different kind of React than Dan is :)))
"The user doesn't care whether it's initialization or update. " --So why am I keeping thinking of "should I need useCallback here?"
"That's what I meant by React not "missing" updates. React doesn't let you write rendering logic that leaves initialization and updates out of sync. " --So I have to fill the annoying dep arrays again and again to achieve this.
;(((