Customizing Cell behavior

Customizing Cell behavior

I have a Cell that displays items in a shopping cart.

I ran in to two problems while doing this

First problem

To grab all the items a gql query is made, with a unique shopping cart ID passed as a parameter. This ID is stored in global state, and might not always be available when this Cell is being displayed. But most likely it will be available very shortly after.

So I display my cell like this

<CartCell checkoutId={checkoutId} />

I could do this instead

{checkoutId && <CartCell checkoutId={checkoutId} />}

But I wanted the Cell’s “loading” state to be shown while waiting for the checkout ID to be populated in my global state

So what I did in my Cell was this

export const Failure = ({ error, variables }) => {
  if (!variables.checkoutId) {
    return <Loading />
  }

  return <div>Error: {error.message}</div>
}

And this works, but I’m getting error logs in my console: Error: Variable "$checkoutId" of required type "String!" was not provided.

My second try at handling this what like so (notice the skip):

export const beforeQuery = (props) => {
  return {
    variables: props,
    fetchPolicy: 'network-only',
    skip: !props.checkoutId,
  }
}

But doing that, I get this error: Uncaught Cannot render cell: graphQL success but data is null

So what I’m doing now is importing both the entire Cell plus the Loading state component and displaying them like this

{checkoutId && <CartCell checkoutId={checkoutId} />}
{!checkoutId && <Loading />}

Is this the best way to do it?

Second problem

When getting the cart, and it’s empty, I’d like to show the “empty” state, i.e. render <Empty />. But to do that I have to do this

export const Success = ({ cart }) => {
  if (cart.items.length === 0) {
    return <Empty />
  }

  return <CartTable cart={cart} />
}

Solution/Discussion points/Ideas

If it was possible to customize the “loading” state condition I could include checkoutId === undefined there and not need the if (!variables.checkoutId) return <Loading /> check in the “failure” state.

If it was possible to customize the “empty” state condition I could include cart.items.length === 0 there, and not need the special handling in the “success” state.

If Cells added a “skipped” state, with a corresponding <Skipped /> component, Cells would be able to handle setting skipped: true in beforeQuery. Users would be free to export const Skipped = Failure or export const Skipped = Empty, depending on what behavior is wanted.

What do you all think of the problem, and my ideas to solve it?

2 Likes

I would consider the checkoutId not being available as a separate loading state (of the parent). Perhaps this loading state could have more dependencies in the future. More state that your app needs to wait for to do anything.

In that case I think your solution is good:

{checkoutId && <CartCell checkoutId={checkoutId} />}
{!checkoutId && <Loading />}

However I would write it something like this:

if (!checkoutId) return <Loading />

return <CartCell checkoutId={checkoutId} />

Depending on where the checkoutId comes from you could even turn the parent component into its own Cell! :slight_smile:

CartCell
Does your CartCell fetch anything besides items?

If so, you could consider splitting the queries into a cart query and an items query.

If not, you could consider querying just for the items.

If neither of those options sound like a good idea, I think it’s still a perfectly viable solution to handle those things in Success. Cells definitely don’t fit every use case!

2 Likes

Yeah, that does make sense.

I have other things I’m rendering as well, that’s why I chose my solution.

Interesting idea, but I don’t think that would work in my case as the checkoutId comes from global state. If it came from a gql query, then I could have done that :slight_smile:

Yes, it does fetch other things in that query as well.
When you say to split them, do you mean at the SDL level? Instead of just

type Query {
  getCart
}

I’d have

type Query {
  getCartItems
  getCartXYZ
}

Like that?
Can I run both in the same cell if I split them like that?

Yes something like that. I don’t know if it’s a good fit tho, the point of GraphQL is kind of that you don’t have to split the calls anymore :man_shrugging:

You could do it in 1 Cell I think, but that would kind of beat the purpose I suppose, since you’d want the Empty state to show up for the items, not for the other Cart data.


I’ve been thinking a bit about your custom state conditions, and what I thought about before is writing your own state machines in XState which would map the states to components in your file. This way you could kind of define your own Cell behaviour!

I’m not very experienced with Apollo and its possibilities, but I feel like the Skipped state would be kind of specific? I’m not entirely sure about all the use-cases of skip tho!

Nice flower example :sunflower: But I think I’d have to see a bigger example to know how I feel about it.

I only found out about skip yesterday, so I have no idea how it’s supposed to be used… Just throwing ideas out there :wink:

I really like the ideas of using cells, and while they will never fit every use case, I’m all for making them as flexible as possible (without making them too complicated) to be able to use them as much as possible

1 Like

I solved this now by changing my resolver function to return null if there are no items. That way the “empty” state is triggered in my Cell.

1 Like