apollo-client: How to get inverse relation from cache? - graphql

I have a graphql query response of the shape
{
table {
id
legs {
id
}
}
This normalizes to table and leg entries in my InMemoryCache.
But then if my application retrieves a leg from cache and needs to know the corresponding table, how would I find this?
The two ideas I had are
adding a table prop to each leg when the query response comes in - not sure if/how that would work (I have multiple queries and mutations containing the graphql fragment with above shape)
having a suitable cache redirect, but I don't know how to do this without searching all tables for the leg.
Does apollo provide any features suitable to achieve this inverse lookup?
Update: to clarify, the leg has a table prop, but I since I already have the info in the client before resolving that prop, I'd like to resolve that prop client-side instead of server-side.

You should be adding a table prop to each leg. According to the graphql.org documentation you should be thinking in graphs:
With GraphQL, you model your business domain as a graph by defining a schema; within your schema, you define different types of nodes and how they connect/relate to one another.
In your model, tables and legs are nodes in your business model graph. When you add a table prop to each leg you are creating a new edge in this graph that your client-side code can traverse to get the relevant data.
Edit after clarification:
You can use writeFragment and to gain fine grained control of the Apollo cache. Once the cache filling query is done, compute the inverse relationship and write it to the cache like so:
fetchTables = async () => {
const client = this.props.client
const result = await client.query({
query: ALL_TABLES_QUERY,
variables: {}
})
// compute the reverse link
const tablesByLeg = {}
for (const table of result.data.table) {
for (const leg of table.legs) {
if (!tablesByLeg[leg.id]) {
tablesByLeg[leg.id] = {
leg: leg,
tables: []
}
}
tablesByLeg[leg.id].tables.push(table)
}
}
// write to the Apollo cache
for (const { leg, tables } of Object.values(tablesByLeg)) {
client.writeFragment({
id: dataIdFromObject(leg),
fragment: gql`
fragment reverseLink from Leg {
id
tables {
id
}
}
`,
data: {
...leg,
tables
}
})
}
// update component state
this.setState(state => ({
...state,
tables: Object.values(result)
}))
}
Demo
I put up a complete exemple here: https://codesandbox.io/s/6vx0m346z
I also put it below just for completeness sake.
index.js
import React from "react";
import ReactDOM from "react-dom";
import { ApolloProvider } from "react-apollo";
import { createClient } from "./client";
import { Films } from "./Films";
const client = createClient();
function App() {
return (
<ApolloProvider client={client}>
<Films />
</ApolloProvider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
client.js
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
export function dataIdFromObject(object) {
return object.id ? object.__typename + ":" + object.id : null;
}
export function createClient() {
return new ApolloClient({
connectToDevTools: true,
ssrMode: false,
link: new HttpLink({
uri: "https://prevostc-swapi-graphql.herokuapp.com"
}),
cache: new InMemoryCache({
dataIdFromObject,
cacheRedirects: {
Query: {
planet: (_, args, { getCacheKey }) =>
getCacheKey({ __typename: "Planet", id: args.id })
}
}
})
});
}
Films.js
import React from "react";
import gql from "graphql-tag";
import { withApollo } from "react-apollo";
import { dataIdFromObject } from "../src/client";
import { Planet } from "./Planet";
const ALL_FILMS_QUERY = gql`
query {
allFilms {
films {
id
title
planetConnection {
planets {
id
name
}
}
}
}
}
`;
const REVERSE_LINK_FRAGMENT = gql`
fragment reverseLink on Planet {
id
name
filmConnection {
films {
id
title
}
}
}
`;
class FilmsComponent extends React.Component {
constructor() {
super();
this.state = { films: [], selectedPlanetId: null };
}
componentDidMount() {
this.fetchFilms();
}
fetchFilms = async () => {
const result = await this.props.client.query({
query: ALL_FILMS_QUERY,
variables: {}
});
// compute the reverse link
const filmByPlanet = {};
for (const film of result.data.allFilms.films) {
for (const planet of film.planetConnection.planets) {
if (!filmByPlanet[planet.id]) {
filmByPlanet[planet.id] = {
planet: planet,
films: []
};
}
filmByPlanet[planet.id].films.push(film);
}
}
// write to the apollo cache
for (const { planet, films } of Object.values(filmByPlanet)) {
this.props.client.writeFragment({
id: dataIdFromObject(planet),
fragment: REVERSE_LINK_FRAGMENT,
data: {
...planet,
filmConnection: {
films,
__typename: "PlanetsFilmsConnection"
}
}
});
}
// update component state at last
this.setState(state => ({
...state,
films: Object.values(result.data.allFilms.films)
}));
};
render() {
return (
<div>
{this.state.selectedPlanetId && (
<div>
<h1>Planet query result</h1>
<Planet id={this.state.selectedPlanetId} />
</div>
)}
<h1>All films</h1>
{this.state.films.map(f => {
return (
<ul key={f.id}>
<li>id: {f.id}</li>
<li>
title: <strong>{f.title}</strong>
</li>
<li>__typename: {f.__typename}</li>
<li>
planets:
{f.planetConnection.planets.map(p => {
return (
<ul key={p.id}>
<li>id: {p.id}</li>
<li>
name: <strong>{p.name}</strong>
</li>
<li>__typename: {p.__typename}</li>
<li>
<button
onClick={() =>
this.setState(state => ({
...state,
selectedPlanetId: p.id
}))
}
>
select
</button>
</li>
<li> </li>
</ul>
);
})}
</li>
</ul>
);
})}
<h1>The current cache is:</h1>
<pre>{JSON.stringify(this.props.client.extract(), null, 2)}</pre>
</div>
);
}
}
export const Films = withApollo(FilmsComponent);
Planet.js
import React from "react";
import gql from "graphql-tag";
import { Query } from "react-apollo";
const PLANET_QUERY = gql`
query ($id: ID!) {
planet(id: $id) {
id
name
filmConnection {
films {
id
title
}
}
}
}
`;
export function Planet({ id }) {
return (
<Query query={PLANET_QUERY} variables={{ id }}>
{({ loading, error, data }) => {
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
const p = data.planet;
return (
<ul key={p.id}>
<li>id: {p.id}</li>
<li>
name: <strong>{p.name}</strong>
</li>
<li>__typename: {p.__typename}</li>
{p.filmConnection.films.map(f => {
return (
<ul key={f.id}>
<li>id: {f.id}</li>
<li>
title: <strong>{f.title}</strong>
</li>
<li>__typename: {f.__typename}</li>
<li> </li>
</ul>
);
})}
</ul>
);
}}
</Query>
);
}

Related

Why are the components not rerendering after Response?

Table with pinia store equipmentlist, fethced from usequipments but table does not rerender
VueTableComponent
<script setup lang="ts">
import { useEquipmentStore } from '~/store/equipmentStore'
import useEquipments from '~/composables/equipments'
const { getEquipmentList } = useEquipments()
onBeforeMount(() => { getEquipmentList() })
const state = useEquipmentStore()
const equipmentList: any = state.equipmentList
const loaded = state.loaded
</script>
<template>
<el-table :key="loaded" :data="equipmentList" style="width: 100%">
<el-table-column type="expand">
<template #default="props">
ID: {{ props.row.id }}
</template>
</el-table-column>
<el-table-column label="ID" prop="id" />
<el-table-column label="Name" prop="name" />
</el-table>
</template>
Typescript File for all CRUD Operations equipment.ts
import { useRouter } from 'vue-router'
import http from '../http-common'
import { useEquipmentStore } from '~/store/equipmentStore'
export default function useEquipments() {
const state = useEquipmentStore()
const errors = ref([]) // array of strings
const router = useRouter()
const getEquipmentList = async() => {
try {
console.log(state.equipmentList.length)
const response = await http.get('/equipment/list')
state.equipmentList = response.data
console.log(state.equipmentList.length)
console.log(state.equipmentList[0])
}
catch (error: any) {
console.log(error.message)
}
}
Equipment(Pinia)Store
import { defineStore } from 'pinia'
import type { Ref } from 'vue'
export const useEquipmentStore = defineStore('equipment', {
state: () => ({
equipment: ref({}) as any,
equipmentList: ref([]) as Ref<any[]>,
}),
actions: {
reset() {
this.equipment = {}
this.equipmentList = []
},
},
})
1. i called several times getEquipment list and it is faster done then i stored an initial equipment, 2. i clicked on the link on the left and fetched several times more and as u can see there is something fetchd but not displayed, 3. after repeating to home and again to Link the component is there and alll other next fetches do indeed function well
Main.ts
app.component('InputText', InputText)
app.mount('#app')
useEquipments().initDB()
}
same fetching class equipment.ts
const initDB = () => {
try {
if (state.equipmentList.length === 0) { storeEquipment({ id: 1, name: 'Firma' }) }
else {
for (const equipment of state.equipmentList) {
console.log(equipment)
if (equipment === 'Firma')
state.equipmentList.splice(state.equipmentList.indexOf(equipment), 1)
}
}
}
catch (error: any) {
console.log(error.message)
}
}

why does my graphql return an empty array?

I am newbie with graphql. I have a front-end project (nextjs) and back-end(strapi).
This is my code
import { ApolloClient, InMemoryCache, gql } from '#apollo/client'
export default function Blog({ posts }) {
console.log('posts', posts)
return (
<div>
{posts.map(post => {
return (
<div>
<p>{posts.heading}</p>
</div>
)
})}
</div>
)
}
export async function getStaticProps() {
const client = new ApolloClient({
url: 'http://localhost:1337/graphql/',
cache: new InMemoryCache(),
})
const { data } = await client.query({
query: gql`
query {
posts {
data {
attributes {
heading
}
}
}
}
`,
})
return {
props: {
posts: data.posts,
},
}
}
alongside this, I also get this message "cannot destructure property of intermediate value". Does anybody know why, i'm sure the code is correct.
Firstly i recommend creating a folder in root and put all graphql codes and you have also a syntax error:
let me give you an example:
/index.js
import { ApolloClient, InMemoryCache, gql } from "#apollo/client";
const client = new ApolloClient({
uri: API,
cache: new InMemoryCache(),
defaultOptions: {
query: {
fetchPolicy: "network-only",
},
},
});
//get category ids
const getCatIds = async () => {
try {
const { data } = await client.query({
query: gql`
query categoriesId {
categories(sort: "id:ASC") {
id
name
}
}
`,
});
return data.categories;
} catch {
console.log("error appolo");
}
};
export {getCatIds}

How to integrate Multiple clients using graphql hooks in react hooks

I wanna integrate multiple clients in my react-hooks application. I'm using graphql-hooks via Apollo client we have a module to integrate multiple clients
Following is the link Apollo graphql multiple client
`https://www.npmjs.com/package/#titelmedia/react-apollo-multiple-clients`
Following I'm using for graphql hooks
https://www.npmjs.com/package/graphql-hooks
How do we achieve the same for graphql-hooks?
My requirement is depending on the selection of the radio button I need to switch between these multiple clients all that in one component using more than one client.
In my app I'm using graphql-hook we wrap the component to the client here the same component has functionality wherein depending on the select one of the radio buttons the client must be switch.I'm having one client but need to integrate multiple clients I googled but i've not found so have questioned here.
Is this possible?
Can anyone please help out there
We can use new GraphQLClient() multiple times to get multiple graphql clients
Here is a good approach and you can follow it: https://www.loudnoises.us/next-js-two-apollo-clients-two-graphql-data-sources-the-easy-way/
I have two middleware graphql servers running. One for the app one for analytics. I do it in two different ways according to my need as below...
CubeClient.jsx
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { SchemaLink } from "apollo-link-schema";
import { makeExecutableSchema } from "graphql-tools";
const cache = new InMemoryCache();
const defaultDashboardItems = [{"vizState":"{\"query\":{\"measures\":[\"Product.count\"],\"timeDimensions\":[{\"dimension\":\"Product.createdAt\"}],\"dimensions\":[\"Producttype.name\"],\"filters\":[],\"order\":{}},\"chartType\":\"bar\",\"orderMembers\":[{\"id\":\"Product.count\",\"title\":\"Product Count\",\"order\":\"none\"},{\"id\":\"Producttype.name\",\"title\":\"Producttype Name\",\"order\":\"none\"},{\"id\":\"Product.createdAt\",\"title\":\"Product Created at\",\"order\":\"none\"}],\"pivotConfig\":{\"x\":[\"Producttype.name\"],\"y\":[\"measures\"],\"fillMissingDates\":true,\"joinDateRange\":false},\"shouldApplyHeuristicOrder\":true,\"sessionGranularity\":null}","name":"Product Types","id":"2","layout":"{\"x\":0,\"y\":0,\"w\":24,\"h\":8}"},{"vizState":"{\"query\":{\"measures\":[\"Sale.total\"],\"timeDimensions\":[{\"dimension\":\"Sale.createdAt\",\"granularity\":\"day\"}],\"dimensions\":[\"Customer.firstname\"],\"order\":{\"Sale.total\":\"desc\"}},\"chartType\":\"area\",\"orderMembers\":[{\"id\":\"Sale.total\",\"title\":\"Sale Total\",\"order\":\"desc\"},{\"id\":\"Customer.firstname\",\"title\":\"Customer Firstname\",\"order\":\"none\"},{\"id\":\"Sale.createdAt\",\"title\":\"Sale Created at\",\"order\":\"none\"}],\"pivotConfig\":{\"x\":[\"Sale.createdAt.day\"],\"y\":[\"Customer.firstname\",\"measures\"],\"fillMissingDates\":true,\"joinDateRange\":false},\"shouldApplyHeuristicOrder\":true}","name":"Sale Total","id":"3","layout":"{\"x\":0,\"y\":8,\"w\":24,\"h\":8}"}]
export function getCubeClient(location){
const getDashboardItems = () => {
// return JSON.parse(window.localStorage.getItem("dashboardItems")) || defaultDashboardItems;
return defaultDashboardItems
}
const setDashboardItems = items => {
return window.localStorage.setItem("dashboardItems", JSON.stringify(items));
}
const nextId = () => {
const currentId = parseInt(window.localStorage.getItem("dashboardIdCounter"), 10) || 1;
window.localStorage.setItem("dashboardIdCounter", currentId + 1);
return currentId.toString();
};
const toApolloItem = i => ({ ...i, __typename: "DashboardItem" });
const typeDefs = `
type DashboardItem {
id: String!
layout: String
vizState: String
name: String
}
input DashboardItemInput {
layout: String
vizState: String
name: String
}
type Query {
dashboardItems: [DashboardItem]
dashboardItem(id: String!): DashboardItem
}
type Mutation {
createDashboardItem(input: DashboardItemInput): DashboardItem
updateDashboardItem(id: String!, input: DashboardItemInput): DashboardItem
deleteDashboardItem(id: String!): DashboardItem
}
`;
const schema = makeExecutableSchema({
typeDefs,
resolvers: {
Query: {
dashboardItems() {
const dashboardItems = getDashboardItems();
return dashboardItems.map(toApolloItem);
},
dashboardItem(_, { id }) {
const dashboardItems = getDashboardItems();
return toApolloItem(dashboardItems.find(i => i.id.toString() === id));
}
},
Mutation: {
createDashboardItem: (_, { input: { ...item } }) => {
const dashboardItems = getDashboardItems();
item = { ...item, id: nextId(), layout: JSON.stringify({}) };
dashboardItems.push(item);
setDashboardItems(dashboardItems);
return toApolloItem(item);
},
updateDashboardItem: (_, { id, input: { ...item } }) => {
const dashboardItems = getDashboardItems();
item = Object.keys(item)
.filter(k => !!item[k])
.map(k => ({
[k]: item[k]
}))
.reduce((a, b) => ({ ...a, ...b }), {});
const index = dashboardItems.findIndex(i => i.id.toString() === id);
dashboardItems[index] = { ...dashboardItems[index], ...item };
setDashboardItems(dashboardItems);
return toApolloItem(dashboardItems[index]);
},
deleteDashboardItem: (_, { id }) => {
const dashboardItems = getDashboardItems();
const index = dashboardItems.findIndex(i => i.id.toString() === id);
const [removedItem] = dashboardItems.splice(index, 1);
setDashboardItems(dashboardItems);
return toApolloItem(removedItem);
}
}
}
});
return new ApolloClient({
cache,
uri: "http://localhost:4000",
link: new SchemaLink({
schema
})
});
}
Dashboard.jsx
import { getCubeClient } from './CubeClient';
import { Query } from "react-apollo";
let cubeClient = getCubeClient()
const Dashboard = () => {
const dashboardItem = item => (
<div key={item.id} data-grid={defaultLayout(item)}>
<DashboardItem key={item.id} itemId={item.id} title={item.name}>
<ChartRenderer vizState={item.vizState} />
</DashboardItem>
</div>
);
return(
<Query query={GET_DASHBOARD_ITEMS} client={cubeClient}>
{({ error, loading, data }) => {
if (error) return <div>"Error!"</div>;
if (loading) return (
<div className="alignCenter">
<Spinner color="#be97e8" size={48} sizeUnit="px" />
</div>
);
if (data) {
return (<DashboardView t={translate} dashboardItems={data && data.dashboardItems}>
{data && data.dashboardItems.map(deserializeItem).map(dashboardItem)}
</DashboardView>)
} else {
return <div></div>
}
}}
</Query>
)
};
Explore.jsx
import { useQuery } from "#apollo/react-hooks";
import { withRouter } from "react-router-dom";
import ExploreQueryBuilder from "../components/QueryBuilder/ExploreQueryBuilder";
import { GET_DASHBOARD_ITEM } from "../graphql/queries";
import TitleModal from "../components/TitleModal.js";
import { isQueryPresent } from "#cubejs-client/react";
import PageHeader from "../components/PageHeader.js";
import ExploreTitle from "../components/ExploreTitle.js";
import { PageLayout } from '#gqlapp/look-client-react';
import Helmet from 'react-helmet';
import { translate } from '#gqlapp/i18n-client-react';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faAngleRight, faTrashAlt, faPlusCircle } from '#fortawesome/free-solid-svg-icons';
import settings from '#gqlapp/config';
import PropTypes from 'prop-types';
import { getCubeClient } from './CubeClient';
let cubeClient = getCubeClient()
const Explore = withRouter(({ history, location,t }) => {
const [addingToDashboard, setAddingToDashboard] = useState(false);
const params = new URLSearchParams(location.search);
const itemId = params.get("itemId");
const { loading, error, data } = useQuery(GET_DASHBOARD_ITEM, {
client: cubeClient,
variables: {
id: itemId
},
skip: !itemId
});
Titlemodal.jsx
import React from "react";
import { Modal, Input } from "antd";
import { useMutation } from "#apollo/react-hooks";
import { GET_DASHBOARD_ITEMS } from "../graphql/queries";
import {
CREATE_DASHBOARD_ITEM,
UPDATE_DASHBOARD_ITEM
} from "../graphql/mutations";
import { getCubeClient } from '../containers/CubeClient';
let cubeClient = getCubeClient()
const TitleModal = ({
history,
itemId,
titleModalVisible,
setTitleModalVisible,
setAddingToDashboard,
finalVizState,
setTitle,
finalTitle
}) => {
const [addDashboardItem] = useMutation(CREATE_DASHBOARD_ITEM, {
client: cubeClient,
refetchQueries: [
{
query: GET_DASHBOARD_ITEMS
}
]
});
const [updateDashboardItem] = useMutation(UPDATE_DASHBOARD_ITEM, {
client: cubeClient,
refetchQueries: [
{
query: GET_DASHBOARD_ITEMS
}
]
});
queries and mutations.js
import gql from "graphql-tag";
export const GET_DASHBOARD_ITEMS = gql`
query GetDashboardItems {
dashboardItems {
id
layout
vizState
name
}
}
`;
export const GET_DASHBOARD_ITEM = gql`
query GetDashboardItem($id: String!) {
dashboardItem(id: $id) {
id
layout
vizState
name
}
}
`;
export const CREATE_DASHBOARD_ITEM = gql`
mutation CreateDashboardItem($input: DashboardItemInput) {
createDashboardItem(input: $input) {
id
layout
vizState
name
}
}
`;
export const UPDATE_DASHBOARD_ITEM = gql`
mutation UpdateDashboardItem($id: String!, $input: DashboardItemInput) {
updateDashboardItem(id: $id, input: $input) {
id
layout
vizState
name
}
}
`;
export const DELETE_DASHBOARD_ITEM = gql`
mutation DeleteDashboardItem($id: String!) {
deleteDashboardItem(id: $id) {
id
layout
vizState
name
}
}
`;

How to use QueryComponent with pollInterval and local state changes

i'm writing a small search app which starts a search over a RESTapi. Until the search is finished the api respond with a 260 status code or a 250 if the search is finished.
For this purpose i've written a component with a Query component which polls the RESTapi:
import React from 'react';
import gql from "graphql-tag";
import { Query } from "react-apollo";
export const GET_SEARCH_RESULTS = gql`
query GetSearchResults($sid: String!) {
getSearchResults(sid: $sid) {
status,
clusteredOffers {
id,
bookingId,
totalPrice
}
}
}
`;
export default class extends React.PureComponent {
client = null;
updateStatus = (data) => {
this.client.writeData({ data: { currentSearch: { status:
data.getSearchResults.status, __typename: 'CurrentSearch' }} })
};
render() {
const { sid } = this.props;
return (
<Query
query={GET_SEARCH_RESULTS}
variables={{ sid }}
pollInterval={500}
onCompleted={this.updateStatus}
>
{({ loading, error, data, stopPolling, client }) => {
this.client = client;
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
if(data.getSearchResults.status &&
data.getSearchResults.status !== "260") stopPolling();
return (
<div>
{data.getSearchResults.clusteredOffers.map(offer => (
<div key={offer.id} >
<div>{offer.id}</div>
<div>{offer.bookingId}</div>
<div>{offer.totalPrice}</div>
</div>
))}
</div>
)
}}
</Query>
)
}
}
In a parent component i also need the status of the current search. So i've added a clientState to my ApolloClient to write the remote data to the client cache:
import ApolloClient from "apollo-boost";
import Search from './components/Search';
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
clientState: {
defaults: {
currentSearch: {
__typename: 'CurrentSearch',
status: "0"
}
},
}
});
....
const GET_CURRENT_SEARCH_STATE = gql`
{
currentSearch #client {
status
}
}
`;
As u can see i'm using onCompleted inside my polling query component to write the current status to the local cache.
Does it make sense or should i read the status from the cached remote data? If so, how i get the cached remote data?
thank you!

relay refetch doesn't show the result

I'm trying to create a live search-result component(lazy load one). It works perfectly for the first time but refetch doesn't update the data. I see the request and respoonse in Network tab! so it does get the data, but it doesn't supply it to the component!
any idea why?
import React, { Component } from 'react';
import {
createRefetchContainer,
graphql,
} from 'react-relay';
import ProfileShow from './ProfileShow';
class ProfileList extends Component {
render() {
console.log("rendering....", this.props)
return (
<div className="row">
<input type="text" onClick={this._loadMe.bind(this)} />
{this.props.persons.map((person) => {
return (
<div className="col-md-3">
<ProfileShow person={person} />
</div>
);
})}
</div>
);
}
_loadMe(e) {
const refetchVariables = fragmentVariables => ({
queryStr: e.target.value,
});
this.props.relay.refetch(refetchVariables, null, (...data) => {
console.log(data)
});
}
}
const FragmentContainer = createRefetchContainer(
ProfileList,
{
persons: graphql.experimental`
fragment ProfileList_persons on Person #relay(plural: true) {
fullname
number
email
pic
}
`
},
graphql.experimental`
query ProfileListRefetchQuery($queryStr: String!) {
talentList(query: $queryStr) {
...ProfileList_persons
}
}
`,
);
export default FragmentContainer;

Resources