Run sequental graphql queries using Apollo Client - reactjs

I am trying to load data using below queries in React.
export const GET_POSTS = gql`
query GetLaunches($limit: Int) {
launches(limit: $limit) {
name
id
date_utc
}
}
`;
I am using above query like below inside React component.
const {loading, error, data} = useQuery(GET_POSTS, {
variables: { limit },
skip: !limit
});
Above return data like below.
data = {
items: [
{
date_utc: "2014-01-06T18:06:00.000Z"
id: ['9D1B7E0']
name: "Thaicom 6"
},
{
date_utc: "2014-01-06T18:06:00.000Z"
id: ['1S1B5D0']
name: "Thai 0"
},
{
date_utc: "2014-01-06T18:06:00.000Z"
id: ['7F1B7E3']
name: "Sun 6"
},
{
date_utc: "2014-01-06T18:06:00.000Z"
id: ['4A1B5K7']
name: "Moon 0"
}
]
}
Now what I want to make another query where I will pass above id as a variable to another query and load description of each individual above list of items.
I tried below. Below is queries.ts file.
import { gql } from 'apollo-boost';
export const GET_POSTS = gql`
query GetLaunches($limit: Int) {
launches(limit: $limit) {
name
id
date_utc
}
}
`;
export const GET_DESCRIPTION = gql`
query GetDescription($id: ID!) {
mission(id: $id) {
description
}
}
`;
Here is Home.tsx file.
import React, { useEffect, useState } from 'react';
import { useQuery } from '#apollo/react-hooks';
import { HomeContainer } from '../Container/';
import { GET_POSTS, GET_DESCRIPTION } from '../Queries/';
export const Home: React.FC = () => {
const limit = 10;
const launches = useQuery(GET_LAUNCHES, {
variables: { limit },
skip: !limit
});
const launchDescription = useQuery(GET_DESCRIPTION, {
variables: { id: launches?.data?.launches?.mission_id?.[0] },
skip: launches.loading || !launches?.data?.launches?.mission_id?.[0],
});
useEffect(() => {
console.log(launchDescription.data); // returns undefined
console.log(launches.data); // this returns correct data
}, [launches]);
return (
<HomeContainer
loading={launches.loading}
error={launches.error}
data={launches.data}
/>
);
}
Inside useEffect hook, I want to add description to each individual launch in launches.data array. How can I make this work?

You need to use optional chaining for launches?.loading or simply just omit this to !launches?.data?.launches?.mission_id?.[0]

Both of your queries right now are fired at the same time. What you need to do is make the second query wait for the first one to be completed (using useLazyQuery), and only fire when the ID is available.
Here is how that should look:
import React, { useEffect, useState } from 'react';
import { useQuery, useLazyQuery } from '#apollo/react-hooks';
import { HomeContainer } from '../Container/';
import { GET_POSTS, GET_DESCRIPTION } from '../Queries/';
export const Home: React.FC = () => {
const limit = 10;
const launches = useQuery(GET_POSTS, {
variables: { limit },
skip: !limit
});
// useLazyQuery provides a function you can use to fire the query
// only when you have what you need for it
const [getLaunchDescription, launchDescription] = useLazyQuery(GET_DESCRIPTION)
useEffect(() => {
// check that you have the id before you ask for the description
if (launches?.data?.launches?.mission_id?.length) {
getLaunchDescription({
variables: { id: launches.data.launches.mission_id[0] }
});
}
}, [launches])
return (
<HomeContainer
loading={launches.loading}
error={launches.error}
data={launches.data}
/>
);
}

Related

React Testing Library's waitFor methods don't work, only wait

I'm having a React Native application of which packages I've recently upgraded to newest versions (with a few exceptions).
I have a very simple component:
import React, { FC } from 'react';
import { useParams } from 'react-router-native';
import { View } from 'react-native';
import { useItemByIdQuery } from '../../hooks/apollo/apollo-generated';
export const Item: FC = () => {
const { itemId } = useParams<{ itemId: string }>();
const { data, previousData, loading, error } = useItemByIdQuery({
variables: { _id: itemId },
notifyOnNetworkStatusChange: true,
});
const item = (data || previousData)?.getItemById;
console.log(data, previousData, error, loading, itemId);
if (!item && loading) {
return <View testID='spinner' />;
}
return null;
};
It returns null to keep it simple, let's say we only want to test that the spinner disappears.
import React from 'react';
import { MockedResponse } from '#apollo/client/testing';
import * as ReactRouter from 'react-router-native';
import { GetItemById } from '../graphql';
import {
MockedProvider,
renderWithRedux,
wait,
waitFor,
waitForElementToBeRemoved,
} from '../utils/test/react-testing-library';
import { Item } from '../containers/Item';
const getItemMockResult = {
_id: 'item',
};
const getItemMock = {
request: {
query: GetItemById,
variables: { _id: 'item' },
},
result: {
data: {
getItemById: getItemMockResult,
},
},
};
const setup = (mocks: MockedResponse[]) =>
renderWithRedux(
<MockedProvider mocks={mocks} addTypename={false}>
<ReactRouter.NativeRouter initialEntries={['/item']}>
<ReactRouter.Route path="/:itemId">
<Item />
</ReactRouter.Route>
</ReactRouter.NativeRouter>
</MockedProvider>,
);
describe('Item', () => {
test(`Spinner disappears once item is loaded`, async () => {
const component = setup([getItemMock]);
const spinner = await component.findByTestId('spinner');
await waitFor(() => expect(component.queryByTestId('spinner')).toBeFalsy());
});
});
It logs undefined undefined undefined true item. It's stuck at the loading state.
If I use wait(0) before the waitFor, it passes. However when I set a higher timeout for the waitFor, it doesn't work.
export const wait = (timeout = 0) =>
new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, timeout);
});
I've met with situations where inside waitFor it was like a different state of the component (based on the console logs the elements were completely different inside the component, and inside waitFor), but I have no idea what causes it.
This is being tested alone to make sure no inference with other tests.
Any ideas?
If I duplicate the test (without wait), and run them, one of them passes as expected, and one fails.

how can I pass arguments to the query method in apollo?

I am using apollo client to fetch the data.
And I want it to only get those todos that are made by the logged in user only.
But this piece doesn't work
Source code:
import { gql } from '#apollo/client';
export const todoService = {
getTodoItems: () => gql`
query todoQuery($loggedUserId: String!) {
todo(where: { userId: { _eq: $loggedUserId } }, order_by: { createdAt: desc }) {
id
todo {
title,
body,
status
}
userId
}
}
}
`
Redux thunk file
import { createAsyncThunk } from '#reduxjs/toolkit';
import { apolloClient } from '#/Apollo';
import { todoService } from './_graphql';
export const todoThunk = {
getTodoItems: createAsyncThunk(`db/getTodoItems`, async (loggedUserId: string) => {
const response = await apolloClient.query({
query: todoService.getTodoItems(),
variables: { loggedUserId },
fetchPolicy: `network-only`,
});
return response;
}),
React Component
useEffect(
dispatch(todoThunk.getTodoItems(loggedUserId));
,[dispatch])
However it works when I hard code the userId in place of variable loggedUserId like this:
export const todoService = {
getTodoItems: () => gql`
query todoQuery {
todo(where: { userId: { _eq: "some_hard_coded_id" } }, order_by: { createdAt: desc }) {
id
todo {
title,
body,
status
}
userId
}
}
}
`
It seems you missed a $ sign
try:
import { gql } from '#apollo/client';
export const todoService = {
getTodoItems: () => gql`
query todoQuery($loggedUserId: String!) {
todo(where: { userId: { _eq: $loggedUserId } }, order_by: { createdAt: desc }) {
id
todo {
title,
body,
status
}
userId
}
}
}
`
this worked for me.
import { gql } from '#apollo/client';
export const todoService = {
getTodoItems: () => gql`
query todoQuery($loggedUserId: uuid! = loggedUserId) {
todo(where: { userId: { _eq: $loggedUserId } }, order_by: { createdAt: desc }) {
id
todo {
title,
body,
status
}
userId
}
}
}
`

Get data from a query in ReactJs using GraphQl

I use Apollo Client and have the next code:
import {InMemoryCache, makeVar} from "#apollo/client";
export const cartItemsVar = makeVar(['test']);
export const demo3 = makeVar({name: 123});
export const cache = new InMemoryCache({
addTypename: true,
typePolicies: {
Person: {
fields: {
nameCar: {
read(name) {
return demo3();
}
}
}},
Query: {
fields: {
cartItems: {
read() {
return cartItemsVar();
}
},
demo: {
merge(name) {
return demo3()
}
}
}
}
}
});
How you can notice, this is my store in my react application. I want to get data from
Person: {
fields: {
nameCar: {
read(name) {
return demo3();
}
}
}
},
But doing something like this
const GET_CART_ITEMS = gql `
query GetCartItems {
nameCar #client
}
`;
//
const {
data,
loading,
error
} = useQuery(GET_CART_ITEMS);
... i get undefined in data., but if i do
export const GET_CART_ITEMS = gql `
query GetCartItems {
cartItems #client
}
`;
...i get the wanted result cartItems: ["test"].
Question: How to get data {name: 123} in the first situation and why the code does not work? What is the difference inside the typePolicies between Person and Query?

getStaticProps returns an empty object

I'm using nextJS V9.5.5 with wp-graphql and apolloClient to get data from WordPress. Everything works fine, but when I try to return context (in the purpose of getting query) from getStaticProps() like it's described in docs, it returns an empty object.
Custom App:
import React from "react";
import getConfig from "next/config";
import LayoutOuter from "../components/LayoutOuter";
import "bootstrap/dist/css/bootstrap.css";
import { ApolloProvider } from "#apollo/client";
import { useApollo } from "../lib/apolloClient";
import { initializeApollo } from "../lib/apolloClient";
import { gql } from "#apollo/client";
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const { DOMAIN } = publicRuntimeConfig;
function CustomApp({ pageProps, Component, props }) {
const apolloClient = useApollo(pageProps.initialApolloState);
return (
<ApolloProvider client={apolloClient}>
{console.log("_app", props)}
<LayoutOuter>
<Component {...pageProps} />
</LayoutOuter>
</ApolloProvider>
);
}
CustomApp.getInitialProps = async (ctx) => {
const apolloClient = initializeApollo();
await apolloClient.query({
query: gql`
{
// my graphql query here
}
`,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
ctx: JSON.stringify(ctx),
},
};
};
export default CustomApp;
One of the page:
import React, { Component, useEffect, useState } from "react";
import getConfig from "next/config";
import { NextSeo } from "next-seo";
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const { DOMAIN, SITENAME } = publicRuntimeConfig;
import { initializeApollo } from "../lib/apolloClient";
import { gql } from "#apollo/client";
import "./services.module.scss";
const Home = (props) => {
let currentPage = Object.values(props.initialApolloState.ROOT_QUERY)[1];
const {
title,
metadesc,
metaRobotsNoindex,
metaRobotsNofollow,
metaRobotsAdv,
opengraphTitle,
opengraphDescription,
opengraphImage,
twitterTitle,
twitterDescription,
twitterImage,
} = currentPage.seo;
return (
<>
{console.log("project", props)}
<NextSeo
noindex={metaRobotsNoindex}
nofollow={metaRobotsNofollow}
title={title != "" ? title : `${props.data.pagetitle} - ${SITENAME}`}
description={metadesc}
canonical={DOMAIN}
openGraph={{
url: DOMAIN,
title:
opengraphTitle != ""
? opengraphTitle
: `${props.data.pagetitle} - Garrison Collection`,
description: opengraphDescription,
images: [
{
url: opengraphImage,
width: 800,
height: 600,
alt: { SITENAME },
},
],
site_name: { SITENAME },
}}
/>
<p>works</p>
</>
);
};
export async function getStaticProps(context) {
const apolloClient = initializeApollo();
await apolloClient.query({
query: gql`
{
project(id: "ca-souls", idType: SLUG) {
seo {
canonical
metaDesc
metaKeywords
metaRobotsNofollow
metaRobotsNoindex
opengraphAuthor
opengraphDescription
opengraphModifiedTime
opengraphPublishedTime
opengraphPublisher
opengraphSiteName
opengraphTitle
opengraphType
opengraphUrl
title
twitterDescription
twitterTitle
}
}
}
`,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
context: JSON.stringify(context) || null,
},
revalidate: 1,
};
}
export default Home;
Here is the log result:
How could I get context.query?
The context parameter includes previewData contains the preview data set by setPreviewData. This means including function, therefore, unable to serealize. Take values out from context.params.

Reuse mutations without duplicating code in Apollo + React?

I have the mutation below in a React Component. Im going to need the same mutation in multiple components and on different pages.
How can I reuse my mutation code without repeating it?
This example isn't that complex but some queries use optimistic UI and write to the store.
import React from 'react';
import { graphql, compose } from 'react-apollo';
import { gql } from 'apollo-boost';
const JoinLocation = props => {
if (props.ME.loading) return null;
const { locationMachineName } = props;
const me = props.ME.me;
const submit = () => {
props
.JOIN_LOCATION({
variables: {
userId: me.id,
locationMachine: locationMachineName,
},
})
.catch(err => {
console.error(err);
});
};
return <button onClick={() => submit()}>Join location</button>;
};
const ME = gql`
query me {
me {
id
}
}
`;
const JOIN_LOCATION = gql`
mutation joinLocation($userId: ID!, $locationId: ID!) {
joinLocation(userId: $userId, locationId: $locationId) {
id
}
}
`;
export default compose(
graphql(JOIN_LOCATION, { name: 'JOIN_LOCATION' }),
graphql(ME, { name: 'ME' }),
)(JoinLocation);
Create a higher-order component (HOC) for the mutation/query that contains the gql options and optimistic UI logic:
const JOIN_LOCATION = gql`
mutation joinLocation($userId: ID!, $locationId: ID!) {
joinLocation(userId: $userId, locationId: $locationId) {
id
}
}
`;
export const withJoinLocation = component => graphql(JOIN_LOCATION, { name: 'JOIN_LOCATION' })(component);
Then wrap your different components with it.
export default withJoinLocation(JoinLocation);
UPDATE: Based on your below comment, if you want to encapsulate the whole submit logic and not just the mutation as stated in your question, you can use a render prop like so:
import React from 'react';
import { graphql, compose } from 'react-apollo';
import { gql } from 'apollo-boost';
const JoinLocation = props => {
if (props.ME.loading) return null;
const { locationMachineName } = props;
const me = props.ME.me;
const submit = () => {
props
.JOIN_LOCATION({
variables: {
userId: me.id,
locationMachine: locationMachineName,
},
})
.catch(err => {
console.error(err);
});
};
return props.render(submit);
};
const ME = gql`
query me {
me {
id
}
}
`;
const JOIN_LOCATION = gql`
mutation joinLocation($userId: ID!, $locationId: ID!) {
joinLocation(userId: $userId, locationId: $locationId) {
id
}
}
`;
export default compose(
graphql(JOIN_LOCATION, { name: 'JOIN_LOCATION' }),
graphql(ME, { name: 'ME' }),
)(JoinLocation);
Now any component can consume the reusable submit logic. Assume you name the above component JoinLocation.js:
import JoinLocation from './JoinLocation';
const Comp = () => {
return <JoinLocation render={submit => <button onClick={() => submit()}>Join location</button>}/>
}

Resources