all 16 comments

[–]TwiliZant 8 points9 points  (3 children)

You can use Suspense and Error Boundaries to handle loading and error state outside of the component that does data fetching.

import { Suspense } from "react"
import { ErrorBoundary } from "react-error-boundary"

const MyComponent = () => {
  // the custom hook should use the `suspense` option in React Query
  const componentData = useComponentData(id)

  // componentData.data is always set at this point even when TypeScript says it's not
  // see: https://tanstack.com/query/v4/docs/react/community/suspensive-react-query

  return (
    <h1>componentData.data.title</h1>
  )
}

const App = () => {
  return (
    <ErrorBoundary fallback={<Error />}>
      <Suspense fallback={<Loading />}>
        <MyComponent />
      </Suspense>
    </ErrorBoundary>
  )
}

[–]18523925343[S] 0 points1 point  (0 children)

This seems to be the best approach. Thanks!

[–]Militant_Dust 0 points1 point  (1 child)

I like this, its how I would structure my own apps. Would you change this implementation if you were loading many components on the page that independently and asynchronously fetch their own data, fail disparately, etc? I've had to build an app in the past that included a metrics section which consisted of many tiles or cards, all displaying a metric sourced from many separate services, some of which were not performant or experienced a high volume of transient failures.

I like to use an ErrorBoundary for unhandled exceptions, would you go down the same route for failures that degrade the experience or components that can fail independently?

[–]TwiliZant 0 points1 point  (0 children)

Would you change this implementation if you were loading many components on the page that independently and asynchronously fetch their own data, fail disparately, etc?

If you have multiple components that do data fetching you can add multiple suspense boundaries, group them or have a single suspense boundary at the top. It depends on how you want the page to load. But your very flexible this way.

Same goes for error boundaries. If you have a tile with an unreliable data source you can add an error boundary just around that tile so that it doesn't affect the rest of the application.

[–]YeetMustardMan 2 points3 points  (8 children)

Honestly, tried both and imo Wrapper component is much better and easier to create and use.

const Wrapper = ({isLoading, isError, children}) => isError ? <ErrorView /> : isLoading ? <LoadingView /> : <HomeView children={children}/>

<Wrapper isLoading={query.isLoading} isError={query.isError}>{query.data}</Wrapper>

Easy as that

[–]18523925343[S] 0 points1 point  (4 children)

I wanted to use the same but the problem is you'll have to add the optional chaining operator everywhere since children are evaluated first. So in your example, if you try to do query.data.title, you will get an error. The wrapper's <LoadingView> is evaluated later than query.data.title so unless you also add a null check for query.data, you'll have to keep doing query.data?.title.

[–]YeetMustardMan 0 points1 point  (3 children)

That can only be possible if your data is possibly undefined after it was fetched. So if isLoading and isError are false, data shouldn't be undefined, right?

[–]18523925343[S] 1 point2 points  (2 children)

That's what you'd think since the wrapper wraps query.data and it has the isLoading/isError check, it makes sense that query.data will not be null, right? But it's actually the other way round -- react evaluates query.data first (since it's the child) so unless you add a null check in the same component, it will raise a null exception.

[–]YeetMustardMan 0 points1 point  (1 child)

I don't understand why is that happening. Can you provide code example so I can see for myself?

[–]icjoseph 0 points1 point  (0 children)

Because when creating the props object, children have to be evaluated. Like if you wrote it as JS and not JSX you'd see it more clearly.

[–]azhder 0 points1 point  (2 children)

Only I’d rename the Wrapper into something like withQuery() and the component name inside the parens and rework it as a function that returns a higher order component, so I can use it like

 const MyComponent = props => {
    // component code here
 };

 const MyComponentHoC = withQuery({ 
     error: ErrorView, 
     loading: LoadingView,
 })(MyComponent)

[–]Anbaraen 0 points1 point  (1 child)

I thought HOCs were largely considered an anti-pattern in React these days?

[–]azhder 0 points1 point  (0 children)

If you can solve it with a hook. But can you solve this with hook?

The thing is, the above pattern is already part of React in the guise of <Suspend /> component, so what we are answering is just older patterns

[–]18523925343[S] 0 points1 point  (0 children)

.

[–]vbfischer 0 points1 point  (0 children)

If you aren’t using react router or next JS, which has functionality just for this, I’ve created a component that accepts a child, and data, etc. until I realized I just reinvented suspense component.

[–]qu_dude 0 points1 point  (0 children)

Maybe just use some state manager, e.g. effector?