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

Resource error handling #36

Merged
merged 19 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e204533
resources have a default identifier
cesarParra Dec 17, 2024
d9aa1f0
allow for custom identifiers to be provided
cesarParra Dec 17, 2024
02a3aa3
resources console error when an exception is thrown
cesarParra Dec 17, 2024
70c1547
resources allow for errors to be handled through a custom function
cesarParra Dec 18, 2024
91b432f
resources allow for errors to be handled through a custom function
cesarParra Dec 18, 2024
430ab85
Merge branch 'main' into resource-error-handling
cesarParra Dec 18, 2024
84ee204
CI action to run tsc and push changes.
cesarParra Dec 18, 2024
a9124d7
CI action to run tsc and push changes.
cesarParra Dec 18, 2024
0482749
CI action to run tsc and push changes.
cesarParra Dec 18, 2024
6286044
CI action to run tsc and push changes.
cesarParra Dec 18, 2024
ad3f734
CI action to run tsc and push changes.
cesarParra Dec 18, 2024
bbcee15
CI action to run tsc and push changes.
cesarParra Dec 18, 2024
10b29c8
Commit from GitHub Actions (CI)
github-actions[bot] Dec 18, 2024
7cff928
CI action to run tsc and push changes.
cesarParra Dec 18, 2024
49f8141
Merge remote-tracking branch 'origin/resource-error-handling' into re…
cesarParra Dec 18, 2024
683dca9
resources allow for errors to be handled through a custom function th…
cesarParra Dec 18, 2024
ad140ff
resources allow for errors to be handled through a custom function th…
cesarParra Dec 18, 2024
3d0d470
resources allow for errors to be handled through a custom function th…
cesarParra Dec 18, 2024
bfe9211
Commit from GitHub Actions (CI)
github-actions[bot] Dec 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
branches: ["main", "develop"]
workflow_dispatch:

permissions:
contents: write

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -20,3 +23,7 @@ jobs:
- run: npm ci
- run: npm run build --if-present
- run: npm test
- uses: EndBug/add-and-commit@v7
with:
commit: "--allow-empty"
default_author: github_actions
69 changes: 62 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,11 @@ The preferred way of reading a signal is through the `signal.value`.

## Error Handling

When unhandled errors occur in a `computed` or `effect`, by default, the error will be logged to the console through
a `console.error` call, and then the error will be rethrown.
When unhandled errors occur in a `computed`, `effect` or `resource`,
by default, the error will be logged to the console through a `console.error` call, and then the error will be rethrown.

If you wish to know which `computed` or `effect` caused the error, you can pass a second argument to the `computed` or
`effect` with a unique identifier.
If you wish to know where the error is coming from, you can pass a second argument to the `computed`,
`effect` or `resource` with a unique identifier.

```javascript
$computed(
Expand All @@ -242,6 +242,14 @@ $effect(
},
{ identifier: "test-identifier" }
);

$resource(
asyncFunction,
{},
{
identifier: "test-identifier"
}
);
```

This value will be used only for debugging purposes, and does not affect the functionality otherwise.
Expand All @@ -250,7 +258,7 @@ In this example, the test-identifier string will appear as part of the console.e

### Custom Error Handlers

Both computed and effect signals can receive a custom `onError` property,
`computed`, `effect`, and `resource` signals can all receive a custom `onError` property,
that allows developers to completely override the default functionality that logs and rethrows the error.

#### Effect handlers
Expand Down Expand Up @@ -316,6 +324,53 @@ $computed(
);
```

#### Resource handlers

For `resource` handlers, you can pass a function with the following shape:

```typescript
(error: unknown, previousValue: T | null, options: { initialValue: T | null, identifier: string | symbol }) =>
AsyncData<T> | void

// Where AsyncData looks as follows
// {
// data: T | null;
// loading: boolean;
// error: unknown | null;
// };
```

Where you can return nothing, or a value of type `AsyncData<T>`.
`AsyncData` is the shape that all resources take, and it contains the data, loading state, and error state.
This allows you to provide a "fallback" value, that the computed value will receive in case of errors.

As a second argument, you will receive the previous value of the resource (or null if there is none), which can be useful to provide a
fallback value based on the previous value.

The third argument is an object with the received identifier as well as any initial value that was provided to the
resource when it was created.

Example

```javascript
function customErrorHandlerFn(error, _previousValue, _options) {
// custom logic or logging or rethrowing here
return {
data: "fallback value",
loading: false,
error: error
};
}

$resource(
asyncFunction,
{},
{
onError: customErrorHandlerFn
}
);
```

## Tracking objects and arrays

By default, for a signal to be reactive it needs to be reassigned. This can be cumbersome when dealing with objects
Expand Down Expand Up @@ -512,11 +567,11 @@ export default class ContactList extends LightningElement {
}
```

Data from a resource signal comes in the following format:
Data from a resource signal comes in as a read-only signal in the following format:

```typescript
type AsyncData<T> = {
data: ReadOnlySignal<T> | null; // The data fetched from the server. It is null until the data is fetched
data: T | null; // The data fetched from the server. It is null until the data is fetched
loading: boolean; // A boolean that indicates if the data is being fetched
error: unknown | null; // An error object that is populated if the fetch fails
};
Expand Down
35 changes: 25 additions & 10 deletions force-app/lwc/signals/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function $computed(fn, options) {
() => {
if (options?.onError) {
// If this computed has a custom error handler, then the
// handling occurs in the computed function itself.
// handling occurs here, in the computed function itself.
try {
computedSignal.value = fn();
} catch (error) {
Expand Down Expand Up @@ -250,7 +250,22 @@ function $signal(value, options) {
delete returnValue.unsubscribe;
return returnValue;
}
function defaultResourceErrorHandler(error, _previousValue, options) {
const errorTemplate = `
LWC Signals: An error occurred in a reactive function \n
Type: Resource \n
Identifier: ${options.identifier.toString()}
`.trim();
console.error(errorTemplate, error);
}
function $resource(fn, source, options) {
const {
initialValue = null,
optimisticMutate = true,
fetchWhen = () => true,
identifier = Symbol(),
onError = defaultResourceErrorHandler
} = options ?? {};
function loadingState(data) {
return {
data: data,
Expand All @@ -259,17 +274,14 @@ function $resource(fn, source, options) {
};
}
let _isInitialLoad = true;
let _value = options?.initialValue ?? null;
let _value = initialValue;
let _previousParams;
const _signal = $signal(loadingState(_value));
// Optimistic updates are enabled by default
const _optimisticMutate = options?.optimisticMutate ?? true;
const _fetchWhen = options?.fetchWhen ?? (() => true);
const execute = async () => {
const derivedSourceFn = source instanceof Function ? source : () => source;
try {
let data = null;
if (_fetchWhen()) {
if (fetchWhen()) {
const derivedSource = derivedSourceFn();
if (!_isInitialLoad && isEqual(derivedSource, _previousParams)) {
// No need to fetch the data again if the params haven't changed
Expand All @@ -289,7 +301,7 @@ function $resource(fn, source, options) {
error: null
};
} catch (error) {
_signal.value = {
_signal.value = onError(error, _value, { identifier, initialValue }) ?? {
data: null,
loading: false,
error
Expand All @@ -298,7 +310,9 @@ function $resource(fn, source, options) {
_isInitialLoad = false;
}
};
$effect(execute);
$effect(execute, {
identifier
});
/**
* Callback function that updates the value of the resource.
* @param value The value we want to set the resource to.
Expand All @@ -316,7 +330,7 @@ function $resource(fn, source, options) {
data: _signal.readOnly,
mutate: (newValue) => {
const previousValue = _value;
if (_optimisticMutate) {
if (optimisticMutate) {
// If optimistic updates are enabled, update the value immediately
mutatorCallback(newValue);
}
Expand All @@ -327,7 +341,8 @@ function $resource(fn, source, options) {
refetch: async () => {
_isInitialLoad = true;
await execute();
}
},
identifier
};
}
function isSignal(anything) {
Expand Down
Loading