Replies: 15 comments
-
What have you been doing with |
Beta Was this translation helpful? Give feedback.
-
I was using it to execute a code in scope of other owner. However now, it is also changing the Listener as well us not throwing an error is case there is an error boundary - which is not what I expected from The breaking change does not allow anymore to relay on the result as in case of error it might return undefined. There is no way now to distinguish between an error and actually returned const result = runWithOwner(owner, () => {
return init()
})
result // undefined - is it an error or a return from init? I would suggest reverting it to a primitive, and build a higher level helper to support those cases to bring back the original behaviour. |
Beta Was this translation helpful? Give feedback.
-
Isn't this the same behavior as any other reactive primitive? instead of throwing and freezing the app, errors will be caught by the error boundary. const result = runWithOwner(owner, () => {
return { v: init() }
})
result // undefined - error thrown, { v: T } - ran without errors I consider the previous behavior a bug, but you may have a different usecase for these. |
Beta Was this translation helpful? Give feedback.
-
I don't see runWithOwner as a control flow function. For me it has nothing to do with error boundary the same as Owner. The function which is calling runWithOwner should decide how to handle its error. It is a very low level primitive that was introduced to not expose I think we could go even further now and control Listener in the same way: export function runWith<T>(
{ listener, owner }: { owner?: typeof Owner; listener?: typeof Listener },
fn: () => T
): T {
const prevListener = Listener;
const prevOwner = Owner;
if (owner) Owner = owner;
if (listener) Listener = listener;
try {
return runUpdates(fn, true)!;
} finally {
Owner = prevOwner;
Listener = prevListener;
}
} My use cases are in global state management as well as in async world, where I want to be able to run a chunk of code under some specific place in reactivity tree. Do exactly what Your suggestion is a huge hack :/ and it's still not fixing the breaking change. Instead we could just use try catch to handle the error and expose API if needed, or build a function that will be like: export function wrapWithOwner(o: typeof Owner, fn: () => T): void {
try {
runWithOwner(o, fn)
} catch (error) {
handleError(error)
}
} This way we would avoid a breaking change, have an access to a powerful and self-healing primitive, as well as be able to handle errors without exposing internals. |
Beta Was this translation helpful? Give feedback.
-
I hope this example could help understand my reasoning: function a() { ... }
async function b(owner, fn) {
const v = runWithOwner(owner, fn)
await sendThisToServer(v)
}
// somewhere in the app
b(owner, () => "hello") The Maybe there should be some API allowing to throw an error in specific boundary: export function handleErrorWithOwner(owner: typeof Owner, error: any) {
runWithOwner(owner, () => handleError(error));
} |
Beta Was this translation helpful? Give feedback.
-
Hmm.. I think the previous Listener behavior was wrong. There is no way that setting the owner should have let the Listener through. It was an oversight and never intended to be that way. I think Error is a more interesting discussion. The problem is that runWithOwner is resuming a reactive scope. I think the expectation is very much that it would play into the error boundary. Like if not to insert it in the reactive context why do it? Batch very much was swallowing these errors anyway from the perspective runWIthOwner shouldn't catch, and if runWithOwner is the start of the boundary so my gut would be if I was creating async function b(owner, fn) {
let error;
const v = runWithOwner(owner, () => {
try {
return fn();
} catch (err) {
error = err;
}
})
if (error) throw error;
await sendThisToServer(v)
} This isn't pretty but I am having a hard time picturing where this would not be the expectation. And adding API surface for this feels like taking complexity to an unwanted place. Opting into error boundaries seems unexpected as you already told it to run under this context. That being all said I can sympathize this is breaking behavior. I very much viewed it as an oversight on my part. A bug. We could revert some things and make this into 1.7 release if that makes more sense. I don't think this is a wait until 2.0 sort of thing. It was very much broken before these "fixes". I wouldn't mind more opinions on this. But I suspect it is super niche. |
Beta Was this translation helpful? Give feedback.
-
related discussion: |
Beta Was this translation helpful? Give feedback.
-
I think our confusion here is about the purpose of While we were designing it in #323 it was meant to be a low level primitive - the same level as If I am currently able to do: untrack(() => runWithOwner(owner, () => "hello"))
// equal to
runWithListener(null, () => runWithOwner(owner, () => "hello")) I don't see a reason why I shouldn't be able to specify my own listener if I am able to get it using runWithListener(listener, () => runWithOwner(owner, () => "hello")) Going even further, now I have a case where I need to run a chunk of code without an owner - which is currently not supported by These low level functions are important to keep Solid open for advanced integrations. While we are not able to predict all use cases, my presence here proves we did miss a case. The open API for reactivity is in my opinion a huge strength of Solid, which is not limiting it to its built-in utilities functions, and allows for plenty of use cases to be handled. The current design of Instead I think we should at least preserve export function runWith<T>(
{ listener, owner }: { owner: typeof Owner; listener: typeof Listener },
fn: () => T
): T {
const prevListener = Listener;
const prevOwner = Owner;
if (owner) Owner = owner;
if (listener) Listener = listener;
try {
return fn();
} finally {
Owner = prevOwner;
Listener = prevListener;
}
} To handle the current use case for batch, we could introduce: export function runUpdatesWithOwner(owner: typeof Owner, fn: () => unknown): void {
runWith({ owner, listener: null }, () => {
try {
return fn();
} catch (error) {
handleError(error);
}
}
} That would work as another utility and handle one of the use cases. Please mind that Furthermore, I think we have a bug in It the example pointed by Damian: batch(() => runWithOwner(owner, () => {
throw new Error("Batch and Throw in owner");
})) we are running function runUpdates<T>(fn: () => T, init: boolean) {
if (Updates) {
try {
return fn();
} catch (error) {
handleError(error);
}
}
...
} For consistency we could even stop returning value there, as we are not able to guarantee the result. Please correct me if Im wrong in my thinking process. I think we used I would be happy to talk and work on it more, as it looks like the issue is more complex than initially assumed. |
Beta Was this translation helpful? Give feedback.
-
I feel like a lot of these "breaking changes" happen because some behavior is assumed, but not actually unit-tested here, so it's easy to override it. batch(() => runWithOwner(owner, () => {
throw new Error("Batch and Throw in owner");
})) In my mind, const runEffect = createCallback() // replaces runWithOwner
<button onClick={() => {
runEffect(() => {
throw "can throw and ErrorBoundary will catch it"
createEffect(() => {
console.log("can create effect that will be disposed with parent owner")
})
})
}} />
// replaces getOwner and getListener
hasOwner() // => boolean
hasListener() // => boolean
if (!hasOwner()) {
console.warn("this should be called under a reactive owner!!")
} |
Beta Was this translation helpful? Give feedback.
-
I think we need to trace back to when Like someone tried to resume a context and something wasn't working. I see that when I changed Jan 3rd 2022. I added a test to make sure created effects and cleanups ran. I'm not sure why though. I was working on streaming at the time and fixing up some concurrency bugs. It must have been for errors. I probably put it in runInUpdates to avoid having to write customer error handling stuff in there. But seems were are here anyway. Which brings me to. @Exelord I'm less clear still about the actual use cases. When you proposed it I was definitely thinking more like Damien's examples which is why I compared it to Context. But immediately saw how this makes it so easy to break things in unpredictable ways. I needed the tool internally so I thought it was reasonable to expose it.
@thetarnav that's a cool API idea. I'd love not to expose Owner although it also is used with createRoot as well to inject owning context. Maybe that's the trick though. Have this singular way of doing it. Even with createRoot. |
Beta Was this translation helpful? Give feedback.
-
Yes, I agree. The issue seams to not be related to latest changes. As you mentioned they are consequences of using runUpdates inside. I see, then I guess our intentions diverged. I have never seen runWithOwner as a function to run "effect" externally. However, it might be exactly what we need. Here is my use case demo: https://playground.solidjs.com/anonymous/b782b478-0803-4595-b30a-211fbd4c2476 What Im trying to achieve is:
So far, I am able to implement it all using custom function: function runInOwner<T>(owner: Owner, fn: () => T) {
let error;
let hasErrored = false;
const result = runWithOwner(owner, () => {
let disposer: () => void;
onCleanup(() => disposer?.());
return createRoot((dispose) => {
disposer = dispose;
try {
return fn();
} catch (e) {
dispose();
hasErrored = true;
error = e;
return;
}
}, owner);
})!;
if (hasErrored) throw error; // We need to throw error here in order to stop further execution of parent function
return result;
} I would love to simplify it into: function runInOwner<T>(owner: typeof Owner, fn: () => T): T {
return runWithOwner(owner, () => {
let disposer: () => void;
onCleanup(() => disposer?.());
return createRoot((dispose) => {
disposer = dispose;
try {
return fn();
} catch (e) {
dispose();
throw error;
}
}, owner);
})!;
} and maybe eventually built it into solid, removing a need for runWithOwner (current implementation). I just thought, If we could support custom owner for export function runWithOwner<T>(o: Owner, fn: RootFunction<T>): T {
return createRoot(dispose => {
try {
const result = fn(dispose);
onCleanup(dispose, o);
return result;
} catch (error) {
dispose();
throw error;
}
}, o);
} eventually even resigning from |
Beta Was this translation helpful? Give feedback.
-
It looks like I have found an elegant solution to my issue: export function runInSubRoot<T>(
fn: RootFunction<T>,
owner?: typeof Owner,
cleanup = false
): T {
return createSubRoot((dispose) => {
onError((error) => {
dispose();
throw error;
});
const result = fn(dispose);
if (cleanup) dispose();
return result;
}, owner);
} Now I am more convinced that we don't need @ryansolid what do you think about the idea? |
Beta Was this translation helpful? Give feedback.
-
Not sure.. We've been playing with the idea that roots should have no impact on owner. Like they just keep their current owner, and then runWithOwner is the only way to change owner. Then I thought |
Beta Was this translation helpful? Give feedback.
-
Isn't the purpose of root to create a new owner? And the detachedOwner is basically an origin? If other APIs would support optionally a custom owner: then there will be never a case to use I think So funny how I went from "I need original |
Beta Was this translation helpful? Give feedback.
-
I think this belongs as a discussion rather than an open issue as to inform 2.0 design. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Describe the bug
In this commit 54f3068 error handling has been added which caused a change in the interface, breaking backwards compatibility, and causing users to fail.
Your Example Website or App
54f3068
Steps to Reproduce the Bug or Issue
Expected behavior
runWithOwner
should stay as a primitive to run code with different owner. Currently it is doing much more than that, like handling errors and managing Listener. This make it impossible to use it for the purpose described in the docs.I think we should create another helper, and leave runWithOwner as a primitive to allow handle advanced use cases.
Screenshots or Videos
No response
Platform
Additional context
No response
Beta Was this translation helpful? Give feedback.
All reactions