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 istrue
in case we're running query one andfalse
in case we're running query two. - Add
skip
touseQuery
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(...);