Home > Enterprise >  How do I map my Union typed GraphQL response Array in React and Typescript
How do I map my Union typed GraphQL response Array in React and Typescript

Time:11-30

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?

CodePudding user response:

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.

CodePudding user response:

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(...);
  • Related