๐Ÿ‘€ Suspensive v2์—์„œ์˜ ๋ณ€๊ฒฝ์„ ํ™•์ธํ•˜์„ธ์š”. ๋”๋ณด๊ธฐ โ†’
๋ฌธ์„œ๋ณด๊ธฐ@suspensive/react<ErrorBoundary/>

ErrorBoundary

์ด ์ปดํฌ๋„ŒํŠธ๋Š” children์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

props.fallback

<ErrorBoundary/>์˜ children์— error๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด error๋Š” ์žกํžˆ๊ณ  fallback์ด ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

import { ErrorBoundary } from '@suspensive/react'
import { useState, useEffect } from 'react'
 
const Example = () => (
  <ErrorBoundary
    fallback={(props) => (
      <>
        <button onClick={props.reset}>Try again</button>
        {props.error.message}
      </>
    )}
  >
    <ErrorAfter2s />
  </ErrorBoundary>
)
import { ErrorBoundary } from '@suspensive/react'
import { useState, useEffect } from 'react'
import { ErrorAfter2s } from './ErrorAfter2s'

export const Example = () => {
  return (
    <ErrorBoundary
      fallback={(props) => (
        <>
          <button onClick={props.reset}>Try again</button>
          {props.error.message}
        </>
      )}
    >
      <ErrorAfter2s />
    </ErrorBoundary>
  )
}

<ErrorBoundary/>์˜ fallback์œผ๋กœ ์ „๋‹ฌํ•  ์ปดํฌ๋„ŒํŠธ ์ •์˜ํ•˜๊ธฐ

ErrorBoundaryFallbackProps

<ErrorBoundary/>์˜ fallback์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ErrorBoundaryFallbackProps ํƒ€์ž…์„ ํ™œ์šฉํ•ด ์‰ฝ๊ฒŒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ ์–ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import type { ErrorBoundaryFallbackProps } from '@suspensive/react'
 
const ErrorBoundaryFallback = ({
  reset,
  error,
}: ErrorBoundaryFallbackProps) => (
  <>
    <button onClick={reset}>reset</button>
    {error.message}
  </>
)
 
const Example = () => (
  <ErrorBoundary fallback={ErrorBoundaryFallback}>
    <ErrorAfter2s />
  </ErrorBoundary>
)
๐Ÿ’ก

<ErrorBoundary/> fallback props์„ prop drilling ์—†์ด ์‚ฌ์šฉํ•˜๊ธฐ

useErrorBoundaryFallbackProps

error ๊ฐ์ฒด์™€ reset ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ค‘์ฒฉ๋˜๋ฉด prop drilling์„ ํ”ผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด ๋•Œ, useErrorBoundaryFallbackProps์„ ํ†ตํ•ด, prop drilling ์—†์ด reset ๋ฉ”์†Œ๋“œ์™€ error ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { ErrorBoundary, useErrorBoundaryFallbackProps } from '@suspensive/react'
 
const Nested = () => {
  const { reset, error } = useErrorBoundaryFallbackProps()
 
  return (
    <>
      <button onClick={reset}>Try again</button>
      {error.message}
    </>
  )
}
 
// ์—ฌ๊ธฐ์„œ fallbackProp ์„ ์ „๋‹ฌํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง‘๋‹ˆ๋‹ค!
const ErrorBoundaryFallback = () => <Nested />
 
const Example = () => (
  <ErrorBoundary fallback={ErrorBoundaryFallback}>
    <ErrorAfter2s />
  </ErrorBoundary>
)

props.resetKeys

<ErrorBoundary/>์˜ fallback ์™ธ๋ถ€์— ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ <ErrorBoundary/>๋ฅผ resetํ•˜๋ ค๋ฉด resetKeys๋ฐฐ์—ด์— resetKey๋ฅผ ํ• ๋‹นํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. resetKeys๋Š” ๋ฐฐ์—ด์˜ ํ•˜๋‚˜ ์ด์ƒ์˜ ์š”์†Œ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ์—๋งŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. useEffect์˜ ์ข…์†์„ฑ ๋ฐฐ์—ด์ด ์ž‘๋™ํ•˜๋Š” ๋ฐฉ์‹๊ณผ ๊ฐ™์ด resetKeys๋กœ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ ๋ฐฐ์—ด์„ ์ฃผ์ž…ํ•˜๋Š” ๊ฒƒ์„ ๊ฑฑ์ •ํ•  ํ•„์š”๋„ ์—†์Šต๋‹ˆ๋‹ค.

import { ErrorBoundary } from '@suspensive/react'
import { useState, useEffect } from 'react'
 
const Example = () => {
  const [resetKey, setResetKey] = useState(0)
 
  return (
    <>
      <button onClick={() => setResetKey((prev) => prev + 1)}>Try again</button>
      <ErrorBoundary
        resetKeys={[resetKey]}
        fallback={(props) => <>{props.error.message}</>}
      >
        <ErrorAfter2s />
      </ErrorBoundary>
    </>
  )
}
import { ErrorBoundary } from '@suspensive/react'
import { useState, useEffect } from 'react'
import { ErrorAfter2s } from './ErrorAfter2s'

export const Example = () => {
  const [resetKey, setResetKey] = useState(0)

  return (
    <>
      <button onClick={() => setResetKey((prev) => prev + 1)}>Try again</button>
      <ErrorBoundary
        resetKeys={[resetKey]}
        fallback={(props) => <>{props.error.message}</>}
      >
        <ErrorAfter2s />
      </ErrorBoundary>
    </>
  )
}

props.onReset

<ErrorBoundary/>๊ฐ€ resetํ•  ๋•Œ ๋จผ์ € ํ˜ธ์ถœ๋˜๋Š” callback์ž…๋‹ˆ๋‹ค. @tanstack/react-query์™€๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { ErrorBoundary } from '@suspensive/react'
import { QueryErrorResetBoundary } from '@tanstack/react-query'
 
const Example = () => (
  <QueryErrorResetBoundary>
    {({ reset }) => (
      <ErrorBoundary
        onReset={reset}
        fallback={(props) => (
          <>
            <button onClick={props.reset}>Try again</button>
            {props.error.message}
          </>
        )}
      >
        <Page />
      </ErrorBoundary>
    )}
  </QueryErrorResetBoundary>
)
import { ErrorBoundary } from '@suspensive/react'
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { Page } from './Page'

export const Example = () => (
  <QueryErrorResetBoundary>
    {({ reset }) => (
      <ErrorBoundary
        onReset={reset}
        fallback={(props) => (
          <>
            <button onClick={props.reset}>Try again</button>
            {props.error.message}
          </>
        )}
      >
        <Page />
      </ErrorBoundary>
    )}
  </QueryErrorResetBoundary>
)

props.onError

<ErrorBoundary/>๊ฐ€ error๋ฅผ ์žก์„ ๋•Œ ํ˜ธ์ถœ๋˜๋Š” callback์ž…๋‹ˆ๋‹ค.

import { ErrorBoundary } from '@suspensive/react'
 
const logError = (error: Error, info: ErrorInfo) => {
  // ...
}
 
const Example = (
  <ErrorBoundary fallback={ErrorBoundaryFallback} onError={logError}>
    <ErrorAfter2s />
  </ErrorBoundary>
)
import { ErrorBoundary } from '@suspensive/react'
import { ErrorAfter2s } from './ErrorAfter2s'

const logError = (error: Error, info: ErrorInfo) => {
  console.log(error, info)
}

export const Example = () => {
  return (
    <ErrorBoundary
      fallback={(props) => (
        <>
          <button onClick={props.reset}>Try again</button>
          {props.error.message}
        </>
      )}
      onError={logError}
    >
      <ErrorAfter2s />
    </ErrorBoundary>
  )
}

props.shouldCatch

shouldCatch๋Š” ์กฐ๊ฑด์— ๋”ฐ๋ผ <ErrorBoundary/>๊ฐ€ ์—๋Ÿฌ๋ฅผ ์žก์„์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

Boolean, ErrorConstructor, Callback์˜ 3๊ฐ€์ง€ ๊ธฐ์ค€์„ ๋ฐ›์œผ๋ฉฐ ๊ธฐ๋ณธ๊ฐ’์€ true์ž…๋‹ˆ๋‹ค.

import { ErrorBoundary } from '@suspensive/react'
import { useState, useEffect, createElement } from 'react'
 
export const Example = () => {
  return (
    <ErrorBoundary
      fallback={({ error }) => (
        <>Parent ErrorBoundary fallback: {error.message}</>
      )}
    >
      <ErrorBoundary
        shouldCatch={false}
        fallback={({ error }) => (
          <>Child ErrorBoundary fallback: {error.message}</>
        )}
      >
        <CustomErrorAfter2s />
      </ErrorBoundary>
    </ErrorBoundary>
  )
}

๋ฐฐ์—ด์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ ์กฐ๊ฑด์„ ์ ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

import { ErrorBoundary } from '@suspensive/react'
import { useState, useEffect, createElement } from 'react'
 
const Example = () => {
  return (
    <ErrorBoundary
      fallback={({ error }) => (
        <>Parent ErrorBoundary fallback: {error.message}</>
      )}
    >
      <ErrorBoundary
        shouldCatch={[
          false,
          CustomError,
          (error) => error instanceof CustomError,
        ]}
        fallback={({ error }) => (
          <>Child ErrorBoundary fallback: {error.message}</>
        )}
      >
        <CustomErrorAfter2s />
      </ErrorBoundary>
    </ErrorBoundary>
  )
}
import { ErrorBoundary } from '@suspensive/react'
import { useState, useEffect, createElement } from 'react'

export const Example = () => {
  return (
    <ErrorBoundary
      fallback={({ error }) => (
        <>Parent ErrorBoundary fallback: {error.message}</>
      )}
    >
      <ErrorBoundary
        shouldCatch={CustomError}
        fallback={({ error }) => (
          <>Child ErrorBoundary fallback: {error.message}</>
        )}
      >
        <CustomErrorAfter2s />
      </ErrorBoundary>
    </ErrorBoundary>
  )
}

export class CustomError extends Error {
  constructor(...args: ConstructorParameters<ErrorConstructor>) {
    super(...args)
    console.error(...args)
  }
}

export const CustomErrorAfter2s = () => {
  const [asyncState, setAsyncState] = useState<
    { isError: true; error: CustomError } | { isError: false; error: null }
  >({
    isError: false,
    error: null,
  })

  useEffect(() => {
    setTimeout(() => {
      setAsyncState({
        isError: true,
        error: () => new CustomError('error made by CustomError'),
      })
    }, 2000)
  }, [])

  if (asyncState.isError) {
    throw asyncState.error()
  }

  return <>No error</>
}

import { ErrorBoundary } from '@suspensive/react'
import { useState, useEffect, createElement } from 'react'

export const Example = () => {
  return (
    <ErrorBoundary
      fallback={({ error }) => (
        <>Parent ErrorBoundary fallback: {error.message}</>
      )}
    >
      <ErrorBoundary
        shouldCatch={CustomError}
        fallback={({ error }) => (
          <>Child ErrorBoundary fallback: {error.message}</>
        )}
      >
        <ErrorAfter2s />
      </ErrorBoundary>
    </ErrorBoundary>
  )
}

export class CustomError extends Error {
  constructor(...args: ConstructorParameters<ErrorConstructor>) {
    super(...args)
    console.error(...args)
  }
}

export const ErrorAfter2s = () => {
  const [asyncState, setAsyncState] = useState<
    { isError: true; error: Error } | { isError: false; error: null }
  >({
    isError: false,
    error: null,
  })

  useEffect(() => {
    setTimeout(() => {
      setAsyncState({ isError: true, error: new Error('error made by Error') })
    }, 2000)
  }, [])

  if (asyncState.isError) {
    throw asyncState.error
  }

  return <>No error</>
}

useErrorBoundary

useErrorBoundary().setError

<ErrorBoundary/>์˜ children์—์„œ useErrorBoundary().setError์„ ์‚ฌ์šฉํ•ด throw ์—†์ด๋„ <ErrorBoundary/>์—์„œ Error๋ฅผ ์•Œ๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'
import { useEffect } from 'react'
 
const Example = () => (
  <ErrorBoundary fallback={ErrorBoundaryFallback}>
    <SetErrorAfterFetch />
  </ErrorBoundary>
)
 
const SetErrorAfterFetch = () => {
  const errorBoundary = useErrorBoundary()
 
  useEffect(() => {
    fetchSomething().then(
      (response) => {},
      (error) => errorBoundary.setError(error) // instead of throw inside
    )
  }, [])
 
  return <>No error</>
}
import { ErrorBoundary, useErrorBoundary } from '@suspensive/react'
import { useEffect } from 'react'
import { ErrorBoundaryFallback } from './ErrorBoundaryFallback'
import { fetchSomething } from './fetchSomething'

export const Example = () => (
  <ErrorBoundary fallback={ErrorBoundaryFallback}>
    <SetErrorAfterFetch />
  </ErrorBoundary>
)

const SetErrorAfterFetch = () => {
  const errorBoundary = useErrorBoundary()

  useEffect(() => {
    fetchSomething().then(
      (response) => {},
      (error) => errorBoundary.setError(error) // instead of throw inside
    )
  }, [])

  return <>No error</>
}
๐Ÿ’ก

๋‹ค์ˆ˜์˜ <ErrorBoundary/>๋ฅผ ์ œ์–ดํ•˜๊ธฐ

<ErrorBoundary/>์€ <ErrorBoundaryGroup/>๊ณผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ๊ฐ•๋ ฅํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. <ErrorBoundaryGroup/>๋กœ ๋‹ค์ˆ˜์˜ <ErrorBoundary/>๋ฅผ ์ œ์–ดํ•˜์„ธ์š”.
์ž์„ธํ•œ ๋‚ด์šฉ์€ <ErrorBoundaryGroup/>ํŽ˜์ด์ง€์—์„œ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.