Skip to content

Commit 8cc6ed5

Browse files
committed
Adds ErrorBoundary (from react-error-boundary, but scaled down for preact) -- fixes a lot of tests, which expect react-related functionality
1 parent a9c5c75 commit 8cc6ed5

21 files changed

Lines changed: 3482 additions & 963 deletions

examples/preact/simple/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"preview": "vite preview"
88
},
99
"dependencies": {
10+
"@tanstack/preact-query": "workspace:^",
1011
"preact": "^10.26.9"
1112
},
1213
"devDependencies": {
Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,44 @@
1-
import { render } from 'preact';
2-
3-
import preactLogo from './assets/preact.svg';
4-
import './style.css';
1+
import { render } from 'preact'
2+
import {
3+
QueryClient,
4+
QueryClientProvider,
5+
useQuery,
6+
} from '@tanstack/preact-query'
57

68
export function App() {
7-
return (
8-
<div>
9-
<a href="https://preactjs.com" target="_blank">
10-
<img src={preactLogo} alt="Preact logo" height="160" width="160" />
11-
</a>
12-
<h1>Get Started building Vite-powered Preact Apps </h1>
13-
<section>
14-
<Resource
15-
title="Learn Preact"
16-
description="If you're new to Preact, try the interactive tutorial to learn important concepts"
17-
href="https://preactjs.com/tutorial"
18-
/>
19-
<Resource
20-
title="Differences to React"
21-
description="If you're coming from React, you may want to check out our docs to see where Preact differs"
22-
href="https://preactjs.com/guide/v10/differences-to-react"
23-
/>
24-
<Resource
25-
title="Learn Vite"
26-
description="To learn more about Vite and how you can customize it to fit your needs, take a look at their excellent documentation"
27-
href="https://vitejs.dev"
28-
/>
29-
</section>
30-
</div>
31-
);
9+
const queryClient = new QueryClient()
10+
return (
11+
<QueryClientProvider client={queryClient}>
12+
<Example />
13+
</QueryClientProvider>
14+
)
3215
}
3316

34-
function Resource(props) {
35-
return (
36-
<a href={props.href} target="_blank" class="resource">
37-
<h2>{props.title}</h2>
38-
<p>{props.description}</p>
39-
</a>
40-
);
17+
const Example = () => {
18+
const { isPending, error, data, isFetching } = useQuery({
19+
queryKey: ['repoData'],
20+
queryFn: async () => {
21+
const response = await fetch(
22+
'https://api.github.com/repos/TanStack/query',
23+
)
24+
return await response.json()
25+
},
26+
})
27+
28+
if (isPending) return 'Loading...'
29+
30+
if (error !== null) return 'An error has occurred: ' + error.message
31+
32+
return (
33+
<div>
34+
<h1>{data.full_name}</h1>
35+
<p>{data.description}</p>
36+
<strong>👀 {data.subscribers_count}</strong>{' '}
37+
<strong>{data.stargazers_count}</strong>{' '}
38+
<strong>🍴 {data.forks_count}</strong>
39+
<div>{isFetching ? 'Updating...' : ''}</div>
40+
</div>
41+
)
4142
}
4243

43-
render(<App />, document.getElementById('app'));
44+
render(<App />, document.getElementById('app'))

packages/preact-query/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@
7575
"@testing-library/react-render-stream": "^2.0.0",
7676
"cpy-cli": "^5.0.0",
7777
"eslint-config-preact": "^2.0.0",
78-
"npm-run-all2": "^5.0.0",
78+
"npm-run-all2": "^5.0.2",
7979
"preact": "^10.28.0",
80-
"preact-iso": "^2.11.0",
80+
"preact-iso": "^2.11.1",
8181
"preact-render-to-string": "^6.6.4",
8282
"typescript-eslint": "^8.50.0"
8383
},

packages/preact-query/src/QueryErrorResetBoundary.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
'use client'
2-
31
import { ComponentChildren, createContext } from 'preact'
42
import { useContext, useState } from 'preact/hooks'
53

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { Component, createElement, ErrorInfo } from 'preact'
2+
import { ErrorBoundaryContext } from './ErrorBoundaryContext'
3+
import { ErrorBoundaryProps, FallbackProps } from './types'
4+
5+
type ErrorBoundaryState =
6+
| {
7+
didCatch: true
8+
error: any
9+
}
10+
| {
11+
didCatch: false
12+
error: null
13+
}
14+
15+
const initialState: ErrorBoundaryState = {
16+
didCatch: false,
17+
error: null,
18+
}
19+
20+
export class ErrorBoundary extends Component<
21+
ErrorBoundaryProps,
22+
ErrorBoundaryState
23+
> {
24+
constructor(props: ErrorBoundaryProps) {
25+
super(props)
26+
27+
this.resetErrorBoundary = this.resetErrorBoundary.bind(this)
28+
this.state = initialState
29+
}
30+
31+
static getDerivedStateFromError(error: Error) {
32+
return { didCatch: true, error }
33+
}
34+
35+
resetErrorBoundary(...args: any[]) {
36+
const { error } = this.state
37+
38+
if (error !== null) {
39+
this.props.onReset?.({
40+
args,
41+
reason: 'imperative-api',
42+
})
43+
44+
this.setState(initialState)
45+
}
46+
}
47+
48+
componentDidCatch(error: Error, info: ErrorInfo) {
49+
/**
50+
* To emulate the react behaviour of console.error
51+
* we add one here to show that the errors bubble up
52+
* to the system and can be seen in the console
53+
*/
54+
console.error('%o\n\n%s', error, info)
55+
this.props.onError?.(error, info)
56+
}
57+
58+
componentDidUpdate(
59+
prevProps: ErrorBoundaryProps,
60+
prevState: ErrorBoundaryState,
61+
) {
62+
const { didCatch } = this.state
63+
const { resetKeys } = this.props
64+
65+
// There's an edge case where if the thing that triggered the error happens to *also* be in the resetKeys array,
66+
// we'd end up resetting the error boundary immediately.
67+
// This would likely trigger a second error to be thrown.
68+
// So we make sure that we don't check the resetKeys on the first call of cDU after the error is set.
69+
70+
if (
71+
didCatch &&
72+
prevState.error !== null &&
73+
hasArrayChanged(prevProps.resetKeys, resetKeys)
74+
) {
75+
this.props.onReset?.({
76+
next: resetKeys,
77+
prev: prevProps.resetKeys,
78+
reason: 'keys',
79+
})
80+
81+
this.setState(initialState)
82+
}
83+
}
84+
85+
render() {
86+
const { children, fallbackRender, FallbackComponent, fallback } = this.props
87+
const { didCatch, error } = this.state
88+
89+
let childToRender = children
90+
91+
if (didCatch) {
92+
const props: FallbackProps = {
93+
error,
94+
resetErrorBoundary: this.resetErrorBoundary,
95+
}
96+
97+
if (typeof fallbackRender === 'function') {
98+
childToRender = fallbackRender(props)
99+
} else if (FallbackComponent) {
100+
childToRender = createElement(FallbackComponent, props)
101+
} else if (fallback !== undefined) {
102+
childToRender = fallback
103+
} else {
104+
console.error(
105+
'preact-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop',
106+
)
107+
108+
throw error
109+
}
110+
}
111+
112+
return createElement(
113+
ErrorBoundaryContext.Provider,
114+
{
115+
value: {
116+
didCatch,
117+
error,
118+
resetErrorBoundary: this.resetErrorBoundary,
119+
},
120+
},
121+
childToRender,
122+
)
123+
}
124+
}
125+
126+
function hasArrayChanged(a: any[] = [], b: any[] = []) {
127+
return (
128+
a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
129+
)
130+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createContext } from 'preact'
2+
3+
export type ErrorBoundaryContextType = {
4+
didCatch: boolean
5+
error: any
6+
resetErrorBoundary: (...args: any[]) => void
7+
}
8+
9+
export const ErrorBoundaryContext =
10+
createContext<ErrorBoundaryContextType | null>(null)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Custom Error Boundary port from 'react-error-boundary'
3+
* Taken directly from https://github.com/bvaughn/react-error-boundary/
4+
* and modified to server a preact use case
5+
*/
6+
7+
export * from './ErrorBoundary'
8+
export * from './ErrorBoundaryContext'
9+
export * from './types'
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
ComponentType,
3+
ErrorInfo,
4+
ComponentChildren,
5+
ComponentChild,
6+
} from 'preact'
7+
8+
export type FallbackProps = {
9+
error: any
10+
resetErrorBoundary: (...args: any[]) => void
11+
}
12+
13+
export type PropsWithChildren<P = {}> = P & {
14+
children?: ComponentChildren
15+
}
16+
17+
type ErrorBoundarySharedProps = PropsWithChildren<{
18+
onError?: (error: Error, info: ErrorInfo) => void
19+
onReset?: (
20+
details:
21+
| { reason: 'imperative-api'; args: any[] }
22+
| { reason: 'keys'; prev: any[] | undefined; next: any[] | undefined },
23+
) => void
24+
resetKeys?: any[]
25+
}>
26+
27+
export type ErrorBoundaryPropsWithComponent = ErrorBoundarySharedProps & {
28+
fallback?: never
29+
FallbackComponent: ComponentType<FallbackProps>
30+
fallbackRender?: never
31+
}
32+
33+
export type ErrorBoundaryPropsWithRender = ErrorBoundarySharedProps & {
34+
fallback?: never
35+
FallbackComponent?: never
36+
fallbackRender: (props: FallbackProps) => ComponentChild
37+
}
38+
39+
export type ErrorBoundaryPropsWithFallback = ErrorBoundarySharedProps & {
40+
fallback: ComponentChild
41+
FallbackComponent?: never
42+
fallbackRender?: never
43+
}
44+
45+
export type ErrorBoundaryProps =
46+
| ErrorBoundaryPropsWithFallback
47+
| ErrorBoundaryPropsWithComponent
48+
| ErrorBoundaryPropsWithRender

packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2-
import { act, fireEvent } from '@testing-library/preact'
2+
import { fireEvent } from '@testing-library/preact'
33
import { queryKey, sleep } from '@tanstack/query-test-utils'
44
import {
55
QueryCache,
@@ -13,7 +13,7 @@ import {
1313
import { renderWithClient } from './utils'
1414
import { useEffect, useState } from 'preact/hooks'
1515
import { Suspense } from 'preact/compat'
16-
import { ErrorBoundary } from './utils'
16+
import { ErrorBoundary } from './ErrorBoundary'
1717

1818
describe('QueryErrorResetBoundary', () => {
1919
beforeEach(() => {

packages/preact-query/src/__tests__/suspense.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2-
import { act, render } from '@testing-library/preact'
2+
import { render } from '@testing-library/preact'
33
import { queryKey, sleep } from '@tanstack/query-test-utils'
44
import { QueryClient, QueryClientProvider, useSuspenseQuery } from '..'
55
import type { QueryKey } from '..'

0 commit comments

Comments
 (0)