loading...
Cover image for Quick React GraphQL Tips

Quick React GraphQL Tips

clschnei profile image Chris Schneider ・2 min read

These are some patterns I've found helpful for using GraphQL in a pragmatic fashion to reduce headaches when building applications. These examples will be overly simple, and lean toward being more expressive.

One query !== better

Prop drilling and squeezing all your data into a single query can lead to poor abstraction and unnecessary component coupling. Think about only what is necessary to make a particular component render in isolation.

Before:

function Comments({ value = [] }) {
  return value.map(({ body, author }) => (
    <p>
      {author.name}: {body}
    </p>
  ));
}

function Article() {
  // Here I'm querying for both the article AND it's comments.
  // This means they will load in and render at the same time.
  const { data, loading } = useQuery(ARTICLE);
  const { title, body, comments, author } = data;

  return loading ? 'Loading article and comments...' : (
    <div>
      <h1>
        "{title}" by: {author.name}
      </h1>
      <p>{body}</p>

      <h4>Comments:</h4>
      <Comments value={comments} />
    </div>
  );
}

After:

function Comments({ value }) {
  const { data, loading } = useQuery(COMMENTS, { variables: { id: value }});

  return loading 
    ? 'Loading comments...' 
    : data.comments.map(({ body, author }) => (
        <p>
          {author.name}: {body}
        </p>
      ));
}

function Article() {
  // Independent loading of article data and comment data.
  // Allows the separate queries to load/render at their own pace.
  const { data, loading } = useQuery(ARTICLE, { variables: { id: 123 } });
  const { id, title, body, author } = data;

  return (
    <div>
      {loading ? 'Loading article...' : (
        <Fragment>
          <h1>
            "{title}" by: {author.name}
          </h1>
          <p>{body}</p>
        </Fragment>
      )}

      <h4>Comments:</h4>
      <Comments value={id} />
    </div>
  );
}

Bind mutations directly to "action" components

Co-locating your mutation methods with any buttons/elements that trigger them enables good control inversion. Parent components will pass contextual information and child components handle execution.

Before:

function Comments({ value, onDelete }) {
  const { data } = useQuery(COMMENTS, { variables: { id: value }});

  return data.comments.map(({ id, body, author }) => (
    <div>
      <p>
        {author.name}: {body}
      </p>

      <button onClick={() => onDelete(id)}>Delete</button>
    </div>
  ));
}

function Article() {
  const { data } = useQuery(ARTICLE, { variables: { id: 123 } });
  const { id, title, body, author } = data;

  const [deleteComment] = useMutation(DELETE);

  return (
    <div>
      <h1>
        "{title}" by: {author.name}
      </h1>
      <p>{body}</p>

      <h4>Comments:</h4>
      <Comments 
        value={id} 
        onDelete={id => {
          deleteComment({ variables: { id }})
        }}
      />
    </div>
  );
}

After:

function DeleteCommentButton({ value }) {
  const [deleteComment] = useMutation(DELETE);

  return (
    <button onClick={() => deleteComment({ variables: { id: value } })}>Delete</button>
  )
}

function Comments({ value }) {
  const { data } = useQuery(COMMENTS, { variables: { id: value } });

  return data.comments.map(({ id, body, author }) => (
    <div>
      <p>
        {author.name}: {body}
      </p>

      <DeleteCommentButton value={id} />
    </div>
  ));
}

function Article() {
  const { data } = useQuery(ARTICLE, { variables: { id: 123 } });
  const { id, title, body, author } = data;

  return (
    <div>
      <h1>
        "{title}" by: {author.name}
      </h1>
      <p>{body}</p>

      <h4>Comments:</h4>
      <Comments value={id} />
    </div>
  );
}

Discussion

markdown guide