👀 Check out the changes in Suspensive v2 read more →
Documentation
@suspensive/react
Migrating to v2

Migrating to v2

What's new

New <DevMode/> #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>
  )
}