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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

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: be9e879

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-4 Minor
@suspensive/react-query-5 Minor
@suspensive/react-query 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 1 Co-authored-by: manudeli <[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]>
@gwansikk #1409 (review) 1 Co-authored-by: gwansikk <[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 Jan 9, 2025 7:20am
v1.suspensive.org ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 9, 2025 7:20am
visualization.suspensive.org ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 9, 2025 7:20am

Copy link

github-actions bot commented Jan 6, 2025

Size Change: +273 B (+0.4%)

Total Size: 69.2 kB

Filename Size Change
packages/react/dist/ErrorBoundary.cjs 2.83 kB +91 B (+3.33%)
packages/react/dist/index.cjs 4.41 kB +92 B (+2.13%)
packages/react/dist/index.js 367 B -1 B (-0.27%)
packages/react/dist/wrap.cjs 3.87 kB +92 B (+2.44%)
packages/react/dist/wrap.js 208 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.13 kB
packages/react-dom/dist/FadeIn.js 140 B
packages/react-dom/dist/index.cjs 2.36 kB
packages/react-dom/dist/index.js 177 B
packages/react-dom/dist/InView.cjs 2.1 kB
packages/react-dom/dist/InView.js 130 B
packages/react-dom/dist/useFadeIn.cjs 2.04 kB
packages/react-dom/dist/useFadeIn.js 133 B
packages/react-dom/dist/useInView.cjs 1.87 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.86 kB
packages/react-query-4/dist/index.js 394 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/QueryErrorBoundary.cjs 1.12 kB
packages/react-query-4/dist/QueryErrorBoundary.js 142 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 923 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 833 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.76 kB
packages/react-query-5/dist/index.js 391 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/QueryErrorBoundary.cjs 1.12 kB
packages/react-query-5/dist/QueryErrorBoundary.js 142 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 137 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 159 B
packages/react/dist/DevMode.cjs 530 B
packages/react/dist/DevMode.js 131 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
packages/react/dist/Suspensive.cjs 1.26 kB
packages/react/dist/Suspensive.js 175 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 74.20%. Comparing base (07b6b77) to head (be9e879).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1409      +/-   ##
==========================================
+ Coverage   73.89%   74.20%   +0.30%     
==========================================
  Files          70       70              
  Lines         590      597       +7     
  Branches      131      132       +1     
==========================================
+ Hits          436      443       +7     
  Misses        142      142              
  Partials       12       12              
Components Coverage Δ
@suspensive/react 100.00% <100.00%> (ø)
@suspensive/react-dom 95.55% <ø> (ø)
@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 42.85% <ø> (ø)

@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]>
Copy link
Collaborator

@gwansikk gwansikk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this change. It aligns with the same mechanism used by React Suspense, react-error-boundary (both propagate issues to the parent when a problem occurs in the fallback).

However, I’m concerned that this change could significantly impact existing users. I believe it qualifies as a breaking change, as updating might result in different behavior for previous users.

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? What if this behavior were made configurable(options)? (this is just my opinion.)

@fe-dudu
Copy link
Contributor

fe-dudu commented Jan 9, 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.

https://suspensive-ex.vercel.app
https://github.com/fe-dudu/suspensive-ex

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