Reputation: 31
Here are my two files. I am trying to mimic the results of this sandbox with my own data: https://codesandbox.io/embed/stoic-haze-ispw2?codemirror=1
Essentially I can see the data was fetched and cache updated, but my component ResourceSection list of data isn't updated.
[UPDATE] Made some major changes based on feedback. Queries were removed from components and I made a skipLimitPagination function. The query works but my cache is not updating or placing the data inside.
import React from "react";
import { BrowserRouter as Router } from "react-router-dom";
import "./App.css";
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
import Home from "./screens";
import { skipLimitPagination } from './utils/utilities'
const client = new ApolloClient({
uri: `https://graphql.contentful.com/content/v1/spaces/${process.env.REACT_APP_SPACE_ID}/?access_token=${process.env.REACT_APP_CDA_TOKEN}`,
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
resourceCollection: {items: skipLimitPagination()}
}
}
}
}),
});
function App() {
return (
<ApolloProvider client={client}>
<Router>
<Home />
</Router>
</ApolloProvider>
);
}
export default App;
import React, { useState } from "react";
import Navbar from "../components/Navbar";
import MobileNav from "../components/MobileNav";
import HeroSection from "../components/HeroSection";
import FeaturesSection from "../components/FeatureSection";
import Split from "../components/SplitWindow";
import Loading from "../components/Loading";
import { useQuery, gql } from "@apollo/client";
import Resource from "../components/ResourceSection";
import Contact from "../components/ContactSection";
import Footer from "../components/Footer";
const MASS_COLLECTION = gql`
query($skip: Int) {
resourceCollection(limit: 5, skip: $skip ) {
items {
type
category
title
link
bgColor
color
}
},
splitSectionCollection(order: splitId_ASC) {
items {
splitId
lightBg
left
lightText
darkText
image {
url
}
alt
heading
content {
json
}
}
}
}
`;
const Home = () => {
const [isOpen, setIsOpen] = useState(false);
const { loading, error, data, fetchMore } = useQuery(MASS_COLLECTION, {
variables: {
skip: 0,
},
});
if (loading) return <Loading />;
if (error) return <p>Error</p>;
const toggle = () => {
setIsOpen(!isOpen);
};
return (
<>
<MobileNav isOpen={isOpen} toggle={toggle} />
<Navbar toggle={toggle} />
<HeroSection />
<FeaturesSection />
{data.splitSectionCollection.items.map((item) => {
return <Split item={item} key={item.splitId} />;
})}
<Resource data={data.resourceCollection.items} fetchMore={fetchMore}/>
<Contact />
<Footer />
</>
);
};
export default Home;
import React, { useState, useCallback } from "react";
import {
ResourceContainer,
ResourcesWrapper,
ResourceRow,
TextWrapper,
Column1,
Heading,
Content,
Column2,
ImgWrap,
Img,
Form,
FormSelect,
FormOption,
// LinkContainer,
// LinkWrapper,
// LinkIcon,
// LinkTitle,
// LoadMore,
// ButtonWrapper,
} from "./ResourceElements";
const ResourceSection = ({ data, fetchMore }) => {
console.log(data)
const handleClick = useCallback(() => {
fetchMore({
variables: {
skip:
data
? data.length
: 0,
},
});
}, [fetchMore, data]);
return (
<ResourceContainer lightBg={true} id="resource">
<ResourcesWrapper>
<ResourceRow left={true}>
<Column1>
<TextWrapper>
<Heading lightText={false}>Resources</Heading>
<Content darkText={true} className="split_cms">
Cyber Streets strives in sharing education resources to all.
Below you can find an exhaustive list of resources covering
everything from computer programming to enterneurship. "Be
knowledgeable in your niche, provide some information free of
charge, and share other trustworthy people's free resources
whenever possible..." - Heather Hart
</Content>
</TextWrapper>
</Column1>
<Column2>
<ImgWrap>
<Img
src="/assets/images/Resource.svg"
alt="Two looking at computer screen svg"
/>
</ImgWrap>
</Column2>
</ResourceRow>
<Form action="">
<FormSelect
// onChange={(e) => {
// setCategory(e.target.value);
// // setLimit(5);
// }}
>
<FormOption value="">Filter by category</FormOption>
<FormOption value="MEDIA">Media</FormOption>
<FormOption value="TEDX">Ted Talks</FormOption>
<FormOption value="INTERNET SAFETY/AWARENESS">
Internet safety & awareness
</FormOption>
<FormOption value="K-12/COMPUTER SCIENCE">
k-12 & computer science
</FormOption>
<FormOption value="CODING">Programming</FormOption>
<FormOption value="CYBER/IT OPERATIONS">
Cyber ∧ IT operations
</FormOption>
<FormOption value="ROBOTICS">Robotics</FormOption>
<FormOption value="CLOUD">Cloud</FormOption>
<FormOption value="SCIENCE">Science</FormOption>
<FormOption value="PROFESSIONAL DEVELOPMENT">
Professional Development
</FormOption>
<FormOption value="3D PRINTING">3D Printing</FormOption>
<FormOption value="ART">Art</FormOption>
<FormOption value="MOOC">Massive Open Online Courses</FormOption>
<FormOption value="GAMES">Games & Challenges</FormOption>
<FormOption value="OTHER">Other</FormOption>
</FormSelect>
</Form>
<div className="list">
{data.map((resource, i) => (
<div key={resource.title} className="item">
{resource.title}
</div>
))}
</div>
<button className="button" onClick={handleClick}>
Fetch!
</button>
</ResourcesWrapper>
</ResourceContainer>
);
};
export default ResourceSection;
My cache after clicking the fetch more button. Two separate resource collections, should this be combined? I got this information through apollo chrome plugin.
I am using the contenful graphql API:
Here is my resource collection args and fields:
ResourceCollection
ARGS
skip: Int = 0
limit: Int = 100
preview: Boolean
locale: String
where: ResourceFilter
order: [ResourceOrder]
Fields
total: Int!
skip: Int!
limit: Int!
items: [Resource]!
export function skipLimitPagination(keyArgs) {
return {
keyArgs,
merge(existing, incoming, { args }) {
const merged = existing ? existing.slice(0) : [];
if (args) {
const { skip = 0 } = args;
for (let i = 0; i < incoming.length; ++i) {
merged[skip + i] = incoming[i];
}
} else {
merged.push.apply(merged, incoming);
}
return merged;
},
};
}
I've been working on this issue for three days straight. I tried the older way with update query but it wasn't working as intended so now I am trying to the most update apollo technique. Please help :(
Upvotes: 3
Views: 6255
Reputation: 636
I used @Lingertje's answer for a while but kept running into duplication issues, especially if my component re-rendered due to a user navigating away and back again without re-rendering the app.
Take a look at merging arrays of non-normalised objects in the Apollo Client documentation.
You need to set a field policy for the items field of your ResourceCollection type, like this (bottom of the example):
import { InMemoryCache } from '@apollo/client'
const mergeItemsById = (existing: any[], incoming: any[], { readField, mergeObjects }) => {
const merged: any[] = existing ? existing.slice(0) : [];
const itemIdToIndex: Record<string, number> = Object.create(null);
if (existing) {
existing.forEach((item, index) => {
itemIdToIndex[readField("id", item)] = index;
});
}
incoming.forEach(item => {
const id = readField("id", item);
const index = itemIdToIndex[id];
if (typeof index === "number") {
// Merge the new item data with the existing item data.
merged[index] = mergeObjects(merged[index], item);
} else {
// First time we've seen this item in this array.
itemIdToIndex[id] = merged.length;
merged.push(item);
}
});
return merged;
}
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
resourceCollection: {
keyArgs: ['limit', 'skip'],
},
},
},
ResourceCollection: {
fields: {
items: {
merge: mergeItemsById
}
}
}
},
})
If your unique key on items
isn't id
then change the three references of "id" to whatever your unique key is.
Then just import this cache definition where you're setting up your client (much less messy than doing it inline when it gets this big):
const client = new ApolloClient({
uri: `https://graphql.contentful.com/content/v1/spaces/${process.env.REACT_APP_SPACE_ID}/?access_token=${process.env.REACT_APP_CDA_TOKEN}`,
cache: ### import the cache object here ###
});
Bonus: see I've set the keyArgs on the resourceCollection field of type Query, that ensures you don't reuse the cached results when you change those parameters.
Upvotes: 3
Reputation: 236
I had almost the same problem and found a solution. The problem is that all the examples on the Apollo site assume that the first element of the response object is your array of items.
This is not how it works with Contentful, the array is always nested in items
within the collection. For example your resourceCollection
has a property items which contains all your resources. So you have to merge the items
but return the whole resourceCollection
, which will look like this:
new InMemoryCache({
typePolicies: {
Query: {
fields: {
resourceCollection: {
keyArgs: false,
merge(existing, incoming) {
if (!incoming) return existing
if (!existing) return incoming // existing will be empty the first time
const { items, ...rest } = incoming;
let result = rest;
result.items = [...existing.items, ...items]; // Merge existing items with the items from incoming
return result
}
}
}
}
}
})
This will return the resourceCollection
with merged items
.
Upvotes: 10
Reputation: 585
The type of the root query is just Query
, not RootQuery
, which I think is why your configuration for the RootQuery.resourceCollection
field is not having any effect.
In other words, try this:
new InMemoryCache({
typePolicies: {
Query: { // not RootQuery
fields: {
resourceCollection: offsetLimitPagination(),
},
},
},
})
I would also recommend creating your RESOURCE_COLLECTION
query outside of your component, so that you aren't creating a new DocumentNode
each time you render your component.
Upvotes: 0