Markell
Markell

Reputation: 31

Data not merging, Apollo 3 pagination with field policies

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 &amp; awareness
            </FormOption>
            <FormOption value="K-12/COMPUTER SCIENCE">
              k-12 &amp; computer science
            </FormOption>
            <FormOption value="CODING">Programming</FormOption>
            <FormOption value="CYBER/IT OPERATIONS">
              Cyber &and; 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 &amp; 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

Answers (3)

Darvanen
Darvanen

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

Lingertje
Lingertje

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

Ben
Ben

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

Related Questions