Next.js Discord

Discord Forum

Suspense and client components

Unanswered
PepeW posted this in #help-forum
Open in Discord
Avatar
PepeWOP
I'm working on a product page. The logic is the following:

// page.ts
export default async function ProductPage({ params }: { params: { productSlug: string } }) {
// api call to get the product
  return <ProductWrapper product={product} />
)


// loading.ts
export default async function Loading() {
  return <Skeleton />
)


// ProductWrapper.tsx
"use client"
export function ProductWrapper({ product }: ProductWrapperProps) {
  return (
    <div>
      {/* some components */}
      <Suspense fallback={<p>loading</p>}>
        <ProductVariantSelector
          product={product}
        />
      </Suspense>
    </div>
  )
)


// ProductVariantSelector.tsx
"use client"
export function ProductVariantSelector({ product }: ProductVariantSelectorProps) {
// a lot of computations to transform the product variants into a list of options
// the computation in not async (just some map(), filters(), reduce(), etc.)
  return (
    <ProductOptions options={options} />
  )
)


Here's the setup.

Now my question is regarding rendering and suspense.

According to me the rendering should happen this way:
1. the loading.ts is displayed while the product api call is being made
2. then, the ProductWrapper.tsx is rendered execpt the ProductVariantSelector.tsx (instead the fallback is displayed)
3. after that the computation is done, ProductVariantSelector.tsx is rendered.


But in fact this is not happening this way.
The loading.ts is working fine however the Suspense is not (the fallback is not displayed)

67 Replies

Avatar
PepeWOP
up
Avatar
Alfonsus Ardani
where is the product api being called?
Avatar
PepeWOP
that's a graphql request to my backend
Avatar
Alfonsus Ardani
yes but at which component
Avatar
PepeWOP
ProductPage in page.ts (there is a comment)
Avatar
Alfonsus Ardani
oh my bad
Avatar
PepeWOP
np
Avatar
Alfonsus Ardani
where is the loading.ts located?
at same level of page.tsx?
Avatar
PepeWOP
right next to the page
Avatar
Alfonsus Ardani
The fall back is not displayed because the one thats doing the async operation is ProductPage not ProductVariantSelector
if you want the suspense to work, there has to be async operation being awaited in side any component inside <Suspense>
Avatar
PepeWOP
aah ok !
So <Suspense /> only works with async operations
?
Avatar
Alfonsus Ardani
Generally yes. I havent fully explored any other unique use cases though
but thats how it works
Avatar
PepeWOP
And it could be a server or client component, if there is a async operation the <Suspense /> will work right ?
Avatar
Alfonsus Ardani
well, client component can't be async
so it has to be a server component
Avatar
PepeWOP
Let's say I have a useQuery() inside my cleint component
It would not work because the component itselft as to be async ?
Avatar
Alfonsus Ardani
useQuery is a client-side hook
client-side can't be async
therefore you can use useQuery in client component
its already irrelevant to Suspense because we're talking about something that is already in the scope of Client Component
Avatar
PepeWOP
Ok I understand
So in my case I want to render my ProductWrapper without waiting for the ProductVariantSelector to do its computation so how coud I do that ?
Avatar
Alfonsus Ardani
You may use Suspense with Client Component but afaik it has nothing to do with useQuery
unless you enable suspense in useQuery
Its just that the use of Suspense in client component is a bit uncommon
its outside of the scope of what im able to explain to you
// page.tsx
export default function ProductPage({ params }: { params: { productSlug: string } }) {
  return <div>
      {/* some CLIENT components */}
      <Suspense fallback={<p>loading</p>}>
        <ProductVariantSelector/>
      </Suspense>
    </div>
)
// still on the same page.tsx
function ProductVariantSelector(){
  // Api CALL to get the product
  return (
    <ProductVariantSelectorClient product={product}>
  )
}
Avatar
PepeWOP
Ok I see what you did but my question is not about the product. In fact I need it inside ProductWrapper.

In my question I added a comment inside ProductVariantSelector saying that it's doing a lot of computations (map(), filter(), reduce(), etc).
This computation can take up to 8sec (in the worst scenarios) so I would like not to wait for this component to render the page.

My initial solution was to use the Suspense but like you explained it doesn't work for client components.
So my question is: Is there a way to differ the render of this component so the rest of the page do not wait for it ?
Avatar
Alfonsus Ardani
do you want Product Page to render as soon as fetch finishes happen or before fetch finished responding?
Avatar
PepeWOP
I want to keep the loading.ts and the api call inside the page, that behaviour is working as I want
Avatar
Alfonsus Ardani
well, afaik you dont need <suspense> then
just conditionally render in the client side using useQuery :D
Avatar
PepeWOP
However I want my ProductWrapper to render without wait for ProductVariantSelector
I know that Suspense is useless now
its not that fully useless, you can use it but its still fairly new
i recommend doing whats written in the blog since its more intuitive :D
Avatar
PepeWOP
There is a mismatch between what I'm trying to achieve and what you're explaining ^^
I'll try to be as clear as possible.

I'm not using any useQuery().
The product api call is looking something like this: const product = await productGraphqlRequest(productSlug).
My problem is not about this api call and the loading.ts. This part is working as expected.

My question is about the render of ProductVariantSelector.
This component is doing a lot of computation (map(), filter, reduce(), etc) that can take up to 8sec.
So I don't want to wait for this component when rendering ProductWrapper.

My original solution was doing:
javascript 
// ProductWrapper.tsx
"use client"
export function ProductWrapper({ product }: ProductWrapperProps) {
  return (
    <div>
      {/* some components */}
      <Suspense fallback={<p>loading</p>}>
        <ProductVariantSelector
          product={product}
        />
      </Suspense>
    </div>
  )
)


But as you explained, doing a <Suspense /> in this scenario is useless.

So my question is: How can I render ProductWrapper and all its components without waiting for ProductVariantSelector (and it's computation) ?
Avatar
Alfonsus Ardani
depends on where you want the computation to happen
do you want the computation to happen in the server or in the client?
Avatar
PepeWOP
I want my computation to stay inside ProductVariantSelector
I've been reading a lot of the doc while speaking.

Is next/dynamic a good solution ?

Doing something like this:
const ProductVariantSelector = dynamic(
  () =>
    import("../features/product/ProductVariantSelector").then((mod) => mod.ProductVariantSelector),
  {
    loading: () => <p>Loading...</p>,
    ssr: false,
  }
)
Avatar
Alfonsus Ardani
the solution isnt limited to react query. However, you can implement what react query do with raw js. use query just makes your live easier without needing to worry about recomputing at component re-mount
const [finished, setFinished] = useState(false)
const [data, setData] = useState(null)
useEffect(()=>{
  async function compute(){
    const data = await expensiveComputationData(params.product)
    setData(data)
    setFinished(true)
  }
  compute()
},[])

if(!finished) return <>Loading... </>

else{
  return <>{data}</>
}
<Suspense> would probably make the code tidier but i havent tried that approach.
and i dont think its the right use case for dynamic()
Avatar
PepeWOP
Hmmm ok
You did this: const data = await expensiveComputationData(params.product)

But the computation is not async
Just a bunch of filter(), map(), reduce(), etc
to transform the data
Avatar
Alfonsus Ardani
then make promise :D
Avatar
PepeWOP
Ok I'll try
And out of curiosity why do you think that dynamic import is not the right use case ?
Because after trying it works as expected
I can the see page except ProductVariantSelector (only its fallback) and after some time it renders
Avatar
Alfonsus Ardani
its for lazy loading. are you sure the computation are still done in the browser?
Avatar
PepeWOP
Yes because it's working
Avatar
Alfonsus Ardani
hmmm
might want to checkout next/dynamic again then
especially its use with Suspense
Avatar
PepeWOP
I've remove the Suspense around ProductVariantSelector
Avatar
Alfonsus Ardani
oh i meant the loading attribute, my bad