GithubHelp home page GithubHelp logo

Comments (15)

ryansolid avatar ryansolid commented on May 5, 2024 1

I've confirmed Adam's proposed subclock solution works and does not have this unexpected behaviour. I'm going to try some things and see if I can update the implementation with it.

from solid.

bluenote10 avatar bluenote10 commented on May 5, 2024

This is a minor variation/simplification of the issue:

import { createState, createEffect } from "solid-js";
import { render, Switch, Match } from "solid-js/dom";

const Title = props => {
  /* prettier-ignore */
  return <div>Title: {(props.title.name)}</div>;
};

const App = () => {
  const [state, setState] = createState({
    title: null
  });

  createEffect(() => {
    console.log("show title:", state.title);
  });

  /* prettier-ignore */
  return (
    <div>
      <Switch>
        <Match when={(state.title != null)}>
          <Title title={(state.title)}/>
        </Match>
        <Match when={(!state.title == null)}>
          <div>switched off</div>
        </Match>
      </Switch>
      <button onclick={() => {
        console.log("switch on");
        setState({title: {name: "test"}});
      }}>Switch on</button>
      <button onclick={() => {
        console.log("switch off");
        setState({title: null});
      }}>Switch off</button>
    </div>
  );
};

render(App, document.getElementById("app"));

The behavior is even more surprising, because the switch off/on buttons sometimes work, but not consistently. I can't even see a clear pattern when they work / fail.

from solid.

ryansolid avatar ryansolid commented on May 5, 2024

I'm not sure this is your original case but what I see here is this error when switching the title to null about length of null. If I change the binding to props.title && props.title.length it works. Obviously being in the switch you wouldn't expect the child to be re-evaluating. It has to be the order that bindings are applied here that is doing this. Depending where in the JS it hits the error it finishes evaluating or not. Even if time is frozen it appears the order of setters does matter. Sometimes it fails before executing the switch. Sometimes after depending on it. It seems consistent per case but you are right the overall scenario is unexpected. I will have to trace this in more detail when I get back to see what is going on.

from solid.

ryansolid avatar ryansolid commented on May 5, 2024

Ok, so this issue traces right back to S.js itself. Disposers are queued up when run under a computation. Basically, manual root cleanup is also frozen, which is obviously awkward for branching behavior when both values are set at the same time. I'm going to follow up on S.js about this. I'm going to see what Adam suggests. Example with just S.js (https://codesandbox.io/s/objective-tdd-dif57).

Issue on S.js: adamhaile/S#32

from solid.

bluenote10 avatar bluenote10 commented on May 5, 2024

Thanks for looking into this!

I'm not sure this is your original case but what I see here is this error when switching the title to null about length of null. If I change the binding to props.title && props.title.length it works.

What you are describing sounds like my original problem, but I'm seeing something else in the example here:

  • In my app, my subcomponent is crashing, because the data it needs becomes undefined. In contrast the this example, this happens even though I have added the "data != null" check to the when condition (see here).

  • In the example here (the first one), I wasn't able to re-create this crash in the subcomponent. Instead I see that even the computation in the createEffect doesn't run as expected.

    Peek 2019-08-09 20-25

    I would have expected to get a console log line show state: false when clicken "Switch off". Conversely, since the state never switches to "off", I'm surprised that there is another show state: true message when clicking on "Switch on" again.

Do you think these issues have a common cause, or should I try to find a better reproducing example for the first problem?

from solid.

ryansolid avatar ryansolid commented on May 5, 2024

In all your examples I get the same issue (including that first posted codesandbox). I needed to check the inspector but it's the error that is preventing it from switching off. The data switching happens but the view rendering doesn't update as it errors out before it finishes (but not before it writes the console.log). When you switch it on again the data updates again and you see the console log. The reason you aren't seeing it when you click switch off again is the state object works off values (equality comparator).

So from my perspective, there is only the first issue. Which I agree is unexpected. I understand why it's happening but I'm not sure yet how to best avoid it. See, each state update call is frozen. So both changes apply at the same time, and all disposals incurred during their execution. The reactive graph is essentially flat so both branches update during this cycle before the departed branch is disposed of. So the easy solution is to make 2 setState calls.

In your first example if you wrote:

setState({show: false});
setState({title: null});

It would work perfectly fine. But obviously, the second, more to the point example, falls apart here. This is a pretty easy scenario to rig up so I'd surprised if it hasn't come up in S.js before,

from solid.

bluenote10 avatar bluenote10 commented on May 5, 2024

What I meant is that in my app there is an exception printed to the console at the point of the undefined data usage (the {(props.title.length)} expression in the example), but in the examples I don't see the exception. Maybe that's why you are saying "I needed to check the inspector" and the exception is caught internally by Solid, but only under certain conditions.

from solid.

ryansolid avatar ryansolid commented on May 5, 2024

Ok, this should be fixed in v0.10.0. The thing to keep in mind. Order matters. Even if values pend updates are still inacted in setState order. So example one is still broken until you reverse order to set show to false first. I need to vet whether that makes total sense but at least it is functional in a sensible way. As always give it a spin and let me know if there are any more issues.

from solid.

bluenote10 avatar bluenote10 commented on May 5, 2024

Thanks it seems to work for me! (although I'm having other issues now, need to understand them first...)

The thing to keep in mind. Order matters. Even if values pend updates are still inacted in setState order.

Now I'm confused. I'm new to JS but my understanding was that within an object, properties do not have an order so the following two line are indistinguishable:

setState({title: null, show: false});
setState({show: false, title: null});

Or do you mean that setState isn't atomic (I thought it runs in a freeze or so) and when updating multiple values the state may actually go through intermediate states arbitrarily? I think it is a fairly common use case to update multiple state values at once to establish some kind of invariant w.r.t. state, like for instance foo should only exist if and only if bar exists. It would be nice not having to worry about temporary states that violate such an invariant.

from solid.

ryansolid avatar ryansolid commented on May 5, 2024

Object keys get iterated in a predictable order ("https://www.stefanjudis.com/today-i-learned/property-order-is-predictable-in-javascript-objects-since-es2015/")

Yes, this is sort of what I'm getting to which is why I'm re-evaluating things. I didn't realize(and I imagine most people as well) how freeze worked even though it is clear in the S.js documentation.

Essentially, all freeze does is ensure that all updates are set at the same time, (and that no new derivations are propagated until after that change has completed). So from a data standpoint all computations see title null, and show false, but the computations still need to execute in certain order. If we weren't using state object proxy it would be more obvious but under the hood we are doing:

freeze(() => {
  title(null);
  show(false);
})

When the freeze gets to the end of it's execution after show false.. It then runs all the dependent computations but it appears be queueing them up based on set order. This could be the result of the approach of the fix that I took (based on S.track in the issue is based off(adamhaile/S#26), but it isn't that different on how S works in general. Basically in all cases the all dependencies are flat so while for an individual signal (like title in Example 2) it will evaluate them in order they were initialized in, the same cannot be said for independent computations (one dependent on show, and one dependent on title). In this case the title computation doesn't know that it has any connection to the upstream show computation. I think this might be related to Adam's concern about this approach in general in the comments on that issue. From the discussion alone I just couldn't visualize it.

I'm going to take a look closer at this and subclocks and see if that's the solution. I had tested many scenarios with this logic but I didn't realize I had switched the order in your example until after I'd already released it. I suspect this approach to batching though this is part of how S.js works and part of why it is so much more performant than MobX. It doesn't try to reorder everything. It just ensures that there is little to no jittering. So the change is atomic in the sense that all data updates at the same time so you won't cause cascading updates by having temporary data states (ie.. {title: null, show: true} will never exist). However it appears that any derivation while handled predictably isn't necessarily in the preferred order.

This is clearly much more complicated than my initial understanding of the reactive system. And I need to get a better grip on the fine details.

from solid.

bluenote10 avatar bluenote10 commented on May 5, 2024

Ah that was very helpful. If I understand it correctly, a computation that explicitly depends on both variables doesn't have to worry to go through "invalid" states, e.g. if I do createEffect(() => console.log(state.show, state.title)) I would never get the impossible combination true / null. The problem here is that a Switch/Show only depends on state.show whereas the nested computation depends on state.title. In this case the updates run independently and order matters. If I arbitrarily squeeze the state.show into the computation (e.g. by passing it as another prop and use an nonsensical expression like {(props.title.length + props.show)}) the dependency becomes explicit and the problem goes away. Would it be a solution to "attach" the dependencies of a Switch/Show to all its nested computation then? Since they are booleans anyway they cannot lead to additional re-computations because all they can do is to switch from true to false in which case the nested computations get discarded anyway.

What's still not clear to me is under which circumstances Solid swallows exceptions in JSX computations. If I place the expression {(undefined.call())} in the top level component, I get an explicit exception (i.e. uncaught, visible in console). But when using the same expression in the <Title> return statement it becomes a silent error. From my naive perspective it would be good to always bubble up user errors, otherwise they are hard to find. Is there a more profound reason for the behavior? (Maybe we can also track that as a separate issue.)

from solid.

ryansolid avatar ryansolid commented on May 5, 2024

Yeah I believe you get it (well as far as I'm getting it). I was not aware of this behavior initially when I chose S but it makes sense from the perspective of it solving batching very cheaply. The the impossible intermediate data states usually are much more dangerous from a consistency standpoint anyway. In terms of binding the dependencies. Can't be done at compile time I think given no guarantee of locality. Title could be coming from anywhere. But could it be done at runtime? Hmm.. hard since the condition evaluates before the children. So we'd need a way of adding dependencies after execution and passing a context down the branch. The most interesting piece would we would need to be aware of the declared scope since we wouldn't want any data nodes referenced by computations higher up since they wouldn't get released until the higher context. Something to think about for sure.

I have not witnessed the same swallow exception bit. What I've been trying to say is in all cases I've seen the error right away which is why I was able to identify it right away. CodeSandBox didn't show it in all cases but Chrome Debugger/Inspector did. I assumed it was a CodeSandBox issue. Solids reactive core(based on S.js) does have try blocks but they don't have catches.. just finally. Maybe the behavior is inconsistent across browsers?

from solid.

bluenote10 avatar bluenote10 commented on May 5, 2024

CodeSandBox didn't show it in all cases but Chrome Debugger/Inspector did. I assumed it was a CodeSandBox issue.

Yes, I did a few more tests and I also think it's just an issue with the built-in codesandbox console. Good to know, I'll have to be more careful with making conclusions using codesandbox.

from solid.

ryansolid avatar ryansolid commented on May 5, 2024

Ok, I fixed it in v0.10.2. This was pretty tricky. I looked at SubClocks but couldn't get node recycling to work and it required even more code. I managed to take the solution I already had and realized that is was detecting this state but not handling. I've managed to pinpoint this scenario to ensure nodes marked for disposal that are children of pending nodes don't update before their parents. This provides minimal overhead for cases that don't fall into this, including if you arrange the arguments in the right order, but if you don't it will just re-queue the update by which point if the disposal is permanent it should be disposed as the parent has finished its evaluation.

It feels like this bug has taken forever to address. But I think it really helped me improve the quality of the library. All the high performing Fine Grained libraries based on S.js have this issue and they should seriously consider looking into this.

As always let me know if you have more issues.

from solid.

ryansolid avatar ryansolid commented on May 5, 2024

Inactive for 2 weeks. Closing.

from solid.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.