Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Computed signal with async callback #284

Open
HappyStriker opened this issue Dec 9, 2022 · 30 comments
Open

Computed signal with async callback #284

HappyStriker opened this issue Dec 9, 2022 · 30 comments

Comments

@HappyStriker
Copy link

HappyStriker commented Dec 9, 2022

Hey there,

I am using Preact more and more since the publication of the Signals API and its a great and, especially in relation to regular state and context, easy way to build a reactive user
Interface.

In my newest project though I have reached an issue for which I am looking for a recommended way to solve it: Whats the best way to handle a computed signal when its callback method is asynchronous but you want to deal only with its resolved value?

Example:

const address = signal('localhost');
const content = computed(async () => {
  // loads something like `http://${address.value}/` and fully handles errors so the async function will never throw
});

The code above will result in content always being a Promise, but I want it to be the resolved value of the async function instead.

So far I have solved this issue with something like this:

const address = signal('localhost');
const content = signal(null);
effect(async () => {
  // loads something like `http://${address.value}/` and fully handles errors so the async function will never throw
});

That solution does work, but seems a bit messy, especially because it can not be used directly in a class.

Is there a better solution to handle this or would it even be a reason for adding a new method to Signals like asyncComputed() (or computed.await())?

Thank you very much for your time and I am looking forward to an interessting discussion.

Kind regards,
Happy Striker

@HappyStriker
Copy link
Author

HappyStriker commented Dec 9, 2022

In the meantime I have played with an additional method that looks like this:

computed.await = function(v, cb = arguments.length === 1 ? v : cb) {
  const s = signal(arguments.length === 2 ? v : undefined);
  effect(() => cb().then((v) => s.value = v));
  return computed(() => s.value);
};

where v is the optional initial value to be used until the async function does return.

This extension can eg. be used for loading a page using a promise in a oneliner like below:

const page = computed.await('Loading...', async () => (await fetch('app.html')).text());
render(HTML`<div>${page}</div>`);

In something like that possible with Signals at the current moment or maybe worth being added? In case an addition is not aimed for, is there any feedback if the method above is save to use or is it messing to much with some internals that I am not aware of?

Thanks for your feedback.

@loicnestler

This comment was marked as spam.

@JoviDeCroock
Copy link
Member

I have an RFC open #291 and a POC implementation main...async-resource 😅

@tylermercer
Copy link

@JoviDeCroock do you know if any further work has been done on this? It looks like you closed your RFC?

@akbr
Copy link

akbr commented Dec 4, 2023

Some kind of standard solution here would be awesome. 😉

@JoviDeCroock
Copy link
Member

Folks testing out the branch I created could go a pretty long way in getting this out there 😅 I don't personally have good testing grounds at the current time

@XantreDev
Copy link
Contributor

I've implemented a resource and tested it: https://www.npmjs.com/package/@preact-signals/utils

@MicahZoltu
Copy link

The problem with any sort of async stuff with Signals right now is that when you do mySignal.value it calls addDependency(this) internally. This function checks to see if it is executing inside of a context that is tracking signal references (e.g., inside a useComputed) and if so, it adds mySignal as a dependency to that context. This is how useComputed is able to automatically update anytime any used signals are touched.

The problem is that as soon as you do await inside of an async function, you leave the context that is tracking changes and any references to other signals inside the async function won't be tracked, because they occur inside a continuation which is in a different context.

I don't know how to address this problem, even with significant changes to how signal tracking is done. Users can deal with this by making sure to reference any signals they care about before the first await, but that is a really easy foot-gun.

You can see the problem illustrated here:

import { computed, signal } from '@preact/signals-core'
import * as process from 'node:process'

async function sleep(milliseconds: number) { await new Promise(resolve => setTimeout(resolve, milliseconds)) }

async function main() {
	const apple = signal(3n)
	const banana = signal(5n)

	// create a computed signal that is derived from the contents of both of the above signals
	const cherry = computed(async () => {
		// apple is "read" before the await
		apple.value
		// sleep for a negligible amount of time to ensure the rest of this function runs in a continuation
		await sleep(1)
		// banana is "read" after the await
		banana.value
		// final computed value
		return apple.value + banana.value
	})

	console.log(`Apple: ${apple.value}; Banana: ${banana.value}; Cherry: ${await cherry.value}`)
	// Apple: 3; Banana: 5; Cherry: 8

	// change the value of apple and notice that Cherry correctly tracks that change
	apple.value = 4n
	console.log(`Apple: ${apple.value}; Banana: ${banana.value}; Cherry: ${await cherry.value}`)
	// Apple: 4; Banana: 5; Cherry: 9

	// change the value of banana, and notice that Cherry is **not** recomputed (should be 10)
	banana.value = 6n
	console.log(`Apple: ${apple.value}; Banana: ${banana.value}; Cherry: ${await cherry.value}`)
	// Apple: 4; Banana: 6; Cherry: 9

	// change the value of apple again, and notice that Cherry is correctly recomputed using the value of banana set above
	apple.value = 11n
	console.log(`Apple: ${apple.value}; Banana: ${banana.value}; Cherry: ${await cherry.value}`)
	// Apple: 11; Banana: 6; Cherry: 17
}

main().then(() => process.exit(0)).catch(error => { console.error(error); process.exit(1) })

@XantreDev
Copy link
Contributor

@MicahZoltu There is no reactivity system that handles this case, because of design of js. Main solution is to use resource like api and pass all deps explicitly:
https://docs.solidjs.com/references/api-reference/basic-reactivity/createResource

I've implemented analog for preact-signals:
https://github.com/XantreGodlike/preact-signals/tree/main/packages/utils#resource

Another solution is to not use async functions but generator functions:

import { Effect } from "effect"
 
const increment = (x: number) => x + 1
 
const divide = (a: number, b: number): Effect.Effect<never, Error, number> =>
  b === 0
    ? Effect.fail(new Error("Cannot divide by zero"))
    : Effect.succeed(a / b)
 
// $ExpectType Effect<never, never, number>
const task1 = Effect.promise(() => Promise.resolve(10))
// $ExpectType Effect<never, never, number>
const task2 = Effect.promise(() => Promise.resolve(2))
 
// $ExpectType Effect<never, Error, string>
export const program = Effect.gen(function* (_) {
  const a = yield* _(task1)
  const b = yield* _(task2)
  const n1 = yield* _(divide(a, b))
  const n2 = increment(n1)
  return `Result is: ${n2}`
})
 
Effect.runPromise(program).then(console.log) // Output: "Result is: 6"

https://www.effect.website/docs/essentials/using-generators#understanding-effectgen

@MicahZoltu
Copy link

The pattern followed by createResource is the same one as I currently follow for async state. See https://github.com/Zoltu/preact-es2015-template/blob/master/app/ts/library/preact-utilities.ts#L18-L62 for my rendition of it. It works well, but not for "computed" values.

I don't see how generators would help solve the problem where we want to re-run some async work when a signal that is read after the async work is touched.

const mySignal = signal(5)
useComputed(async () => {
	// mySignal.value
	const result = await fetch(...)
	const fetchedValue = getValueFromResult(result)
	return fetchedValue + mySignal.value
}

In this situation, we want to re-fetch the value when mySignal changes, but that won't happen unless we uncomment the first line of the computed function. While this works, it is a really easy mistake to make to forget to read all of your signals before the first await, just like it is easy to forget to pass your dependencies to things like useEffect.

@XantreDev
Copy link
Contributor

You need to write your own wrapper around generators which will start tracking after some promise resolves

@XantreDev
Copy link
Contributor

Write it with resource in this way

const [resource, { refetch }] = createResource({
  source: () => ({ mySignal: mySignal.value }),
  fetcher: async ({ mySignal }) => {
	const result = await fetch(...)
	const fetchedValue = getValueFromResult(result)
	return fetchedValue + mySignal
  },
});

@MicahZoltu
Copy link

This doesn't seem much better than just doing this (which works):

const mySignal = signal(5)
useComputed(async () => {
	mySignal.value
	const result = await fetch(...)
	const fetchedValue = getValueFromResult(result)
	return fetchedValue + mySignal.value
}

Having the source field required helps a little bit, as it reminds you that you need to do something, but if you reference multiple signals it is very easy to forget one of them in either case.

@XantreDev
Copy link
Contributor

I don't think this case should be handled. You have primitives for this case, if you want - you can write your own wrapper around generators, but I don't think it will be useful or beautiful
Maybe good solution will be eslint plugin that checks it, but it will have false positives with some object with value field.
I actually don't think you should use async functions with computed at all, there are resource for that cases. Signals is sync primitive, if you want async reactivity - you should use rxjs

@oravecz
Copy link

oravecz commented Dec 29, 2023 via email

@aadamsx
Copy link

aadamsx commented Jan 6, 2024

I'm starting a new React.js project, and was looking at replacing react's useEffect, etc. with Singals. But it seems when l populate the signal with the result of an async fetch statement and try to use the resulting signal to populate my component, nothing ever renders (unless I do a console log of the data for some strange reason). I look all over the place and have never seen an example of an async fetch call used to populate a signal. We do this all the time in React.js with useEffect. Does anyone know if this is possible with signals? If not then I guess I'll stick with react hooks.

@XantreDev

This comment was marked as off-topic.

@aadamsx

This comment was marked as off-topic.

@XantreDev

This comment was marked as off-topic.

@oravecz

This comment was marked as off-topic.

@XantreDev

This comment was marked as off-topic.

@XantreDev

This comment was marked as off-topic.

@aadamsx

This comment was marked as off-topic.

@XantreDev

This comment was marked as off-topic.

@AfaqaL

This comment was marked as off-topic.

@XantreDev

This comment was marked as off-topic.

@AfaqaL

This comment was marked as off-topic.

@XantreDev

This comment was marked as off-topic.

@AfaqaL

This comment was marked as off-topic.

@XantreDev

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants