3

I am using React, Typescript and Apollo Client.

In my React component I query with useQuery hook NODES_TYPE_ONE or NODES_TYPE_TWO based on a value blockData.myType. This works fine.

The GraphQL queries looks like:

export const NODES_TYPE_ONE = gql`
  query GetNodesOne($customKey: String!) {
    getNodesTypeOne(customKey: $customKey) {
      nodes {
        id
        title
      }
    }
  }
`;

export const NODES_TYPE_TWO = gql`
  query GetNodesTwo($customKey: String!) {
    getNodesTypeTwo(customKey: $customKey) {
      nodes {
        id
        title
      }
    }
  }
`;

But how do I type my data in GqlRes type?

When I console.log(data); I get: two different objects:

getNodesTypeOne {
  nodes[// array of objects]
}

and

getNodesTypeTwo {
  nodes[// array of objects]
}

My GqlRes type:

export type GqlRes = {
  getNodesTypeOne: {
    nodes: NodeTeaser[];
  };
};

/** @jsx jsx */
import { useQuery } from '@apollo/client';
import { jsx } from '@emotion/react';

import { Slides } from 'app/components';

import { NODES_TYPE_ONE, NODES_TYPE_TWO } from './MyBlock.gql';
import { Props, GqlRes, NodesArgs } from './MyBlock.types';

const MyBlock = ({ data: blockData, metadata }: Props) => {
  const customKey = metadata.customKey;

  const { data } = useQuery<GqlRes, NodesArgs>(
    blockData.myType === 'type-one' ? NODES_TYPE_ONE : NODES_TYPE_TWO,
    {
      variables: {
        customKey: metadata.customKey || 0,
      },
      errorPolicy: 'all',
      notifyOnNetworkStatusChange: true,
      ssr: false,
    }
  );

  const items =
    data?.getNodesTypeOne.nodes.map((video) => {
      return {
        id: video.uuid,
        type: 'type-one',
        title: title,
      };
    }) || [];


  return <Slides items={items} /> : null;
};

export default MyBlock;

Now my items returns only getNodesTypeOne but how do I get them both?

Update:

I created a union type for GqlRes:

type GetNodeTypeOne = {
  getNodesTypeOne: {
    nodes: Teaser[];
  };
};

type GetNodeTypeTwo = {
  getNodesTypeTwo: {
    nodes: Teaser[];
  };
};

export type GqlRes = GetNodeTypeOne | GetNodeTypeTwo;

But how do I map the nodes array now?

Update 2

As mention by @Urmzd I tried another approach. Just use multiple useQuery hooks:

const MyBlock = ({ data: blockData, metadata }: Props) => {
      const customKey = metadata.customKey;
    
      const { data: nodesOne } = useQuery<NodesOneGqlRes, NodesArgs>(NODES_TYPE_ONE,
        {
          variables: {
            customKey: metadata.customKey || 0,
          },
          errorPolicy: 'all',
          notifyOnNetworkStatusChange: true,
          ssr: false,
        }
      );

const { data: nodesTwo } = useQuery<NodesTwoGqlRes, NodesArgs>(NODES_TYPE_TWO,
        {
          variables: {
            customKey: metadata.customKey || 0,
          },
          errorPolicy: 'all',
          notifyOnNetworkStatusChange: true,
          ssr: false,
        }
      );
    
    
      const items =
        data?.// How do I get my nodes in a single variable?? .map((video) => {
          return {
            id: video.uuid,
            type: 'type-one',
            title: title,
          };
        }) || [];
    
    
      return <Slides items={items} /> : null;
    };
    
    export default MyBlock;

But how do I map my data now, since I have two different GraphQL responses? And what is the best approach in this case?

5
  • Wait, I'm confused. Why do you have two entity containers that contain the exact same entity? Commented Nov 29, 2021 at 13:17
  • Also, when someone answers your question, you might want to expand on the thread and not just edit your own post. Commented Nov 29, 2021 at 13:17
  • Can you provide the gql queries you're making? Commented Nov 29, 2021 at 13:21
  • @Urmzd added the query to my question. Thanks Commented Nov 29, 2021 at 13:27
  • @Urmzd first try to get my example working. How do I map my union nodes array now? Commented Nov 29, 2021 at 14:02

2 Answers 2

1

If I understand your code directly then depending on the value of blockData.myType you're either executing one query or the other and you want to reuse the same useQuery hook for this logic. If you want that you'd need to make sure that GqlRes is a union type of getNodesTypeOne and getNodesTypeTwo.

// I don't know what NodeType is so I'm just using a string for this example
type NodeType = string

interface GetNodesTypeOne {
    readonly getNodesTypeOne: {
        readonly nodes: NodeType[]
    }
}

interface GetNodesTypeTwo {
    readonly getNodesTypeTwo: {
        readonly nodes: NodeType[]
    }
}

type GqlRes = GetNodesTypeOne | GetNodesTypeTwo

const resultOne:GqlRes = {
  getNodesTypeOne: {
    nodes: [ "test" ]
  }
}

const resultTwo:GqlRes = {
  getNodesTypeTwo: {
    nodes: [ "test" ]
  }
}

So this will solve the TypeScript issue. Then later in your code you're doing this:

  const items = data?.getNodesTypeOne.nodes.map(...)

Since data may contain either getNodesTypeOne or getNodesTypeTwo we need to change this to something else. A quick fix would be to just select the first one that has values:

const nodes = "getNodesTypeOne" in data 
    ? data?.getNodesTypeOne?.nodes 
    : data?.getNodesTypeTwo?.nodes
const items = nodes.map(...);

Or if you want to use the same condition:

const nodes = blockData.myType === 'type-one'
    ? (data as GetNodesTypeOne)?.getNodesTypeOne?.nodes 
    : (data as GetNodesTypeTwo)?.getNodesTypeTwo?.nodes
const items = nodes.map(...);

Note that in the second example we need to help TypeScript figure out the specific type by narrowing it down using a type assertion. In the first example this is not necessary because TypeScript is smart enough to figure out that the first expression will always result in a GetNodesTypeOne and the second expression will always result in a GetNodesTypeOne.


To answer your second question using the two separate queries:

  • Add a new variable useQueryOne which is true in case we're running query one and false in case we're running query two.
  • Add skip to useQuery to run only the appropriate query.
  • Add a new variable nodes that contains either the results from the first or from the second query (based on the useQueryOne condition)
const useQueryOne = blockData.myType === 'type-one';

const { data: nodesOne } = useQuery<NodesOneGqlRes, NodesArgs>(NODES_TYPE_ONE,
    {
        variables: {
            customKey: metadata.customKey || 0,
        },
        errorPolicy: 'all',
        notifyOnNetworkStatusChange: true,
        ssr: false,
        skip: !useQueryOne
    }
);

const { data: nodesTwo } = useQuery<NodesTwoGqlRes, NodesArgs>(NODES_TYPE_TWO,
    {
        variables: {
            customKey: metadata.customKey || 0,
        },
        errorPolicy: 'all',
        notifyOnNetworkStatusChange: true,
        ssr: false,
        skip: useQueryOne
    }
);

const nodes = useQueryOne
    ? nodesOne?.getNodesTypeOne?.nodes
    : nodesTwo?.getNodesTypeTwo?.nodes;
const items = (nodes || []).map(...);
Sign up to request clarification or add additional context in comments.

7 Comments

do you have any idea how to map over this union now?
ok approach is clear, thanks. But I get this typescript error: Property 'getNodesTypesOne' does not exist on type 'GqlRes'.. Seems like I have no acces to property getNodesTypesOne and getNodesTypesTwo?
@meez unfortunately Martin's code is incorrect, he should be using : instead of |. Also, it's not type-safe. You need to use a type-predicates. Again, all of this can be resolved by splitting the query.
I agree by the way with splitting the query: I'd personally never try to jam two different queries into the same hook (even if they're very similar). I just answered it with this constraint in mind since the author is looking specifically for a way to achieve this.
As @Urmzd correctly pointed out my answer was lacking the type narrowing required to satisfy the TypeScript checker, so I added those.
|
1

I should note that your queries are duplicates and hence should be refactored into a single query (unless they're just duplicate namespaces and not values).

Regardless, you can achieve your desired result and in a safer way using useLazyQuery

const [invokeQuery1, {loading, data, error}] = useLazyQuery<>(...)
const [invokeQuery2, {loading2, data2, error2}] = useLazyQuery<>(...)

// Run the query every time the condition changes.
useEffect(() => { 
  if (condition) {
    invokeQuery1()
  } else {
    invokeQuery2()
  }
}, [condition])

// Return the desired conditional daa
const {nodes} = useMemo(() => {
  return condition ? data?.getNodesTypeOne : data2?.getNodesTypeTwo
} , 
[condition])

This also ensures that computation is not done needlessly (and you can invoke your queries based on events, as they should be).

-- Edit

Since you're adamant about using the union types (and mapping the data from the source).

Here's one possible, type-safe approach.

const isNodeTypeOne = (t: unknown): t is GetNodeTypeOne => {
  return (t as GetNodeTypeOne).getNodesTypeOne !== undefined;
};

const { nodes } = isNodeTypeOne(data)
  ? data?.getNodesTypeOne
  : data?.getNodesTypeTwo;

const items = nodes.map((val) => {
  // Your mapping here
})

If you had different node types, you could also use predicates within the map.

4 Comments

@meez Updated to answer your other question.
I updated my question. Thanks
@meez Look at my first answer (useEffect and useMemo)
@meez did this solve your problem?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.