Open HappyStriker opened 1 year ago
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.
bump
I have an RFC open https://github.com/preactjs/signals/issues/291 and a POC implementation https://github.com/preactjs/signals/compare/main...async-resource π
@JoviDeCroock do you know if any further work has been done on this? It looks like you closed your RFC?
Some kind of standard solution here would be awesome. π
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
I've implemented a resource and tested it: https://www.npmjs.com/package/@preact-signals/utils
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) })
@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
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
.
You need to write your own wrapper around generators which will start tracking after some promise resolves
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
},
});
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.
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
Sorry to interject something I have just been exposed to, and I canβt say whether it would help this particular use case, but it does handle generators and promises in a way I have yet to see.
Β https://effect.website/ On Dec 29, 2023 at 2:34 PM -0500, Valerii Smirnov @.***>, wrote:
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 β Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you are subscribed to this thread.Message ID: @.***>
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.
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.
I've implemented two way to manage async state with signals. Resource
const A = () => {
const [resource, { refetch }] = useResource(() => ({
fetcher: async () => {
const response = await fetch("https://example.com");
return response.json();
},
}));
return <div>{resource()}</div>
}
Also there are full featured port of @tanstack/react-query
with signals tracking, btw you can use original react query.
Can I use
import { useResource } from "@preact-signals/utils/hooks"; // this import
...
const App = () => {
const [layoutResource, { refetch }] = useResource<SectionData[]>({
fetcher: async () => {
const response = await fetch("/api/layout");
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
updateHeaderLayoutData(data);
return data;
},
});
...
with just preact
and NOT react.js
? If it works with preact too, what do I need to install?
This doc: https://github.com/XantreGodlike/preact-signals/tree/main/packages/utils
This part:
Ensure that one of the preact signals runtimes is installed:
@preact/signals for preact, requiring an additional step. @preact/signals-core for vanilla js requiring an additional step. @preact-signals/safe-react for react, requiring an additional step. @preact/signals-react for react.
Makes it sound like I only need one of these packages installed for this to work with preact (I'm using Vite for build), in my case since I'm using only preact @preact/signals
, but when I install:
npm i @preact-signals/utils
it creates a dependency on @preact/signals-react
that can be seen in the package-log.json
file.
EDIT:
I see, I added:
alias: [
{ find: "react", replacement: "preact/compat" },
{ find: "react-dom/test-utils", replacement: "preact/test-utils" },
{ find: "react-dom", replacement: "preact/compat" },
{ find: "react/jsx-runtime", replacement: "preact/jsx-runtime" },
{ find: "@preact/signals-react", replacement: "@preact/signals" },
],
},
to my vite.config.ts and it seems to be working. Is this the recommendation?
Can I use
import { useResource } from "@preact-signals/utils/hooks"; // this import ... const App = () => { const [layoutResource, { refetch }] = useResource<SectionData[]>({ fetcher: async () => { const response = await fetch("/api/layout"); if (!response.ok) { throw new Error("Network response was not ok"); } const data = await response.json(); updateHeaderLayoutData(data); return data; }, }); ...
with just
preact
and NOTreact.js
? If it works with preact too, what do I need to install?This doc: https://github.com/XantreGodlike/preact-signals/tree/main/packages/utils
This part:
Ensure that one of the preact signals runtimes is installed:
@preact/signals for preact, requiring an additional step. @preact/signals-core for vanilla js requiring an additional step. @preact-signals/safe-react for react, requiring an additional step. @preact/signals-react for react.
Makes it sound like I only need one of these packages installed for this to work with preact (I'm using Vite for build), in my case since I'm using only preact
@preact/signals
, but when I install:npm i @preact-signals/utils
it creates a dependency on
@preact/signals-react
that can be seen in thepackage-log.json
file.EDIT:
I see, I added:
alias: [ { find: "react", replacement: "preact/compat" }, { find: "react-dom/test-utils", replacement: "preact/test-utils" }, { find: "react-dom", replacement: "preact/compat" }, { find: "react/jsx-runtime", replacement: "preact/jsx-runtime" }, { find: "@preact/signals-react", replacement: "@preact/signals" }, ],
},
to my vite.config.ts and it seems to be working. Is this the recommendation?
Yes, it's recommended way to use it with preact
Is there a version of useResource that works outside a component?
Ofc. There are createResource
function
@XantreGodlike thanks for your feedback.
I assign the JSON from the async call to a signal. I make another async call to the same URL, it returns the JSON, and again, nothing has changed in the data structure (no values are different), and I again assign it to the same signal. I am passing the values of this signal to child components. Will the child components rerender? What I'm looking for is to only update compoents where the data has changed.
For example:
\\ app.tsx
import { effect } from "@preact/signals";
import { useResource } from "@preact-signals/utils/hooks";
import type { SectionData } from "./types";
import {
updateHeaderLayoutData,
h1LinkStore,
h2LinkStore,
h3LinkStore,
} from "./signals/store";
import Header from "./components/header/Header";
const App = () => {
const [layoutResource, { refetch }] = useResource<SectionData[]>({
fetcher: async () => {
console.log("Fetching data");
const response = await fetch("/api/layout");
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
updateHeaderLayoutData(data);
return data;
},
});
// effect(() => {
// const interval = setInterval(() => {
// refetch();
// }, 60000); // 60 seconds
// console.log("Calling refetch");
// return () => clearInterval(interval); // Cleanup on unmount or signal change
// });
return (
<div className="min-h-screen flex flex-col">
<Header
h1Links={h1LinkStore.value || []}
h2Links={h2LinkStore.value || []}
h3Links={h3LinkStore.value || []}
/>
</div>
);
};
export default App;
Then the signal:
\\ store.ts
import { signal } from "@preact/signals";
import type { SectionData, Link } from "../types";
export const headerLayoutStore = signal<SectionData[]>([]);
export const h1LinkStore = signal<Link[]>([]);
export const h2LinkStore = signal<Link[]>([]);
export const h3LinkStore = signal<Link[]>([]);
export const updateHeaderLayoutData = (newData: SectionData[]) => {
const filteredData = newData.filter(data =>
data.section.startsWith("H1") ||
data.section.startsWith("H2") ||
data.section.startsWith("H3")
);
headerLayoutStore.value = filteredData;
h1LinkStore.value = filteredData
.filter(data => data.section.startsWith("H1"))
.flatMap(data => data.placements.flatMap(p => p.links));
h2LinkStore.value = filteredData
.filter(data => data.section.startsWith("H2"))
.flatMap(data => data.placements.flatMap(p => p.links));
h3LinkStore.value = filteredData
.filter(data => data.section.startsWith("H3"))
.flatMap(data => data.placements.flatMap(p => p.links));
};
Then the header:
\\ header.tsx
import DefaultLink from "../common/DefaultLink";
import { Link } from "../../types";
type HeaderProps = {
h1Links: Link[];
h2Links: Link[];
h3Links: Link[];
};
let renderCount = 0;
const Header = ({ h1Links, h2Links, h3Links }: HeaderProps) => {
// Increment and log the render count
console.log(`Header render count: ${++renderCount}`);
return (
<header className="p-4 text-center">
<ul className="list-none text-left">
{h1Links.map((link, index) => (
<DefaultLink
key={index}
URL={link.URL}
ImageURL={link.ImageURL}
Description={link.Description}
TextColorClass={link.TextColorClass}
/>
))}
</ul>
</header>
);
};
export default Header;
and finally the last child component:
\\ defaultlink.tsx
import { Link as LinkType } from "../../types";
let renderCount = 0;
const DefaultLink: React.FC<LinkType> = ({
URL,
ImageURL,
Description,
TextColorClass = "text-black",
}) => {
// Increment and log the render count
console.log(`DefaultLink render count: ${++renderCount}`);
return (
<>
{ImageURL && (
<img
src={ImageURL}
alt={Description}
className="max-w-full h-auto mb-2 pl-2 pr-2 pt-4"
/>
)}
<a
href={URL}
target="_blank"
rel="noopener noreferrer"
className={`block p-2 underline custom-font hover:bg-gray-100 ${TextColorClass}`}
>
{Description}
</a>
</>
);
};
export default DefaultLink;
if the data doesn't change for the H1-H3 portions of the JSON in the async call in app.tsx, I don't want the defaultlin.tsx component to rerender.
Do I need a "computed" to check the value of the new state against the old one and set a boolean as a rerender check perhaps?
@aadamsx You should pass signals to children components to prevent child components rerenders. Also it's better to use resource like this:
const filterSections = (
sections: SectionData[],
startsWith: `H${1 | 2 | 3}`[]
) =>
sections.filter((it) =>
startsWith.some((start) => it.section.startsWith(start))
);
const [resource] = createResource({
fetcher: async () => {
console.log("Fetching data");
const response = await fetch("/api/layout");
if (!response.ok) {
throw new Error("Network response was not ok");
}
const newData = (await response.json()) as SectionData[];
const filteredData = filterSections(newData, ["H1", "H2", "H3"]);
return {
h1: filterSections(filteredData, ["H1"]).flatMap((data) =>
data.placements.flatMap((p) => p.links)
),
h2: filterSections(filteredData, ["H2"]).flatMap((data) =>
data.placements.flatMap((p) => p.links)
),
h3: filterSections(filteredData, ["H3"]).flatMap((data) =>
data.placements.flatMap((p) => p.links)
),
filtered: filteredData,
};
},
});
resource.latest.h1 // some data if fetched
// if you prefer to wrap it in signal
const h1Sig = computed(() => resource.latest.h1)
@XantreGodlike is it possible to pass fetcher arguments from the outside? for example an access token so the fetch can bypass the authorization?
@AfaqaL You can pass arguments from source
and with refetch
method. But for authorization it's better to use wrapper around fetch with this functionality
@XantreGodlike I don't know if I'm understanding this correctly but this is how I managed to pass the data through
const sigAuthToken = signal("");
const [res] = createResource({
source: () => sigAuthToken.value,
fetcher: async (token) => {
if (token === "") return {authed: false}
const infoResp = await fetch(`${process.env.REACT_APP_HOST}/auth/user-info`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
}
}
)
const {data} = await infoResp.json();
return {...data, authed: true}
}
})
when I log the data everything seems to be fetched as expected, however when I use the resource in my JSX components don't budge whatsoever (I tried to wrap in in computed()
as well but no luck). Am I missing something to make JSX responsive to the resource ?
@AfaqaL i think it's ok implementation if you can have no token in fetcher. But in most places of the app use can be sure is user authorized or not. So you can just pass read sigAuthToken.value
in fetcher.
If you're trying to create resource inside of component - you should use useResource
to preserve resource on every render
@XantreGodlike I'm using this resource in a lot of places in different components, thats why I want it to be created outside of the component, however the issue is that it doesn't cause a re-render whatsoever, maybe I'm using its value incorrectly? should I be accessing the .latest
property directly inside a component for it to cause re-renders?
It's ok to create resource with createResource
outside of components
Do you use preact signals babel plugin? Can you send repro in stackblitz?
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:
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:
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()
(orcomputed.await()
)?Thank you very much for your time and I am looking forward to an interessting discussion.
Kind regards, Happy Striker