Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

[Question] How to extend Query component #1980

Closed
malimccalla opened this issue May 14, 2018 · 11 comments
Closed

[Question] How to extend Query component #1980

malimccalla opened this issue May 14, 2018 · 11 comments

Comments

@malimccalla
Copy link

malimccalla commented May 14, 2018

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.

@malimccalla malimccalla changed the title Extending Query [Question] How to extend Query component May 14, 2018
@TLadd
Copy link
Contributor

TLadd commented May 15, 2018

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.

@malimccalla
Copy link
Author

malimccalla commented May 15, 2018

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
}

@jsslai
Copy link

jsslai commented May 16, 2018

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>

@majelbstoat
Copy link

majelbstoat commented May 22, 2018

When the next version of Typescript comes out that includes this patch: microsoft/TypeScript#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>
  )
}

@malimccalla
Copy link
Author

malimccalla commented May 28, 2018

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

@slorber
Copy link

slorber commented Nov 13, 2018

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>
    );
  }
}

@OneCyrus
Copy link

@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]

@Herlevsen
Copy link

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/>

@Slessi
Copy link

Slessi commented Jan 29, 2019

@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 DefinitelyTyped/DefinitelyTyped#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}
    />
  );
}

@Herlevsen
Copy link

@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

@drasive
Copy link

drasive commented Aug 2, 2019

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants