Declarative rendering with Apollo Client results

tmikeschu

Mike Schutte

Posted on December 7, 2020

Declarative rendering with Apollo Client results

RedwoodJS introduces a great abstraction for dealing with query results from Apollo Client: cells.

If you've used Apollo Client before, you've probably written something like the following hundreds of times.

const { loading, data } = useQuery(...)
if (loading) {
  return ...
}

if (data.length === 0) {
  return ...
}

return (
  ...
)
Enter fullscreen mode Exit fullscreen mode

Am I wrong?

I love the idea of cells. I can tell it's a great abstraction because there's no need to port my whole app over to RedwoodJS to get the same immediate declarative improvements. Here is a Redwoods-y utility function to render the result of a GraphQL query in any codebase with Apollo Client query results.

import * as React from "react";
import { ApolloError, QueryResult } from "@apollo/client";

const isEmpty = (data: NonNullable<QueryResult["data"]>): boolean => {
  const dataValue = data[Object.keys(data)[0]];
  return (
    dataValue === null || (Array.isArray(dataValue) && dataValue.length === 0)
  );
};

export const renderResult = <T extends QueryResult>(
  result: T,
  options: {
    isEmpty?: (data: NonNullable<T["data"]>) => boolean;
    Loading: React.FC;
    Failure: React.FC<{ error: ApolloError }>;
    Empty: React.FC;
    Success: React.FC<{ data: NonNullable<T["data"]> }>;
  }
): React.ReactElement => {
  return result.loading ? (
    <options.Loading />
  ) : result.error ? (
    <options.Failure error={result.error} />
  ) : (options.isEmpty ?? isEmpty)(result.data) ? (
    <options.Empty />
  ) : (
    <options.Success data={result.data} />
  );
};
Enter fullscreen mode Exit fullscreen mode

We can pass a custom isEmpty function if the shape of our data is more unique than the basic case.

Example usage:

import * as React from "react";

import { render } from "lib/render-result";

const MyComponent: React.FC = () => {
  const result = useQuery(...)
  return render(result, {
    Loading,
    Failure,
    Success,
    Empty,
    isEmpty: (data) => data.customPath.toCheck.length === 0
  });
};

export default MyComponent;

const Loading: React.FC = () => {
  return ...
};

const Empty: React.FC = () => {
  return ...
};

type Data = NonNullable<QueryResult["data"]>;
const Success: React.FC<{ data: Data }> = ({ data }) => {
  return ...
};

type FetchError = NonNullable<QueryResult["error"]>;
const Failure: React.FC<{ error: FetchError }> = ({ error }) => {
  console.error(error);
  return ...
};
Enter fullscreen mode Exit fullscreen mode

✌️

💖 💪 🙅 🚩
tmikeschu
Mike Schutte

Posted on December 7, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related