Async Preact Signals
When working with
signals
in Javascript, it is very common to work with async data from
Promises
.
Async vs Sync
But unlike other state management libraries, signals do not have an
asynchronous
state graph and all values must be computed
synchronously
.
When people first start using signals they want to simply add
async
to the function callback but this breaks how they work under the hood and leads to
undefined
behavior. ☹️
Async functions are a leaky abstraction and force you to handle them all the way up the graph. Async is also not always better and can have a
performance impact
. 😬
Working with Promises
We can still do so much with sync operations, and make it eaiser to work with common async patterns.
For example when you make a
http
request using
fetch
, you want to return the data in the
Promise
and update some UI.
const el = document.querySelector('#output');
let postId = '123';
fetch(/posts/${postId}).then(res => res.json()).then(post => {
el.innerText = post.title;
})
Now when we add signals we can rerun the fetch everytime the post id changes.
import { effect, signal } from "@preact/signals-core";
const el = document.querySelector('#output');
const postId = signal( '123');
effect(() => {
fetch(/posts/${postId.value}).then(res => res.json()).then(post => {
el.innerText = post.title;
});
});
This is better, but now we need to handle stopping the previous request if the post id changes before the previous fetch completes.
import { effect, signal } from "@preact/signals-core";
const el = document.querySelector('#output');
const postId = signal( '123');
let controller;
effect(() => {
if (controller) {
controller.abort();
}
controller = new AbortController();
const signal = controller.signal;
try {
fetch(/posts/${postId.value}, { signal }).then(res => res.json()).then(post => {
el.innerText = post.title;
});
} catch (err) {
// todo: show error message
}
});
But this still skips a lot of things we normally want to show like loading states and error states.
import { effect, signal, batch } from "@preact/signals-core";
const el = document.querySelector('#output');
const postId = signal( '123');
const postData = signal({});
const errorMessage = signal('');
const loading = signal(false);
let controller;
effect(() => {
if (controller) {
controller.abort();
}
controller = new AbortController();
const signal = controller.signal;
batch(() => {
loading.value = true;
errorMessage.value = '';
postData.value = {};
});
try {
fetch(/posts/${postId.value}, { signal }).then(res => res.json()).then(post => {
batch(() => {
postData.value = post;
loading.value = false;
});
});
} catch (err) {
errorMessage.value = err.message;
}
});
effect(() => {
if (loading.value) {
el.innerText = 'Loading...';
} else if (errorMessage.value) {
el.innerText = Error: ${errorMessage.value};
} else {
el.innerText = postData.value.title;
}
});
Now we can show the proper states, but this is only for one request...
We could wrap this up in a class to reuse or create a new type of signal that can work with asynchronous data.
AsyncState
We want to have a base class that we can make our loading states easily extend from:
export class AsyncStateAsyncData{${this._value}};
}
}
AsyncLoading
For the loading state we override the methods like
AsyncData
.
export class AsyncLoadingAsyncLoading{};
}
}
AsyncError
For the error state we can pass an object of any type to return the error as value instead of throwing an exception (like Go).
export class AsyncErrorAsyncError{${this._error}};
}
}
asyncSignal
Now we the state classes created, we can create a function to create an asynchronous signal with all the logic we talked about earlier.
We need to show the sync value at any time and have a way to abort previous requests.
export function asyncSignal/posts/${postId.value}).then(res => res.json()));
effect(() => {
el.innerText = result.value.map({
onLoading: () => 'Loading...',
onError: (err) => Error: ${err},
onData: (post) => post.title,
});
});
postId.value = '456';
Conclusion
I have started a Preact Signals GitHub discussion
here
and you can find a gist with the
final source code here
. 🎉
This has made working with asynchronous data a lot eaiser to work with and would love to hear your thoughts about ways to improve it 👀
Also if you are curious about how Angular does asynchronous signals you can check out the
resource signal
and the
computedFrom/Async signal
.
async-preact-signals
安装
npx skills add https://github.com/rodydavis/skills --skill async-preact-signals