Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

usePagination doesn't update state #96

Closed
nibblesnbits opened this issue Jun 26, 2020 · 15 comments
Closed

usePagination doesn't update state #96

nibblesnbits opened this issue Jun 26, 2020 · 15 comments

Comments

@nibblesnbits
Copy link

nibblesnbits commented Jun 26, 2020

My Components:

/***** UnviewedActivities.js *****/
import PropTypes from "prop-types";
import React, { useState, useEffect } from "react";
import { graphql } from "babel-plugin-relay/macro";
import ActivityCard from "./ActivityCard";
import { Grid, Button } from "@material-ui/core";
import { usePagination } from "relay-hooks";

const propTypes = {
  user: PropTypes.object.isRequired,
};

const fragmentSpec = graphql`
  fragment UnviewedActivities_user on AppUser
    @argumentDefinitions(
      cursor: { type: "ID" }
      count: { type: "Int!", defaultValue: 5 }
    ) {
    appUserId
    list: unviewedActivities(first: $count, after: $cursor)
      @connection(key: "UnviewedActivities_list") {
      totalCount
      edges {
        node {
          activityId
          name
          shortDescription
          activityUrl
          imageUrl
          isApproved
        }
        cursor
      }
      pageInfo {
        endCursor
        hasNextPage
        startCursor
        hasPreviousPage
      }
    }
  }
`;

const connectionConfig = {
  direction: "forward",
  query: graphql`
    query UnviewedActivitiesRefetchQuery(
      $appUserId: Int!
      $count: Int!
      $cursor: ID
    ) {
      user(id: $appUserId) {
        ...UnviewedActivities_user @arguments(cursor: $cursor, count: $count)
      }
    }
  `,
  getConnectionFromProps(props) {
    return props.list;
  },
  getFragmentVariables(previousVariables, totalCount) {
    return {
      ...previousVariables,
      count: totalCount,
    };
  },
  getVariables({ appUserId }, { count, cursor }) {
    return {
      appUserId,
      count,
      cursor,
    };
  },
};

const UnviewedActivities = (props) => {
  const [user, { isLoading, hasMore, loadMore }] = usePagination(
    fragmentSpec,
    props.user
  );
  const [loads, setLoads] = useState(0);
  const [selected, setSelected] = useState([]);

  const {
    list: { edges },
  } = user;
  debugger;

  useEffect(() => {
    const key = "app:selectedActivities";
    const item = localStorage.getItem(key);
    if (item) {
      setSelected(JSON.parse(item));
    } else {
      localStorage.setItem(key, JSON.stringify([]));
    }
  }, []);

  const loadNextPage = () => {
    if (!hasMore() || isLoading()) {
      return;
    }
    loadMore(
      connectionConfig,
      5,
      () => {
        setLoads((v) => v + 1);
        console.log("loaded more");
      },
      {
        test: "test",
      }
    );
  };
  const addActivity = (id, pass) => {
    const key = "app:selectedActivities";
    const stored = localStorage.getItem(key) || [];
    const parsed = stored instanceof Array ? stored : JSON.parse(stored);
    const selected = [...parsed, { id, pass }];
    localStorage.setItem(key, JSON.stringify(selected));
    setSelected(selected);

    const remaining = edges.filter(
      ({ node: a }) => !selected.some((s) => s.id === a.activityId)
    );

    if (remaining.length === 1) {
      loadNextPage();
    }
  };

  const remaining = edges.filter(
    ({ node: a }) => !selected.some((s) => s.id === a.activityId)
  );
  return (
    <>
      <Button onClick={() => loadNextPage()}>Load More</Button>
      <Grid
        container
        direction="row"
        justify="center"
        alignItems="center"
        spacing={2}
      >
        {remaining.map(({ node: activity }) => (
          <Grid item key={activity.activityId}>
            <ActivityCard {...activity} addActivity={addActivity} />
          </Grid>
        ))}
      </Grid>
    </>
  );
};

UnviewedActivities.propTypes = propTypes;
export default UnviewedActivities;

/***** Activities.js *****/
import PropTypes from "prop-types";
import React from "react";
import { ReactRelayContext, createFragmentContainer } from "react-relay";
import { graphql } from "babel-plugin-relay/macro";
import UnviewedActivities from "./UnviewedActivities";
import { Container, Typography } from "@material-ui/core";

const propTypes = {
  authInfo: PropTypes.object.isRequired,
  relay: PropTypes.object.isRequired,
};

const contextType = ReactRelayContext;

class Activities extends React.Component {
  render() {
    const { authInfo } = this.props;
    if (!authInfo) {
      return <div>Loading...</div>;
    }

    return (
      <Container>
        <Typography variant="h6">
          Welcome! Select some activities to get started.
        </Typography>
        <UnviewedActivities user={authInfo.viewer} />
      </Container>
    );
  }
}

Activities.propTypes = propTypes;
Activities.contextType = contextType;

export default createFragmentContainer(Activities, {
  authInfo: graphql`
    fragment Activities_authInfo on AuthInfo {
      viewer {
        ...UnviewedActivities_user
      }
    }
  `,
});

My Problem:

Upon a successful call to loadMore(), the fetch is made and I see the correct new set of data in my dev tools Network tab, but the user value returned from usePagination() is not updated. Even if I force a rerender with setLoads((v) => v + 1);, the user value has not been updated.

My Setup:

{
  "dependencies": {
    "farce": "^0.4.1",
    "found": "^0.5.3",
    "found-relay": "^0.8.0",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-relay": "^9.1.0",
    "react-scripts": "3.4.1",
    "relay-hooks": "^3.5.0",
  },
  "devDependencies": {
    "babel-plugin-macros": "^2.8.0",
    "babel-plugin-relay": "^9.1.0",
    "graphql": "^15.0.0",
    "relay-compiler": "^9.1.0",
    "relay-config": "^9.1.0"
  },
}
@morrys
Copy link
Member

morrys commented Jun 26, 2020

Hi @nibblesnbits
this weekend i can't stay at the pc so i can only answer you by phone: /

can you give me more info by doing some debugging in the store and here?
https://github.com/relay-tools/relay-hooks/blob/master/src/FragmentResolver.ts#L206

@nibblesnbits
Copy link
Author

When I call loadMore() I got this in my Relay dev tools.

image

But I also had a break point on that line and it was never hit. Line 205 was executed and I grabbed this from renderedSnapshot:

{
  "data": {
    "appUserId": 0,
    "list": {
      "totalCount": 38,
      "edges": [
        {
          "node": {
            "activityId": 6,
            "name": "Virtual Drinks",
            "shortDescription": "Whether it's coffee break or happy hour, grab the beverage of your choice for a chemically-enhanced video chat.",
            "activityUrl": "https://www.nytimes.com/2020/03/20/well/virus-virtual-happy-hour.html",
            "imageUrl": "https://uploads-ssl.webflow.com/5e576c2ceb561c252e1d2e2d/5ec6e38e3e71f1a96e78f940_min-virtual%20coffee.jpg",
            "isApproved": true,
            "__typename": "Activity"
          },
          "cursor": "Ng=="
        },
        {
          "node": {
            "activityId": 7,
            "name": "Cook Mexican Street Tacos with a Pro Chef",
            "shortDescription": "Immerse yourself in Mexico's extraordinary street food culture in a guided cooking class with a professional chef.",
            "activityUrl": "https://www.airbnb.com/experiences/1661135?source=p2",
            "imageUrl": "https://a0.muscache.com/im/pictures/ec6a9398-1ed3-400a-8f97-ee3e7eeed6dd.jpg?aki_policy=exp_md",
            "isApproved": true,
            "__typename": "Activity"
          },
          "cursor": "Nw=="
        },
        {
          "node": {
            "activityId": 8,
            "name": "Meet My Bees",
            "shortDescription": "Open a beehive with a fourth-generation beekeeper to see these amazing creatures building honeycombs, making honey, and working together.",
            "activityUrl": "https://www.airbnb.com/experiences/1675237?source=p2",
            "imageUrl": "https://a0.muscache.com/im/pictures/80add1a5-f051-4aea-a7ee-f5b4491d3346.jpg?aki_policy=exp_md",
            "isApproved": true,
            "__typename": "Activity"
          },
          "cursor": "OA=="
        },
        {
          "node": {
            "activityId": 9,
            "name": "Mobile Photo Secrets with a Nat Geo Winner",
            "shortDescription": "Learn the secrets of taking amazing photos with your phone with examples from beautiful Barcelona.",
            "activityUrl": "https://www.airbnb.com/experiences/1718920?source=p2",
            "imageUrl": "https://a0.muscache.com/im/pictures/lombard/MtTemplate-1718920-media_library/original/b211f9ca-0154-4b7f-9fb3-76757298119e.jpeg?aki_policy=exp_md",
            "isApproved": true,
            "__typename": "Activity"
          },
          "cursor": "OQ=="
        },
        {
          "node": {
            "activityId": 10,
            "name": "Draw from Within with a New York Artist",
            "shortDescription": "Re-connect, re-imagine & relax through the restorative power of the creative process.",
            "activityUrl": "https://www.airbnb.com/experiences/1655361?source=p2",
            "imageUrl": "https://a0.muscache.com/im/pictures/lombard/MtTemplate-1655361-media_library/original/5d1c7ca9-8397-4884-91c1-c5eb0fea7dff.jpg?aki_policy=exp_md",
            "isApproved": true,
            "__typename": "Activity"
          },
          "cursor": "MTA="
        }
      ],
      "pageInfo": {
        "endCursor": "MTA=",
        "hasNextPage": true,
        "startCursor": "Ng==",
        "hasPreviousPage": false
      }
    }
  },
  "isMissingData": false,
  "seenRecords": {
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer",
      "__typename": "AppUser",
      "appUserId": 0,
      "firstName": "Anonymous",
      "lastName": null,
      "unviewedActivities(first:5)": {
        "__ref": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5)"
      },
      "__UnviewedActivities_list_connection": {
        "__ref": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection"
      }
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection",
      "__typename": "UnviewedActivitiesConnection",
      "__connection_next_edge_index": 5,
      "totalCount": 38,
      "edges": {
        "__refs": [
          "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:0",
          "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:1",
          "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:2",
          "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:3",
          "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:4"
        ]
      },
      "pageInfo": {
        "__ref": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:pageInfo"
      }
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:0": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:0",
      "__typename": "UnviewedActivitiesConnectionEdge",
      "node": {
        "__ref": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:0:node"
      },
      "cursor": "Ng=="
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:0:node": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:0:node",
      "__typename": "Activity",
      "activityId": 6,
      "name": "Virtual Drinks",
      "shortDescription": "Whether it's coffee break or happy hour, grab the beverage of your choice for a chemically-enhanced video chat.",
      "activityUrl": "https://www.nytimes.com/2020/03/20/well/virus-virtual-happy-hour.html",
      "imageUrl": "https://uploads-ssl.webflow.com/5e576c2ceb561c252e1d2e2d/5ec6e38e3e71f1a96e78f940_min-virtual%20coffee.jpg",
      "isApproved": true
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:1": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:1",
      "__typename": "UnviewedActivitiesConnectionEdge",
      "node": {
        "__ref": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:1:node"
      },
      "cursor": "Nw=="
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:1:node": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:1:node",
      "__typename": "Activity",
      "activityId": 7,
      "name": "Cook Mexican Street Tacos with a Pro Chef",
      "shortDescription": "Immerse yourself in Mexico's extraordinary street food culture in a guided cooking class with a professional chef.",
      "activityUrl": "https://www.airbnb.com/experiences/1661135?source=p2",
      "imageUrl": "https://a0.muscache.com/im/pictures/ec6a9398-1ed3-400a-8f97-ee3e7eeed6dd.jpg?aki_policy=exp_md",
      "isApproved": true
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:2": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:2",
      "__typename": "UnviewedActivitiesConnectionEdge",
      "node": {
        "__ref": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:2:node"
      },
      "cursor": "OA=="
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:2:node": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:2:node",
      "__typename": "Activity",
      "activityId": 8,
      "name": "Meet My Bees",
      "shortDescription": "Open a beehive with a fourth-generation beekeeper to see these amazing creatures building honeycombs, making honey, and working together.",
      "activityUrl": "https://www.airbnb.com/experiences/1675237?source=p2",
      "imageUrl": "https://a0.muscache.com/im/pictures/80add1a5-f051-4aea-a7ee-f5b4491d3346.jpg?aki_policy=exp_md",
      "isApproved": true
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:3": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:3",
      "__typename": "UnviewedActivitiesConnectionEdge",
      "node": {
        "__ref": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:3:node"
      },
      "cursor": "OQ=="
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:3:node": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:3:node",
      "__typename": "Activity",
      "activityId": 9,
      "name": "Mobile Photo Secrets with a Nat Geo Winner",
      "shortDescription": "Learn the secrets of taking amazing photos with your phone with examples from beautiful Barcelona.",
      "activityUrl": "https://www.airbnb.com/experiences/1718920?source=p2",
      "imageUrl": "https://a0.muscache.com/im/pictures/lombard/MtTemplate-1718920-media_library/original/b211f9ca-0154-4b7f-9fb3-76757298119e.jpeg?aki_policy=exp_md",
      "isApproved": true
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:4": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:edges:4",
      "__typename": "UnviewedActivitiesConnectionEdge",
      "node": {
        "__ref": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:4:node"
      },
      "cursor": "MTA="
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:4:node": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:unviewedActivities(first:5):edges:4:node",
      "__typename": "Activity",
      "activityId": 10,
      "name": "Draw from Within with a New York Artist",
      "shortDescription": "Re-connect, re-imagine & relax through the restorative power of the creative process.",
      "activityUrl": "https://www.airbnb.com/experiences/1655361?source=p2",
      "imageUrl": "https://a0.muscache.com/im/pictures/lombard/MtTemplate-1655361-media_library/original/5d1c7ca9-8397-4884-91c1-c5eb0fea7dff.jpg?aki_policy=exp_md",
      "isApproved": true
    },
    "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:pageInfo": {
      "__id": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer:__UnviewedActivities_list_connection:pageInfo",
      "__typename": "PageInfo",
      "hasNextPage": true,
      "hasPreviousPage": false,
      "endCursor": "MTA=",
      "startCursor": "Ng=="
    }
  },
  "selector": {
    "kind": "SingularReaderSelector",
    "dataID": "client:root:authInfo(redirectUri:\"http://localhost:3000/login\",source:\"google\"):viewer",
    "node": {
      "argumentDefinitions": [
        {
          "defaultValue": null,
          "kind": "LocalArgument",
          "name": "cursor",
          "type": "ID"
        },
        {
          "defaultValue": 5,
          "kind": "LocalArgument",
          "name": "count",
          "type": "Int!"
        }
      ],
      "kind": "Fragment",
      "metadata": {
        "connection": [
          {
            "count": "count",
            "cursor": "cursor",
            "direction": "forward",
            "path": ["list"]
          }
        ]
      },
      "name": "UnviewedActivities_user",
      "selections": [
        {
          "alias": null,
          "args": null,
          "kind": "ScalarField",
          "name": "appUserId",
          "storageKey": null
        },
        {
          "alias": "list",
          "args": null,
          "concreteType": "UnviewedActivitiesConnection",
          "kind": "LinkedField",
          "name": "__UnviewedActivities_list_connection",
          "plural": false,
          "selections": [
            {
              "alias": null,
              "args": null,
              "kind": "ScalarField",
              "name": "totalCount",
              "storageKey": null
            },
            {
              "alias": null,
              "args": null,
              "concreteType": "UnviewedActivitiesConnectionEdge",
              "kind": "LinkedField",
              "name": "edges",
              "plural": true,
              "selections": [
                {
                  "alias": null,
                  "args": null,
                  "concreteType": "Activity",
                  "kind": "LinkedField",
                  "name": "node",
                  "plural": false,
                  "selections": [
                    {
                      "alias": null,
                      "args": null,
                      "kind": "ScalarField",
                      "name": "activityId",
                      "storageKey": null
                    },
                    {
                      "alias": null,
                      "args": null,
                      "kind": "ScalarField",
                      "name": "name",
                      "storageKey": null
                    },
                    {
                      "alias": null,
                      "args": null,
                      "kind": "ScalarField",
                      "name": "shortDescription",
                      "storageKey": null
                    },
                    {
                      "alias": null,
                      "args": null,
                      "kind": "ScalarField",
                      "name": "activityUrl",
                      "storageKey": null
                    },
                    {
                      "alias": null,
                      "args": null,
                      "kind": "ScalarField",
                      "name": "imageUrl",
                      "storageKey": null
                    },
                    {
                      "alias": null,
                      "args": null,
                      "kind": "ScalarField",
                      "name": "isApproved",
                      "storageKey": null
                    },
                    {
                      "alias": null,
                      "args": null,
                      "kind": "ScalarField",
                      "name": "__typename",
                      "storageKey": null
                    }
                  ],
                  "storageKey": null
                },
                {
                  "alias": null,
                  "args": null,
                  "kind": "ScalarField",
                  "name": "cursor",
                  "storageKey": null
                }
              ],
              "storageKey": null
            },
            {
              "alias": null,
              "args": null,
              "concreteType": "PageInfo",
              "kind": "LinkedField",
              "name": "pageInfo",
              "plural": false,
              "selections": [
                {
                  "alias": null,
                  "args": null,
                  "kind": "ScalarField",
                  "name": "endCursor",
                  "storageKey": null
                },
                {
                  "alias": null,
                  "args": null,
                  "kind": "ScalarField",
                  "name": "hasNextPage",
                  "storageKey": null
                },
                {
                  "alias": null,
                  "args": null,
                  "kind": "ScalarField",
                  "name": "startCursor",
                  "storageKey": null
                },
                {
                  "alias": null,
                  "args": null,
                  "kind": "ScalarField",
                  "name": "hasPreviousPage",
                  "storageKey": null
                }
              ],
              "storageKey": null
            }
          ],
          "storageKey": null
        }
      ],
      "type": "AppUser",
      "hash": "e1164886b2d41be49df16b84abc3c0cf"
    },
    "variables": {
      "appUserId": 0,
      "count": 10,
      "cursor": null,
      "redirectUri": "http://localhost:3000/login",
      "source": "google"
    },
    "owner": {
      "identifier": "query UnviewedActivitiesRefetchQuery(  $appUserId: Int!  $count: Int!  $cursor: ID) {  user(id: $appUserId) {    ...UnviewedActivities_user_1G22uz  }}fragment UnviewedActivities_user_1G22uz on AppUser {  appUserId  list: unviewedActivities(first: $count, after: $cursor) {    totalCount    edges {      node {        activityId        name        shortDescription        activityUrl        imageUrl        isApproved        __typename      }      cursor    }    pageInfo {      endCursor      hasNextPage      startCursor      hasPreviousPage    }  }}{\"appUserId\":0,\"count\":10,\"cursor\":null,\"redirectUri\":\"http://localhost:3000/login\",\"source\":\"google\"}",
      "node": {
        "fragment": {
          "argumentDefinitions": [
            {
              "defaultValue": null,
              "kind": "LocalArgument",
              "name": "appUserId",
              "type": "Int!"
            },
            {
              "defaultValue": null,
              "kind": "LocalArgument",
              "name": "count",
              "type": "Int!"
            },
            {
              "defaultValue": null,
              "kind": "LocalArgument",
              "name": "cursor",
              "type": "ID"
            }
          ],
          "kind": "Fragment",
          "metadata": null,
          "name": "UnviewedActivitiesRefetchQuery",
          "selections": [
            {
              "alias": null,
              "args": [
                {
                  "kind": "Variable",
                  "name": "id",
                  "variableName": "appUserId"
                }
              ],
              "concreteType": "AppUser",
              "kind": "LinkedField",
              "name": "user",
              "plural": false,
              "selections": [
                {
                  "args": [
                    {
                      "kind": "Variable",
                      "name": "count",
                      "variableName": "count"
                    },
                    {
                      "kind": "Variable",
                      "name": "cursor",
                      "variableName": "cursor"
                    }
                  ],
                  "kind": "FragmentSpread",
                  "name": "UnviewedActivities_user"
                }
              ],
              "storageKey": null
            }
          ],
          "type": "RootQueryType"
        },
        "kind": "Request",
        "operation": {
          "argumentDefinitions": [
            {
              "defaultValue": null,
              "kind": "LocalArgument",
              "name": "appUserId",
              "type": "Int!"
            },
            {
              "defaultValue": null,
              "kind": "LocalArgument",
              "name": "count",
              "type": "Int!"
            },
            {
              "defaultValue": null,
              "kind": "LocalArgument",
              "name": "cursor",
              "type": "ID"
            }
          ],
          "kind": "Operation",
          "name": "UnviewedActivitiesRefetchQuery",
          "selections": [
            {
              "alias": null,
              "args": [
                {
                  "kind": "Variable",
                  "name": "id",
                  "variableName": "appUserId"
                }
              ],
              "concreteType": "AppUser",
              "kind": "LinkedField",
              "name": "user",
              "plural": false,
              "selections": [
                {
                  "alias": null,
                  "args": null,
                  "kind": "ScalarField",
                  "name": "appUserId",
                  "storageKey": null
                },
                {
                  "alias": "list",
                  "args": [
                    {
                      "kind": "Variable",
                      "name": "after",
                      "variableName": "cursor"
                    },
                    {
                      "kind": "Variable",
                      "name": "first",
                      "variableName": "count"
                    }
                  ],
                  "concreteType": "UnviewedActivitiesConnection",
                  "kind": "LinkedField",
                  "name": "unviewedActivities",
                  "plural": false,
                  "selections": [
                    {
                      "alias": null,
                      "args": null,
                      "kind": "ScalarField",
                      "name": "totalCount",
                      "storageKey": null
                    },
                    {
                      "alias": null,
                      "args": null,
                      "concreteType": "UnviewedActivitiesConnectionEdge",
                      "kind": "LinkedField",
                      "name": "edges",
                      "plural": true,
                      "selections": [
                        {
                          "alias": null,
                          "args": null,
                          "concreteType": "Activity",
                          "kind": "LinkedField",
                          "name": "node",
                          "plural": false,
                          "selections": [
                            {
                              "alias": null,
                              "args": null,
                              "kind": "ScalarField",
                              "name": "activityId",
                              "storageKey": null
                            },
                            {
                              "alias": null,
                              "args": null,
                              "kind": "ScalarField",
                              "name": "name",
                              "storageKey": null
                            },
                            {
                              "alias": null,
                              "args": null,
                              "kind": "ScalarField",
                              "name": "shortDescription",
                              "storageKey": null
                            },
                            {
                              "alias": null,
                              "args": null,
                              "kind": "ScalarField",
                              "name": "activityUrl",
                              "storageKey": null
                            },
                            {
                              "alias": null,
                              "args": null,
                              "kind": "ScalarField",
                              "name": "imageUrl",
                              "storageKey": null
                            },
                            {
                              "alias": null,
                              "args": null,
                              "kind": "ScalarField",
                              "name": "isApproved",
                              "storageKey": null
                            },
                            {
                              "alias": null,
                              "args": null,
                              "kind": "ScalarField",
                              "name": "__typename",
                              "storageKey": null
                            }
                          ],
                          "storageKey": null
                        },
                        {
                          "alias": null,
                          "args": null,
                          "kind": "ScalarField",
                          "name": "cursor",
                          "storageKey": null
                        }
                      ],
                      "storageKey": null
                    },
                    {
                      "alias": null,
                      "args": null,
                      "concreteType": "PageInfo",
                      "kind": "LinkedField",
                      "name": "pageInfo",
                      "plural": false,
                      "selections": [
                        {
                          "alias": null,
                          "args": null,
                          "kind": "ScalarField",
                          "name": "endCursor",
                          "storageKey": null
                        },
                        {
                          "alias": null,
                          "args": null,
                          "kind": "ScalarField",
                          "name": "hasNextPage",
                          "storageKey": null
                        },
                        {
                          "alias": null,
                          "args": null,
                          "kind": "ScalarField",
                          "name": "startCursor",
                          "storageKey": null
                        },
                        {
                          "alias": null,
                          "args": null,
                          "kind": "ScalarField",
                          "name": "hasPreviousPage",
                          "storageKey": null
                        }
                      ],
                      "storageKey": null
                    }
                  ],
                  "storageKey": null
                },
                {
                  "alias": "list",
                  "args": [
                    {
                      "kind": "Variable",
                      "name": "after",
                      "variableName": "cursor"
                    },
                    {
                      "kind": "Variable",
                      "name": "first",
                      "variableName": "count"
                    }
                  ],
                  "filters": null,
                  "handle": "connection",
                  "key": "UnviewedActivities_list",
                  "kind": "LinkedHandle",
                  "name": "unviewedActivities"
                }
              ],
              "storageKey": null
            }
          ]
        },
        "params": {
          "id": null,
          "metadata": {},
          "name": "UnviewedActivitiesRefetchQuery",
          "operationKind": "query",
          "text": "query UnviewedActivitiesRefetchQuery(  $appUserId: Int!  $count: Int!  $cursor: ID) {  user(id: $appUserId) {    ...UnviewedActivities_user_1G22uz  }}fragment UnviewedActivities_user_1G22uz on AppUser {  appUserId  list: unviewedActivities(first: $count, after: $cursor) {    totalCount    edges {      node {        activityId        name        shortDescription        activityUrl        imageUrl        isApproved        __typename      }      cursor    }    pageInfo {      endCursor      hasNextPage      startCursor      hasPreviousPage    }  }}"
        },
        "hash": "555ab7868d36d6356ff2a4e06048da12"
      },
      "variables": {
        "appUserId": 0,
        "count": 10,
        "cursor": null,
        "redirectUri": "http://localhost:3000/login",
        "source": "google"
      }
    }
  }
}

The screenshot is the data I was expecting to be rendered. data above is the previous data pulled in from the user prop the component was passed.

@nibblesnbits
Copy link
Author

nibblesnbits commented Jun 29, 2020

I did notice tonight that in https://github.com/relay-tools/relay-hooks/blob/master/src/FragmentResolver.ts#L404, we call getData() to grab the selected data from the current dataset, then we call changeVariables(), then another call to getData(). I'm assuming the idea was that changeVariables() would update the necessary variables to select the next result, but it doesn't seem to be doing that.

There's a couple obvious things to note here.

  1. The payload argument in that function already contains the correct new result set, so nextData could become just a selected portion of the payload argument
  2. It seems we need an additional call to resolve() somewhere in here to make sure everything is up to date. Even when I coerce prevData into the correct new result, there's still a ton of stuff not being updated to reflect the new data, and from what I can tell resolve() does a lot of that for us.
  3. Based on what you've directed me to so far, it may be as simple as the subscription bits are not working properly. I did notice on https://github.com/relay-tools/relay-hooks/blob/master/src/FragmentResolver.ts#L538 in my case, observer.start is undefined.

Am I at all on the right track? I'm working on finding a way to debug this in the actual TypeScript (npm link is all I need, I hope), so I may make more progress soon.

@morrys
Copy link
Member

morrys commented Jun 29, 2020

@nibblesnbits, reading your code it seems that the problem was the difference between the two queries executed.
The first one has as root viewer, the other user.

This function is called by relay when there is an update of the store to notify all the fragments / queries subscribed

@nibblesnbits
Copy link
Author

nibblesnbits commented Jun 30, 2020

I've updated the code to use a refetch, and ensured the queries match, but still no dice. In this case I'm simply asking for a larger result set, but I see the same result.

import PropTypes from "prop-types";
import React from "react";
import { ReactRelayContext, createFragmentContainer } from "react-relay";
import { graphql } from "babel-plugin-relay/macro";
import UnviewedActivities from "./UnviewedActivities";
import { Container, Typography } from "@material-ui/core";

const propTypes = {
  authInfo: PropTypes.object.isRequired,
  relay: PropTypes.object.isRequired,
};

const contextType = ReactRelayContext;

class Activities extends React.Component {
  render() {
    const { authInfo } = this.props;
    if (!authInfo) {
      return <div>Loading...</div>;
    }

    return (
      <Container>
        <Typography variant="h6">
          Welcome to my app! Select some activities to get started.
        </Typography>
        <UnviewedActivities authInfo={authInfo} />
      </Container>
    );
  }
}

Activities.propTypes = propTypes;
Activities.contextType = contextType;

export default createFragmentContainer(Activities, {
  authInfo: graphql`
    fragment Activities_authInfo on AuthInfo {
      ...UnviewedActivities_authInfo
    }
  `,
});
import PropTypes from "prop-types";
import React, { useState, useEffect } from "react";
import { graphql } from "babel-plugin-relay/macro";
import ActivityCard from "./ActivityCard";
import { Grid, Button } from "@material-ui/core";
import { useRefetch } from "relay-hooks";

const propTypes = {
  authInfo: PropTypes.object.isRequired,
};

const fragmentSpec = graphql`
  fragment UnviewedActivities_authInfo on AuthInfo
    @argumentDefinitions(
      cursor: { type: "ID" }
      count: { type: "Int!", defaultValue: 5 }
    ) {
    viewer {
      appUserId
      list: unviewedActivities(first: $count, after: $cursor)
        @connection(key: "UnviewedActivities_list") {
        edges {
          node {
            activityId
            name
            shortDescription
            activityUrl
            imageUrl
          }
          cursor
        }
        pageInfo {
          endCursor
          hasNextPage
        }
      }
    }
  }
`;

const connectionConfig = {
  direction: "forward",
  query: graphql`
    query UnviewedActivitiesRefetchQuery($count: Int!, $cursor: ID) {
      authInfo {
        ...UnviewedActivities_authInfo
          @arguments(cursor: $cursor, count: $count)
      }
    }
  `,
  getConnectionFromProps(props) {
    return props.list;
  },
  getFragmentVariables(previousVariables, totalCount) {
    return {
      ...previousVariables,
      count: totalCount,
    };
  },
  getVariables({ list }, { count, cursor }) {
    return {
      count,
      cursor: list.pageInfo.endCursor,
    };
  },
};

const UnviewedActivities = (props) => {
  const [authInfo, refetch] = useRefetch(fragmentSpec, props.authInfo);
  const [, setLoads] = useState(0);
  const [selected, setSelected] = useState([]);

  const {
    viewer: {
      list: { edges },
    },
  } = authInfo;

  useEffect(() => {
    const key = "app:selectedActivities";
    const item = localStorage.getItem(key);
    if (item) {
      setSelected(JSON.parse(item));
    } else {
      localStorage.setItem(key, JSON.stringify([]));
    }
  }, []);

  const loadNextPage = () => {
    // if (!hasMore() || isLoading()) {
    //   return;
    // }
    refetch(
      connectionConfig.query,
      { count: 10 },
      null,
      () => {
        setLoads((v) => v + 1);
        console.log("loaded more");
      },
      {
        force: true,
      }
    );
  };

  const addActivity = (id, pass) => {
    const key = "app:selectedActivities";
    const stored = localStorage.getItem(key) || [];
    const parsed = stored instanceof Array ? stored : JSON.parse(stored);
    const selected = [...parsed, { id, pass }];
    localStorage.setItem(key, JSON.stringify(selected));
    setSelected(selected);

    const remaining = edges.filter(
      ({ node: a }) => !selected.some((s) => s.id === a.activityId)
    );

    if (remaining.length === 1) {
      loadNextPage();
    }
  };

  const remaining = edges.filter(
    ({ node: a }) => !selected.some((s) => s.id === a.activityId)
  );

  return (
    <>
      <Button onClick={() => loadNextPage()}>Load More</Button>
      <Grid
        container
        direction="row"
        justify="center"
        alignItems="center"
        spacing={2}
      >
        {remaining.map(({ node: activity }) => (
          <Grid item key={activity.activityId}>
            <ActivityCard {...activity} addActivity={addActivity} />
          </Grid>
        ))}
      </Grid>
    </>
  );
};

UnviewedActivities.propTypes = propTypes;
export default UnviewedActivities;

refetch() still makes the correct fetch call and returns the correct data, but still does not update the component.

@morrys
Copy link
Member

morrys commented Jun 30, 2020

In this function you can see the relay logics of fragment update notification.

Here you can find all the tests that are performed for usePagination

The refetch/loadMore appears to execute a different query than the one the fragment was subscribed to.

Could you send me the response of the query and the response of the refetch (you can avoid including all the edges).

@nibblesnbits
Copy link
Author

First response:

{
  "data": {
    "authInfo": {
      "viewer": {
        "appUserId": 0,
        "list": {
          "edges": [
            // ...
          ],
          "pageInfo": { "endCursor": "MTA=", "hasNextPage": true }
        }
      }
    }
  }
}

Second response:

{
  "data": {
    "authInfo": {
      "viewer": {
        "appUserId": 0,
        "list": {
          "edges": [
            // ...
          ],
          "pageInfo": { "endCursor": "MTU=", "hasNextPage": true }
        }
      }
    }
  }
}

@morrys
Copy link
Member

morrys commented Jun 30, 2020

have you tried debugging In this function?

@morrys
Copy link
Member

morrys commented Jun 30, 2020

even better if you can create a minimal example project on github so that i can investigate the error

@nibblesnbits
Copy link
Author

@morrys
Copy link
Member

morrys commented Jun 30, 2020

The fragment is not updated by the Relay store because the query is performed with different parameters. This for Relay is as if a different query had been executed.

to work it is necessary to modify fetchQuery in UnviewedActivities.js

const fetchQuery = graphql`
  query UnviewedActivitiesRefetchQuery(
    $redirectUri: String!
    $source: String!
    $count: Int!
    $cursor: ID
  ) {
    authInfo(redirectUri: $redirectUri, source: $source) {
      ...UnviewedActivities_authInfo @arguments(cursor: $cursor, count: $count)
    }
  }
`;

@nibblesnbits
Copy link
Author

That works!

But there's one new problem. It's pulling in the new data, but only appending the data, not replacing the entire result set with the new edges. Is this intended?

Pull on the repo to get what I mean.

@morrys
Copy link
Member

morrys commented Jul 1, 2020

yes, this is its behavior.
Maybe @sibelius can give you some advice for your use case :)

@sibelius
Copy link
Contributor

sibelius commented Jul 1, 2020

the default behavior of connection is to append in the end

https://github.com/facebook/relay/blob/master/packages/relay-runtime/handlers/RelayDefaultHandlerProvider.js#L25

check the connection handler code, you can modify it to your will

@nibblesnbits
Copy link
Author

My problem is solved! I guess it was never a bug to begin with. Sorry! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants