relay-tools / relay-hooks

Use Relay as React hooks
https://relay-tools.github.io/relay-hooks/docs/relay-hooks.html
MIT License
542 stars 56 forks source link

usePagination doesn't update state #96

Closed nibblesnbits closed 4 years ago

nibblesnbits commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

have you tried debugging In this function?

morrys commented 4 years ago

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

nibblesnbits commented 4 years ago

https://github.com/nibblesnbits/relay-hooks-test

morrys commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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

sibelius commented 4 years ago

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 commented 4 years ago

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