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

feat(react): prevent recursive exposing fallback when fallback throw error #1409

Merged
merged 16 commits into from
Feb 1, 2025

Conversation

manudeli
Copy link
Member

@manudeli manudeli commented Jan 6, 2025

🚧 This PR could make a huge difference, so please help us make sure this change gets a very thorough review. 🚧

const Throw = {
  Error: ({ message, after = 0, children }: PropsWithChildren<{ message: string; after?: number }>) => {
    const [isNeedThrow, setIsNeedThrow] = useState(after === 0)
    if (isNeedThrow) {
      throw new Error(message)
    }
    useTimeout(() => setIsNeedThrow(true), after)
    return <>{children}</>
  }
}

const Example = () => (
  <ErrorBoundary fallback={() => <>This is expected</>}>
    <ErrorBoundary
      fallback={() => (
        <Throw.Error message={ERROR_MESSAGE} after={100}>
          ErrorBoundary's fallback before error
        </Throw.Error>
      )}
    >
      <Throw.Error message={ERROR_MESSAGE} after={100}>
        ErrorBoundary's children before error
      </Throw.Error>
    </ErrorBoundary>
  </ErrorBoundary>
)

Problem: ErrorBoundary's fallback can't be treated by parent ErrorBoundary

Thrown Error in fallback will be caught by ErrorBoundary self and then expose fallback recursively 🥲

  1. ErrorBoundary's children before error
  2. ErrorBoundary's fallback before error
  3. ErrorBoundary's fallback before error
  4. ErrorBoundary's fallback before error
  5. ... expose fallback self recursively ...

Solution: When we meet thrown error in fallback of ErrorBoundary wrap it as InternalFallbackError, re-throw InternalFallbackError.fallbackError, if it is InternalFallbackError

Thrown Error in fallback will be caught by parent ErrorBoundary 👍

  1. ErrorBoundary's children before error
  2. ErrorBoundary's fallback before error
  3. This is expected

PR Checklist

  • I did below actions if need
  1. I read the Contributing Guide
  2. I added documents and tests.

@manudeli manudeli self-assigned this Jan 6, 2025
Copy link

changeset-bot bot commented Jan 6, 2025

🦋 Changeset detected

Latest commit: 17817fe

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@suspensive/react Minor
@suspensive/react-query Minor
@suspensive/react-query-4 Minor
@suspensive/react-query-5 Minor
@suspensive/jotai Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

coauthors bot commented Jan 6, 2025

People can be co-author:

Candidate Reasons Count Add this as commit message
@manudeli #1409 (comment) #1409 (comment) #1409 (comment) #1409 4 Co-authored-by: manudeli <[email protected]>
@gwansikk #1409 (review) #1409 (comment) 2 Co-authored-by: gwansikk <[email protected]>
@codecov-commenter #1409 (comment) 1 Co-authored-by: codecov-commenter <[email protected]>
@fe-dudu #1409 (comment) 1 Co-authored-by: fe-dudu <[email protected]>

Copy link

vercel bot commented Jan 6, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
suspensive.org ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 1, 2025 11:05am
v1.suspensive.org ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 1, 2025 11:05am
visualization.suspensive.org ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 1, 2025 11:05am

Copy link

github-actions bot commented Jan 6, 2025

Size Change: +277 B (+0.44%)

Total Size: 63.8 kB

Filename Size Change
packages/react/dist/ErrorBoundary.cjs 2.82 kB +92 B (+3.37%)
packages/react/dist/index.cjs 4.19 kB +90 B (+2.19%)
packages/react/dist/index.js 331 B +2 B (+0.61%)
packages/react/dist/wrap.cjs 3.86 kB +92 B (+2.44%)
packages/react/dist/wrap.js 209 B +1 B (+0.48%)
ℹ️ View Unchanged
Filename Size
packages/jotai/dist/Atom.cjs 660 B
packages/jotai/dist/Atom.js 115 B
packages/jotai/dist/AtomValue.cjs 647 B
packages/jotai/dist/AtomValue.js 120 B
packages/jotai/dist/index.cjs 755 B
packages/jotai/dist/index.js 158 B
packages/jotai/dist/SetAtom.cjs 645 B
packages/jotai/dist/SetAtom.js 118 B
packages/react-dom/dist/FadeIn.cjs 2.16 kB
packages/react-dom/dist/FadeIn.js 139 B
packages/react-dom/dist/index.cjs 2.38 kB
packages/react-dom/dist/index.js 176 B
packages/react-dom/dist/InView.cjs 2.12 kB
packages/react-dom/dist/InView.js 130 B
packages/react-dom/dist/useFadeIn.cjs 2.06 kB
packages/react-dom/dist/useFadeIn.js 133 B
packages/react-dom/dist/useInView.cjs 1.89 kB
packages/react-dom/dist/useInView.js 120 B
packages/react-native/dist/index.cjs 619 B
packages/react-native/dist/index.js 122 B
packages/react-native/dist/TestText.cjs 612 B
packages/react-native/dist/TestText.js 119 B
packages/react-query-4/dist/index.cjs 1.63 kB
packages/react-query-4/dist/index.js 372 B
packages/react-query-4/dist/infiniteQueryOptions.cjs 548 B
packages/react-query-4/dist/infiniteQueryOptions.js 144 B
packages/react-query-4/dist/Mutation.cjs 821 B
packages/react-query-4/dist/Mutation.js 132 B
packages/react-query-4/dist/PrefetchInfiniteQuery.cjs 722 B
packages/react-query-4/dist/PrefetchInfiniteQuery.js 155 B
packages/react-query-4/dist/PrefetchQuery.cjs 712 B
packages/react-query-4/dist/PrefetchQuery.js 147 B
packages/react-query-4/dist/QueryClientConsumer.cjs 665 B
packages/react-query-4/dist/QueryClientConsumer.js 140 B
packages/react-query-4/dist/queryOptions.cjs 540 B
packages/react-query-4/dist/queryOptions.js 135 B
packages/react-query-4/dist/SuspenseInfiniteQuery.cjs 1.05 kB
packages/react-query-4/dist/SuspenseInfiniteQuery.js 155 B
packages/react-query-4/dist/SuspenseQueries.cjs 928 B
packages/react-query-4/dist/SuspenseQueries.js 149 B
packages/react-query-4/dist/SuspenseQuery.cjs 1.04 kB
packages/react-query-4/dist/SuspenseQuery.js 147 B
packages/react-query-4/dist/usePrefetchInfiniteQuery.cjs 648 B
packages/react-query-4/dist/usePrefetchInfiniteQuery.js 148 B
packages/react-query-4/dist/usePrefetchQuery.cjs 639 B
packages/react-query-4/dist/usePrefetchQuery.js 140 B
packages/react-query-4/dist/useSuspenseInfiniteQuery.cjs 833 B
packages/react-query-4/dist/useSuspenseInfiniteQuery.js 148 B
packages/react-query-4/dist/useSuspenseQueries.cjs 837 B
packages/react-query-4/dist/useSuspenseQueries.js 142 B
packages/react-query-4/dist/useSuspenseQuery.cjs 824 B
packages/react-query-4/dist/useSuspenseQuery.js 140 B
packages/react-query-5/dist/index.cjs 1.36 kB
packages/react-query-5/dist/index.js 370 B
packages/react-query-5/dist/infiniteQueryOptions.cjs 573 B
packages/react-query-5/dist/infiniteQueryOptions.js 144 B
packages/react-query-5/dist/Mutation.cjs 821 B
packages/react-query-5/dist/Mutation.js 132 B
packages/react-query-5/dist/PrefetchInfiniteQuery.cjs 647 B
packages/react-query-5/dist/PrefetchInfiniteQuery.js 145 B
packages/react-query-5/dist/PrefetchQuery.cjs 639 B
packages/react-query-5/dist/PrefetchQuery.js 137 B
packages/react-query-5/dist/QueryClientConsumer.cjs 663 B
packages/react-query-5/dist/QueryClientConsumer.js 140 B
packages/react-query-5/dist/queryOptions.cjs 563 B
packages/react-query-5/dist/queryOptions.js 136 B
packages/react-query-5/dist/SuspenseInfiniteQuery.cjs 833 B
packages/react-query-5/dist/SuspenseInfiniteQuery.js 145 B
packages/react-query-5/dist/SuspenseQueries.cjs 671 B
packages/react-query-5/dist/SuspenseQueries.js 139 B
packages/react-query-5/dist/SuspenseQuery.cjs 825 B
packages/react-query-5/dist/SuspenseQuery.js 136 B
packages/react-query-5/dist/usePrefetchInfiniteQuery.cjs 577 B
packages/react-query-5/dist/usePrefetchInfiniteQuery.js 148 B
packages/react-query-5/dist/usePrefetchQuery.cjs 569 B
packages/react-query-5/dist/usePrefetchQuery.js 140 B
packages/react-query-5/dist/useSuspenseInfiniteQuery.cjs 577 B
packages/react-query-5/dist/useSuspenseInfiniteQuery.js 148 B
packages/react-query-5/dist/useSuspenseQueries.cjs 571 B
packages/react-query-5/dist/useSuspenseQueries.js 142 B
packages/react-query-5/dist/useSuspenseQuery.cjs 569 B
packages/react-query-5/dist/useSuspenseQuery.js 140 B
packages/react-query/dist/index.cjs 551 B
packages/react-query/dist/index.js 121 B
packages/react-query/dist/v4.cjs 550 B
packages/react-query/dist/v4.js 116 B
packages/react-query/dist/v5.cjs 550 B
packages/react-query/dist/v5.js 116 B
packages/react/dist/ClientOnly.cjs 734 B
packages/react/dist/ClientOnly.js 141 B
packages/react/dist/DefaultProps.cjs 1.05 kB
packages/react/dist/DefaultProps.js 167 B
packages/react/dist/Delay.cjs 1.23 kB
packages/react/dist/Delay.js 158 B
packages/react/dist/ErrorBoundary.js 205 B
packages/react/dist/ErrorBoundaryGroup.cjs 1.38 kB
packages/react/dist/ErrorBoundaryGroup.js 195 B
packages/react/dist/Suspense.cjs 1.29 kB
packages/react/dist/Suspense.js 171 B

compressed-size-action

@codecov-commenter
Copy link

codecov-commenter commented Jan 6, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 79.40%. Comparing base (59a809d) to head (17817fe).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             next    #1409      +/-   ##
==========================================
+ Coverage   79.15%   79.40%   +0.25%     
==========================================
  Files          67       67              
  Lines         566      573       +7     
  Branches      125      126       +1     
==========================================
+ Hits          448      455       +7     
  Misses        113      113              
  Partials        5        5              
Components Coverage Δ
@suspensive/react 100.00% <100.00%> (ø)
@suspensive/react-dom 95.60% <ø> (ø)
@suspensive/react-native 100.00% <ø> (ø)
@suspensive/react-query 83.47% <ø> (ø)
@suspensive/react-query-4 0.00% <ø> (ø)
@suspensive/react-query-5 0.00% <ø> (ø)
@suspensive/jotai 0.00% <ø> (ø)
@suspensive/codemods 67.03% <ø> (ø)

@manudeli manudeli marked this pull request as ready for review January 6, 2025 14:11
@manudeli manudeli changed the title feat(react): prevent recursive expose fallback when fallback throw error feat(react): prevent recursive exposing fallback when fallback throw error Jan 6, 2025
Co-authored-by: lucas0530 <[email protected]>
Co-authored-by: HYUNGU KANG <[email protected]>
@manudeli
Copy link
Member Author

manudeli commented Feb 1, 2025

Hi, I'm a big fan of Suspensive. I have a suggestion 🙌

@manudeli I wish <Throw.Error/> could be changed.

because I want to see the ErrorBoundary if an error occurs when executing invalidateQueries after fetching data to useSuspenseQuery or useSuspenseQueries.

After reading the react-query docs | useSuspenseQuery, I made a component like this. (before I saw this PR)

interface Props {
  isFetching: boolean;
  error: Error | null;
  children: React.ReactNode;
}

export default function ThrowError({ isFetching, error, children }: Props) {
  if (error && !isFetching) {
    throw error;
  }

  return <>{children}</>;
}
<SuspenseQuery {...itemsQueryOptions()}>
  {({ data: items , isFetching, error }) => (
    <ThrowError isFetching={isFetching} error={error}>
      <QueryClientConsumer>
        {queryClient => (
          <>
            {items.map(item => (
              <ItemCard
                key={item.id}
                item={item}
                onClick={() => {
                  queryClient.invalidateQueries(itemsQueryOptions());
                }}
              />
            ))}
          </>
        )}
      </QueryClientConsumer>
    </ThrowError>
  )}
</SuspenseQuery>

I think ThrowError will be necessary to throw the error to the parent when the error occurs in the refetch caused by invalidateQueries.

I hope the <Throw.Error/> you made functions similarly to my <ThrowError/>.

The link below is a website that has an API error with a 50% probability. When you click on the card, invalidateQueries works.

suspensive-ex.vercel.app fe-dudu/suspensive-ex

I agree that it would be good to discuss whether such an interface is needed, but I think adding such an interface would be better discussed somewhere else than this Pull Request. Make issue or pull request please.

@manudeli
Copy link
Member Author

manudeli commented Feb 1, 2025

Additionally, looking at it differently, I think a looping fallback could be an optional feature. Why must errors within a fallback propagate to the parent fallback?

@gwansikk I think developers will expect a similar implementation, as the official React documentation introduces ErrorBoundary as an interface that replaces try catch.

image

try catch

try {
  try {
    throw new Error('throwed in try');
  } catch {
    throw new Error('throwed in catch inside'); // infinite loop does not occur
  }
} catch (e) {
  console.log(e); // caught
}

Expected ErrorBoundary

<ErrorBoundary fallback={({ error }) => console.log(error)}> // I thought the throw that occurred in fallback(catch) should be caught at the top.
  <ErrorBoundary fallback={() => { throw new Error('throwed in fallback(catch)') }}> // I thought an infinite loop shouldn't happen
    {createElement(() => {
      throw new Error('throwed in children(try)')
    })}
  </ErrorBoundary>
</ErrorBoundary>

What if this behavior were made configurable(options)? (this is just my opinion.)

I think configurable looping fallback is not good idea. I think the more settings a library user has to know, the harder it becomes to understand. Also, exposing fallbacks loop doesn't have the advantage of being configurable.

Since this is a breaking change, it seems like it would be easier to migrate by providing a prop that guarantees the same behavior as the existing ErrorBoundary. However, it would be nice if this prop had @deprecated in jsdoc.

I think it would be better to continue this in another Pull Request/issue to provide a configurable prop to make it behave like the existing ErrorBoundary, so I'll merge it first. 👍

@manudeli manudeli merged commit 124238c into next Feb 1, 2025
14 checks passed
@manudeli manudeli deleted the feat/prevent-recursive-fallback-throw-error branch February 1, 2025 11:36
@gwansikk
Copy link
Collaborator

gwansikk commented Feb 1, 2025

Since this is a breaking change, it seems like it would be easier to migrate by providing a prop that guarantees the same behavior as the existing ErrorBoundary. However, it would be nice if this prop had @deprecated in jsdoc.

I think it would be better to continue this in another Pull Request/issue to provide a configurable prop to make it behave like the existing ErrorBoundary, so I'll merge it first. 👍

Great, I completely understand and agree!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants