React-apollo: [Question] How to extend `Query` component

Created on 14 May 2018  路  11Comments  路  Source: apollographql/react-apollo

Within a typescript project I am looking to achieve something like this

<UserQuery variables={variables}>
  {({ data }) => {
      ...
  }}
</UserQuery />

Following on from the docs I am trying to get UserQuery to extend the Query component in a way that means I do not have to pass the query prop in manually. UserQuery should already know what query to run.

Most helpful comment

Hey, maybe this will be hepful to someone, but if you want to create your own Query component to handle queries in a consistant way across your app, I've done this with typescript.

The idea is that by default you render the spinner/error/empty state in the same way everywhere by default, so this reduces you from writing boilerplate that is similar across app screens (I use this on a RN app), and you only need to render the happy path.

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

type OmitChildren<T> = Omit<T,"children">

interface AppQueryProps<Data,Variables> {
  children: (data: Data,result: QueryResult<Data,Variables>) => ReactNode
  renderNetworkStatus?: (networkStatus: NetworkStatus, result: QueryResult<Data,Variables>) => ReactNode
  renderError?: (error: Error, result: QueryResult<Data,Variables>) => ReactNode
  renderNoData?: (result: QueryResult<Data,Variables>) => ReactNode
}

export class AppQuery<Data, Variables> extends React.Component<OmitChildren<QueryProps<Data, Variables>> & AppQueryProps<Data,Variables>> {
  defaultRenderNetworkStatus = (networkStatus: NetworkStatus, _result: QueryResult<Data, Variables>) => {
    if (networkStatus === NetworkStatus.loading) {
      return (
        <Centered style={{ flex: 1 }}>
          <AppSpinner/>
        </Centered>
      );
    }
    return null;
  };
  defaultRenderError = (_error: Error, result: QueryResult<Data, Variables>) => {
    if (!result.data) {
      return (
        <Centered style={{ flex: 1 }}>
          <AppText>Error</AppText>
        </Centered>
      );
    }
    return null;
  };
  defaultRenderNoData = (_result: QueryResult<Data, Variables>) => {
    return (
      <Centered style={{ flex: 1 }}>
        <AppText>No data</AppText>
      </Centered>
    );
  };
  render() {
    const {children, renderNetworkStatus, renderError, renderNoData,...queryProps} = this.props;
    return (
      <Query<Data,Variables>
        {...queryProps}
      >
        {(result: QueryResult<Data,Variables>) => {
          const networkStatusNode = (renderNetworkStatus || this.defaultRenderNetworkStatus)(result!.networkStatus,result);
          if ( networkStatusNode ) {
            return networkStatusNode;
          }
          const errorNode = result.error ? (renderError || this.defaultRenderError)(result!.error!,result) : undefined;
          if ( errorNode ) {
            return errorNode;
          }
          const noDataNode = !result.data ? (renderNoData || this.defaultRenderNoData)(result) : undefined;
          if ( noDataNode ) {
            return noDataNode;
          }
          return children(result.data!,result);
        }}
      </Query>
    );
  }
}

All 11 comments

You will still need to pass the query prop into the component. When you extend the Query class with the Data and Variables types, it make it so the data and variables are typed. You get errors if you try to pass in variables that don't match the provided type or try to access fields on data in an unsafe way. A lot of the other values passed to the children render prop also benefit from the type info (refetch, updateQuery, etc). Type information isn't going to be able to replace the passing of a prop though.

Thanks @TLadd. I thought there might be something I could do in the body of the extended class to set the query.

class UserQuery extends Query<
  User.Query,
  User.Variables
> {
  ... predefine what query to run here
}

I've been using query components which wraps the Apollo Query component and query description:

import React, { Component, ReactNode } from "react"
import { Query as ApolloQuery } from "react-apollo"
import gql from "graphql-tag"

interface Props {
  id: string
  children: (data?: Data) => ReactNode
}

interface Data {
  user: {
    id: string
    name: string
  }
}

interface Variables {
  id: string
}

export class UserQuery extends Component<Props> {
  render() {
    const { id, children } = this.props

    return (
      <Query query={query} variables={{ id }}>
        {result => children(result.data)}
      </Query>
    )
  }
}

class Query extends ApolloQuery<Data, Variables> {}

const query = gql`
  query User($id: String!) {
    user(id: $id) {
      id
      name
    }
  }
`

The you can use the query this way:

<UserQuery id={"0"}>
  {data => ... }
</UserQuery>

When the next version of Typescript comes out that includes this patch: https://github.com/Microsoft/TypeScript/pull/22415, it should hopefully be even easier:

render() {
  return (
    <Query<positionPageQuery, positionPageQueryVariables> query={POSITION_PAGE} variables={{ slug }}>
      {({ loading, error, data }) => {
        // data is strongly typed.
      })
    </Query>
  )
}

@majelbstoat Thats great this seems like it will solve my problem

Hey, maybe this will be hepful to someone, but if you want to create your own Query component to handle queries in a consistant way across your app, I've done this with typescript.

The idea is that by default you render the spinner/error/empty state in the same way everywhere by default, so this reduces you from writing boilerplate that is similar across app screens (I use this on a RN app), and you only need to render the happy path.

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

type OmitChildren<T> = Omit<T,"children">

interface AppQueryProps<Data,Variables> {
  children: (data: Data,result: QueryResult<Data,Variables>) => ReactNode
  renderNetworkStatus?: (networkStatus: NetworkStatus, result: QueryResult<Data,Variables>) => ReactNode
  renderError?: (error: Error, result: QueryResult<Data,Variables>) => ReactNode
  renderNoData?: (result: QueryResult<Data,Variables>) => ReactNode
}

export class AppQuery<Data, Variables> extends React.Component<OmitChildren<QueryProps<Data, Variables>> & AppQueryProps<Data,Variables>> {
  defaultRenderNetworkStatus = (networkStatus: NetworkStatus, _result: QueryResult<Data, Variables>) => {
    if (networkStatus === NetworkStatus.loading) {
      return (
        <Centered style={{ flex: 1 }}>
          <AppSpinner/>
        </Centered>
      );
    }
    return null;
  };
  defaultRenderError = (_error: Error, result: QueryResult<Data, Variables>) => {
    if (!result.data) {
      return (
        <Centered style={{ flex: 1 }}>
          <AppText>Error</AppText>
        </Centered>
      );
    }
    return null;
  };
  defaultRenderNoData = (_result: QueryResult<Data, Variables>) => {
    return (
      <Centered style={{ flex: 1 }}>
        <AppText>No data</AppText>
      </Centered>
    );
  };
  render() {
    const {children, renderNetworkStatus, renderError, renderNoData,...queryProps} = this.props;
    return (
      <Query<Data,Variables>
        {...queryProps}
      >
        {(result: QueryResult<Data,Variables>) => {
          const networkStatusNode = (renderNetworkStatus || this.defaultRenderNetworkStatus)(result!.networkStatus,result);
          if ( networkStatusNode ) {
            return networkStatusNode;
          }
          const errorNode = result.error ? (renderError || this.defaultRenderError)(result!.error!,result) : undefined;
          if ( errorNode ) {
            return errorNode;
          }
          const noDataNode = !result.data ? (renderNoData || this.defaultRenderNoData)(result) : undefined;
          if ( noDataNode ) {
            return noDataNode;
          }
          return children(result.data!,result);
        }}
      </Query>
    );
  }
}

@slorber that looks like what i was searching for though i get the following error with the current typescript version and your example. any idea?

Type '{ children: (result: QueryResult<Data, Variables>) => {} | null | undefined; query: DocumentNode; displayName?: string | undefined; skip?: boolean | undefined; onCompleted?: ((data: {} | Data) => void) | undefined; ... 9 more ...; partialRefetch?: boolean | undefined; }' is not assignable to type '{ readonly children: ((result: QueryResult<Data, Variables>) => ReactNode) | (string & ((result: QueryResult<Data, Variables>) => ReactNode)) | (number & ((result: QueryResult<Data, Variables>) => ReactNode)) | ... 4 more ... | (ReactPortal & ((result: QueryResult<...>) => ReactNode)); ... 13 more ...; readonly part...'.
  Types of property 'variables' are incompatible.
    Type 'Variables | undefined' is not assignable to type '(IsExactlyAny<Variables | undefined> extends true ? object | null | undefined : Variables | undefined) | undefined'.
      Type 'Variables' is not assignable to type '(IsExactlyAny<Variables | undefined> extends true ? object | null | undefined : Variables | undefined) | undefined'.
        Type 'Variables' is not assignable to type 'IsExactlyAny<Variables | undefined> extends true ? object | null | undefined : Variables | undefined'. [2322]

I am wrapping my query components the following way:

type QueryType = Omit<QueryProps<getSpacesByUserId, getSpacesByUserIdVariables>, 'query'>

export const GetSpacesByUserId: React.SFC<QueryType> = props => (
  <Query<getSpacesByUserId, getSpacesByUserIdVariables> {...props} query={query} />
)

And the Omit type i have declared in a types.d.ts file and it looks like this:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

The QueryType just takes the props from the Query Component and removes the query prop.

So now it can be used like this:

<GetSpacesByUserId variables={{userId: this.props.authUserId}}>{getSpacesQuery => {
    // ...
}}<GetSpacesByUserId/>

@OneCyrus The error you are seeing seems to be due to a change in @types/react 16.7.18, I posted an issue about it here https://github.com/DefinitelyTyped/DefinitelyTyped/issues/32588

@Herlevsen does that actually work with latest @types/react ? I still receive the same issue.

function test<getSpacesByUserId, getSpacesByUserIdVariables>(
  query: DocumentNode,
) {
  type QueryType = Omit<
    QueryProps<getSpacesByUserId, getSpacesByUserIdVariables>,
    'query'
  >;

  // Error
  const GetSpacesByUserId: React.SFC<QueryType> = (props) => (
    <Query<getSpacesByUserId, getSpacesByUserIdVariables>
      {...props}
      query={query}
    />
  );
}

@Slessi My comment was not a response to @OneCyrus. I am currently on React 16.x, and so are my type definitions, so unfortunately I don't know about that

Has anyone managed to solve the issue described by @OneCyrus and updated @slorber's component to @types/react >= 16.7.18?

Was this page helpful?
0 / 5 - 0 ratings