👀 Check out the changes in Suspensive v2 read more →
Documentation
Migrating to v2

Migrating to Suspensive v2

Suspensive v2, we focused on increasing compatibility and improving DX. To accomplish this, we had to introduce some dramatic changes. As a result, we removed some previously deprecated features and added some new interfaces.

The minimum required React version is now 18.0

Suspensive v2 requires React 18.0 or later. This is because we are using the new useSyncExternalStore, useId hook, which is only available in React 18.0 and later. Previously, we have been using the shim provided by React. In addition, in React 18, Suspense-related features were added (opens in a new tab), We decided that the direction of the Suspensive libraries needed to focus on React 18 or higher in the future rather than responding to Legacy React.

@suspensive/react

New <DevMode/> component #470 (opens in a new tab)

This is our new feature for DX with new devMode prop of <Suspense/>, <ErrorBoundary/>

We have seen many people use way such as forcing a fallback into each component to test the fallback directly or reloading the page to re-fetch. so We added the devMode prop to each <Suspense/> and <ErrorBoundary/> to easily test the fallback of them.

devMode example

import { useState } from 'react'
import { DevMode, Suspensive, SuspensiveProvider, Suspense, ErrorBoundary } from '@suspensive/react'
 
const Example = () => {
  const [suspensive] = useState(new Suspensive())
 
  return (
    <SuspensiveProvider value={suspensive}>
      {/* This devMode prop will work only in development mode */}
      <Suspense fallback={<>loading...</>} devMode={{ showFallback: true }}>
        {/* children */}
      </Suspense>
      <ErrorBoundary fallback={<>error</>} devMode={{ showFallback: true }}>
        {/* children */}
      </ErrorBoundary>
      {/* This <DevMode/> component will appear only in development mode */}
      {/* If developer click <DevMode/>, devMode prop of <Suspense/> <ErrorBoundary/> will be activated */}
      <DevMode />
    </SuspensiveProvider>
  )
}

New wrap builder #270 (opens in a new tab)

A new feature that wraps a component in <Suspense/>, <ErrorBoundary/>, and <ErrorBoundaryGroup/> all at once.

For <Suspense/>, <ErrorBoundary/>, <ErrorBoundaryGroup/>, etc. many people use hoc to wrap these around a component. This is because these components require some processing on their children. So, in order to not unnecessarily divide components and create depth, we use the hocs for each interface, withErrorBoundary, withErrorBoundaryGroup, and withSuspense, but as we often use each hoc in combination, we also need to improve readability. To improve this, we decided to provide wrap.

import { wrap } from '@suspensive/react'
import { useSuspenseQuery } from '@suspensive/react-query'
 
const Example = wrap
  .ErrorBoundaryGroup({ blockOutside: false })
  .ErrorBoundary({ fallback: ({ error }) => <>{error.message}</>, onError: logger.log })
  .Suspense({ fallback: <>loading...</>, clientOnly: true })
  .on(() => {
    const query = useSuspenseQuery({
      queryKey: ['key'],
      queryFn: () => api.text(),
    })
    return <>{query.data.text}</>
  })

New shouldCatch prop of <ErrorBoundary/> #569 (opens in a new tab)

Suspensive's <ErrorBoundary/> can catch all thrown errors that occur in children. However, because it catches all thrown errors, when using <ErrorBoundary/>, I thought about placing <ErrorBoundary/> in a narrower position. For this reason, we added a new prop called shouldCatch to ErrorBoundary, which allows you to set which Error should be caught.

ErrorBoundary shouldCatch example

  1. shouldCatch: ErrorConstructor
import { ErrorBoundary } from '@suspensive/react'
 
class CustomError extends Error {}
 
const Example = () => {
  return (
    <ErrorBoundary fallback={({ error }) => <>RestError: {error.message}</>}>
      <ErrorBoundary
        shouldCatch={CustomError}
        onError={logOnCustomError}
        fallback={({ error }) => <>CustomError: {error.message}</>}
      >
        <ThrowErrorComponent />
      </ErrorBoundary>
    </ErrorBoundary>
  )
}
  1. shouldCatch: callback
import { ErrorBoundary } from '@suspensive/react'
 
class CustomError extends Error {}
 
const Example = () => {
  return (
    <ErrorBoundary fallback={({ error }) => <>RestError: {error.message}</>}>
      <ErrorBoundary
        shouldCatch={(error) => error instanceof CustomError}
        onError={logOnCustomError}
        fallback={({ error }) => <>CustomError: {error.message}</>}
      >
        <ThrowErrorComponent />
      </ErrorBoundary>
    </ErrorBoundary>
  )
}
  1. shouldCatch: boolean
import { ErrorBoundary } from '@suspensive/react'
 
class CustomError extends Error {}
 
const Example = () => {
  return (
    <ErrorBoundary fallback={({ error }) => <>RestError: {error.message}</>}>
      <ErrorBoundary
        shouldCatch={new Date().toISOString() > '2024-01-01T00:00:00.000Z'}
        onError={logOnErrorAfter2024}
        fallback={({ error }) => <>ErrorAfter2024: {error.message}</>}
      >
        <ThrowErrorComponent />
      </ErrorBoundary>
    </ErrorBoundary>
  )
}

New <ErrorBoundary.Consumer/>, <ErrorBoundaryGroup.Consumer/> #610 (opens in a new tab)

These components can be used to use useErrorBoundary, useErrorBoundaryGroup in jsx inlinely

import { ErrorBoundary, ErrorBoundaryGroup } from '@suspensive/react'
 
const Example = () => {
  return (
    <ErrorBoundaryGroup>
      <ErrorBoundaryGroup.Consumer>
        {({ reset }) => <button onClick={reset}>reset all</button>}
      </ErrorBoundaryGroup.Consumer>
      <ErrorBoundary fallback={({ error }) => <>{error.message}</>}>
        <ErrorBoundary.Consumer>
          {({ setError }) => <button onClick={() => setError(new Error('error message'))}>setError</button>}
        </ErrorBoundary.Consumer>
      </ErrorBoundary>
    </ErrorBoundaryGroup>
  )
}

Handling BREAKING CHANGES

Removed <AsyncBoundary/>

We removed <AsyncBoundary/> in v2 #295 (opens in a new tab)

Because <AsyncBoundary/> uses <ErrorBoundary/> internally, it can be used with useErrorBoundary and is affected by <ErrorBoundaryGroup/>. We decided to remove this component from v2, believing that it would be better for maintainability and interface unification for library users.

<AsyncBoundary/>'s feature is just wrap two component(<Suspense/>, <Errorboundry/>) by one. So, you can split by two like this.

+ import { Suspense, Errorboundry } from '@suspensive/react'
- import { AsyncBoundary } from '@suspensive/react'
 
+ <Errorboundry fallback={<Error />} onError={onError} onReset={onReset}>
+   <Suspense fallback={<Loading />}>
+     <Children />
+   </Suspense>
+ </Errorboundry>
- <AsyncBoundary pendingFallback={<Loading />} rejectedFallback={<Error />} onError={onError} onReset={onReset}>
-   <Children />
- </AsyncBoundary>

Removed withSuspense, withDelay, withErrorboundry, withErrorBoundaryGroup

These all hocs can be replaced beautifully by new hoc builder wrap in v2

+ import { wrap } from '@suspensive/react'
- import { withSuspense, withErrorBoundary, withErrorBoundaryGroup } from '@suspensive/react'
 
+ const Example = wrap
+   .ErrorBoundaryGroup({ blockOutside: false })
+   .ErrorBoundary({ fallback: ({ error }) => <>{error.message}</>, onError: logger.log })
+   .Suspense({ fallback: <>loading...</>, clientOnly: true })
+   .on(() => {
+     const query = useSuspenseQuery({
+       queryKey: ['key'],
+       queryFn: () => api.text(),
+     })
+     return <>{query.data.text}</>
+   })
- const Example = withErrorBoundaryGroup(
-   withErrorBoundary(
-     withSuspense(
-       () => {
-         const query = useSuspenseQuery({
-           queryKey: ['key'],
-           queryFn: () => api.text(),
-         })
-         return <>{query.data.text}</>
-       },
-       { fallback: <>loading...</>, clientOnly: true }
-     ),
-     { fallback: ({ error }) => <>{error.message}</>, onError: logger.log }
-   ),
-   { blockOutside: false }
- )
+ import { wrap } from '@suspensive/react'
- import { withSuspense } from '@suspensive/react'
 
+ const Example = wrap
+   .Suspense({
+     fallback: <>loading...</>,
+     clientOnly: true,
+   })
+   .on(() => {
+     const query = useSuspenseQuery({
+       queryKey: ['key'],
+       queryFn: () => api.text(),
+     })
+     return <>{query.data.text}</>
+   })
- const Example = withSuspense(
-   () => {
-     const query = useSuspenseQuery({
-       queryKey: ['key'],
-       queryFn: () => api.text(),
-     })
-     return <>{query.data.text}</>
-   },
-   {
-     fallback: <>loading...</>,
-     clientOnly: true,
-   }
- )

Removed <ErrorBoundaryGroup.Reset/>

<ErrorBoundaryGroup.Reset/> just use useErrorBoundaryGroup internally. so We thought that changing it to something like Context.Consumer would make the component's behavior easier to understand for React developers. We changed the name to <ErrorBoundaryGroup.Consumer/> and kept the interface the same.

import { ErrorBoundaryGroup } from '@suspensive/react'
 
const Exmample = () => {
  return (
    <ErrorBoundaryGroup>
-     <ErrorBoundaryGroup.Reset trigger={(group) => <button onClick={group.reset}>reset all</button>} />
+     <ErrorBoundaryGroup.Consumer>
+       {(group) => <button onClick={group.reset}>reset all</button>}
+     </ErrorBoundaryGroup.Consumer>
    </ErrorBoundaryGroup>
  )
}

Rename defaultOptionsdefaultProps of Suspensive

import { ErrorBoundaryGroup } from '@suspensive/react'
 
const suspensive = new Suspensive({
- defaultOptions: {
+ defaultProps: {
    suspense: {
      fallback: 'default loading...',
    },
  },
})

Rename <Suspense.CSROnly/><Suspense clientOnly/> as prop

import { Suspense } from '@suspensive/react'
 
const Example = () => {
  return (
-   <Suspense.CSROnly fallback={<>loading...</>}>
+   <Suspense clientOnly fallback={<>loading...</>}>
      <>children</>
    </Suspense.CSROnly>
  )
}

@suspensive/react-query

@suspensive/react-query v1 was initially one of the community resources for TanStack Query v4 and was a library that provided previews of useSuspenseQuery, useSuspenseQueries, and useSuspenseInfiniteQuery in @tanstack/react-query v4 not officially.

However, @tanstack/react-query v5 officially added useSuspenseQuery, useSuspenseQueries, and useSuspenseInfiniteQuery as hooks for Suspense. So, we want to make @suspensive/react-query v2 a library by making the interface as similar to the original as possible to make it easier for developers to migrate to @tanstack/react-query v5.

And @tanstack/react-query v4 is still useful in teams that have difficulty introducing the latest version due to the high browser specifications required by @tanstack/react-query v5.

Even in such cases, we want to make this library useful if you want to use hooks for suspense.

Handling BREAKING CHANGES

Removed enabled, placeholderData option of useSuspenseQuery

import { useSuspenseQuery } from '@suspensive/react-query'
 
const Example = () => {
  const query = useSuspenseQuery({
    queryKey: ['key'],
    queryFn: () => api.text()
-   enabled: Math.random() > 0.5,
-   placeholderData: 'placeholder'
  })
}

Removed enabled, placeholderData option of useSuspenseInfiniteQuery

import { useSuspenseInfiniteQuery } from '@suspensive/react-query'
 
const Example = () => {
  const infiniteQuery = useSuspenseInfiniteQuery({
    queryKey: ['key'],
    queryFn: () => api.text()
-   enabled: Math.random() > 0.5,
-   placeholderData: 'placeholder'
  })
}

Unified interface naming for options and return type of useSuspenseQuery same with @tanstack/react-query v5's useSuspenseQuery

index.ts of @suspensive/react-query
export { useSuspenseQuery } from './useSuspenseQuery'
export type {
- BaseUseSuspenseQueryResult,
  UseSuspenseQueryOptions,
- UseSuspenseQueryResultOnLoading,
- UseSuspenseQueryResultOnSuccess,
+ UseSuspenseQueryResult
} from './useSuspenseQuery'
export { useSuspenseQueries } from './useSuspenseQueries'
+ export type { SuspenseQueriesOptions, SuspenseQueriesResults } from './useSuspenseQueries'
export { useSuspenseInfiniteQuery } from './useSuspenseInfiniteQuery'
export type {
- BaseUseSuspenseInfiniteQueryResult,
  UseSuspenseInfiniteQueryOptions,
- UseSuspenseInfiniteQueryResultOnLoading,
- UseSuspenseInfiniteQueryResultOnSuccess,
+ UseSuspenseInfiniteQueryResult
} from './useSuspenseInfiniteQuery'
- export { QueryAsyncBoundary } from './QueryAsyncBoundary'
export { QueryErrorBoundary } from './QueryErrorBoundary'

Thanks to all Suspensive contributors

We were able to release v2 because of Suspensive's contributors. Thank you to everyone who worked together to make it a better library, and we look forward to your continued support.

contributors (opens in a new tab)