facebook / relay

Relay is a JavaScript framework for building data-driven React applications.
https://relay.dev
MIT License
18.4k stars 1.82k forks source link

Pagination fails after refetching connection #2468

Closed stan-sack closed 6 years ago

stan-sack commented 6 years ago

Initially my component paginates fine. I am able to update my filter and call relay.refetchConnection which returns the new connection and as well as a new cursor which I can see from logging the props in getConnectionFromProps.

My issue is that once i try to scroll my list and relay.loadMore is called to get the next page of the new filtered list, the cursor and query never update. I can see the request on my server asking for the next page which I believe executes correctly but relay never loads the new data and every subsequent call to loadMore sends the same request. I think that loadMore is not updating the internal state correctly after refetchConnection is called.

My component is as shown below:

import React from 'react'
import { View, TextInput, Text, TouchableOpacity } from 'react-native'
import { FormLabel, FormValidationMessage } from 'react-native-elements'
import { createPaginationContainer, graphql, QueryRenderer } from 'react-relay'
import ModalFilterPicker from '../../components/ModalFilterPicker'

import { withNavigation } from 'react-navigation'
import environment from '../../createRelayEnvironment'
import hoistStatics from 'hoist-non-react-statics'

@withNavigation
class AgencySelector extends React.Component {
    constructor(props, ctx) {
        super(props, ctx)

        this.state = {
            modalVisible: false,
            filter: null,
            refreshing: false,
        }
    }

    _loadMore() {
        if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
            return
        }
        this.setState({ refreshing: true })
        this.props.relay.loadMore(
            10, // Fetch the next 10 feed items
            () => {
                this.setState({ refreshing: false })
            }
        )
    }

    _updateText(text) {
        this.setState({ filter: text })
        this._refetch()
    }

    _refetch() {
        if (this.props.relay.isLoading() || !this.state.filter) {
            return
        }

        this.setState({ refreshing: true })

        this.props.relay.refetchConnection(
            30,
            () => {
                this.setState({ refreshing: false })
            },
            {
                filter: {
                    field: 'name',
                    value: this.state.filter,
                },
                count: 30,
            }
        )
    }

    render() {
        let agencies = this.props.agencies.findAgencies.edges.map((edge, i) => {
            return {
                label: edge.node.name,
                key: i,
            }
        })
        return (
            <View>
                <FormLabel>Agency</FormLabel>
                <TouchableOpacity
                    onPress={() => this.setState({ modalVisible: true })}>
                    <TextInput
                        style={{
                            borderWidth: 1,
                            borderColor: '#ccc',
                            padding: 10,
                            height: 40,
                        }}
                        editable={false}
                        placeholder="Name of the agency"
                        value={this.props.parentProps.value}
                        pointerEvents="none"
                    />
                </TouchableOpacity>

                <ModalFilterPicker
                    onFilterTextChanged={text => this._updateText(text)}
                    options={agencies}
                    visible={this.state.modalVisible}
                    onSelect={option =>
                        this.props.parentProps.onOptionSelected(option.label)
                    }
                    onCancel={() => this.setState({ modalVisible: false })}
                    flatListViewProps={{
                        onEndReached: () => this._loadMore(),
                        onEndReachedThreshold: 3,
                        refreshing: this.state.refreshing,
                        onRefresh: () => this._refetch(),
                    }}
                />

                {this.props.error &&
                    this.props.parentProps.showError && (
                        <FormValidationMessage>
                            this.props.parentProps.error
                        </FormValidationMessage>
                    )}
            </View>
        )
    }
}

const AgenciesPaginationContainer = createPaginationContainer(
    AgencySelector,
    graphql`
        fragment AgencySelector_agencies on Query {
            findAgencies(
                after: $cursor
                first: $count
                countryName: $countryName
                state: $state
                filter: $filter
            ) @connection(key: "AgencySelector_findAgencies") {
                edges {
                    node {
                        name
                    }
                }
            }
        }
    `,
    {
        direction: 'forward',
        query: graphql`
            query AgencySelectorPaginationQuery(
                $cursor: String
                $count: Int
                $countryName: String
                $state: String
                $filter: FilterInput
            ) {
                ...AgencySelector_agencies
            }
        `,
        getConnectionFromProps(props) {
            console.log(props)
            return props.agencies && props.agencies.findAgencies
        },
        getFragmentVariables(previousVariables, totalCount) {
            return {
                ...previousVariables,
                count: totalCount,
            }
        },
        getVariables(props, { count, cursor }, fragmentVariables) {
            return {
                cursor: cursor,
                count,
            }
        },
    }
)

const rootQuery = graphql`
    query AgencySelectorQuery(
        $count: Int
        $cursor: String
        $countryName: String
        $state: String
        $filter: FilterInput
    ) {
        ...AgencySelector_agencies
    }
`

const AgencySelectorQueryRenderer = props => {
    let parentProps = props
    return (
        <QueryRenderer
            environment={environment}
            query={rootQuery}
            variables={{
                count: 30,
                countryName: props.countryName,
                state: props.state,
            }}
            render={({ error, props }) => {
                if (props) {
                    return (
                        <AgenciesPaginationContainer
                            agencies={props}
                            parentProps={parentProps}
                        />
                    )
                } else {
                    return <Text>Loading</Text>
                }
            }}
        />
    )
}

export default hoistStatics(AgencySelectorQueryRenderer, AgencySelector)
sibelius commented 6 years ago

try to do pagination using refetchContainer, check this guide https://github.com/entria/guidelines/blob/master/relay/flatlist-relaymodern.md#onendreached-relay-modern-createrefetchcontainer

stan-sack commented 6 years ago

@sibelius I followed your guidelines but still no luck. I think it's because I don't pass the arguments down correctly and because of this when I try to paginate after refetching, the new cursor and field params are not propagated.

I am getting the error GraphQLParser: Unknown directive @argumentDefinitions. when I try to compile the code. Do you know how I can fix this error? How do you disable the compiler check for this? I see a lot of people using these directives but cant find any documentation about how to enable them.

sibelius commented 6 years ago

use relay 1.5.0 or the latest version to have @argumentDefinitions

read more about it here https://medium.com/entria/relay-modern-argumentdefinitions-d53769dbb95d

more about how connection are handled in relay:

all connections need @connection, this is how relay can find the connection.

@connection has to params:

so for you example

it will create a new connection based on different values of your connection args

countryName: $countryName
                state: $state
                filter: $filter

you should use filters: [] to create only one connection

I think on paginationContainer you need to use getVariables and pass with args are you using to select the correct connection to read data from.

that's what we do with renderVariables on refetchContainer

stan-sack commented 6 years ago

Yes I understand. I've read all of your blogs - they are basically the only in depth resource on relay modern. I am actually using the your ReactNavigationRelayModern boilerplate :).

I am on relay 1.6 which is why I am confused that I am getting this error.

My package.json looks like this:

    "dependencies": {
        "lodash": "^4.17.10",
        "react": "^16.0.0",
        "react-native": "^0.55.4",
        "react-native-datepicker": "^1.7.2",
        "react-native-easy-toast": "^1.1.0",
        "react-native-elements": "^0.19.1",
        "react-native-facebook-login": "^1.6.1",
        "react-native-google-places-autocomplete": "^1.3.6",
        "react-native-modal-filter-picker": "^1.3.4",
        "react-native-modal-selector": "0.0.27",
        "react-native-permissions": "^1.1.1",
        "react-native-radio-buttons": "^1.0.0",
        "react-native-sensitive-info": "git://github.com/mcodex/react-native-sensitive-info.git#keystore",
        "react-native-splash-screen": "^3.0.6",
        "react-native-svg": "^6.3.1",
        "react-native-vector-icons": "^4.6.0",
        "react-navigation": "^2.0.1",
        "react-relay": "^1.6.0",
        "victory-native": "^0.17.4"
    },
    "devDependencies": {
        "@babel/core": "^7.0.0-beta.47",
        "babel-eslint": "^8.2.3",
        "babel-jest": "22.4.3",
        "babel-plugin-relay": "^1.6.0",
        "babel-polyfill": "^6.26.0",
        "babel-preset-react-native": "4.0.0",
        "babel-preset-react-native-stage-0": "^1.0.1",
        "eslint": "^4.19.1",
        "eslint-config-prettier": "^2.9.0",
        "eslint-config-standard": "^12.0.0-alpha.0",
        "eslint-config-standard-react": "^6.0.0",
        "eslint-plugin-babel": "^5.1.0",
        "eslint-plugin-flowtype": "^2.46.3",
        "eslint-plugin-import": "^2.11.0",
        "eslint-plugin-node": "^6.0.1",
        "eslint-plugin-prettier": "^2.6.0",
        "eslint-plugin-promise": "^3.7.0",
        "eslint-plugin-react": "^7.8.2",
        "eslint-plugin-react-native": "^3.2.1",
        "eslint-plugin-standard": "^3.1.0",
        "graphql": "^0.13.2",
        "hoist-non-react-statics": "^2.5.0",
        "jest": "22.4.3",
        "lint-staged": "^7.1.0",
        "prettier": "^1.12.1",
        "prettier-eslint": "^8.8.1",
        "react-devtools": "^3.2.3",
        "react-test-renderer": "16.3.2",
        "relay-compiler": "^1.6.0",
        "relay-devtools": "^1.4.0"
    }
sibelius commented 6 years ago

try to use relay 1.5.0 on react-native

image

relay 1.6.0 requires react 16.3.0

if you use an older react version it causes some bugs

stan-sack commented 6 years ago

I got it working. For anyone reading this in future the working component looks like this:

import React from 'react'
import { View, TextInput, Text, TouchableOpacity } from 'react-native'
import { FormLabel, FormValidationMessage } from 'react-native-elements'
import { createPaginationContainer, graphql, QueryRenderer } from 'react-relay'
import ModalFilterPicker from '../../components/ModalFilterPicker'

import { withNavigation } from 'react-navigation'
import environment from '../../createRelayEnvironment'
import hoistStatics from 'hoist-non-react-statics'

@withNavigation
class AgencySelector extends React.Component {
    constructor(props, ctx) {
        super(props, ctx)

        this.state = {
            modalVisible: false,
            filter: null,
            refreshing: false,
        }
    }

    _loadMore() {
        if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
            return
        }
        this.setState({ refreshing: true })
        this.props.relay.loadMore(
            10, // Fetch the next 10 feed items
            () => {
                this.setState({ refreshing: false })
            }
        )
    }

    _updateText(text) {
        this.setState({ filter: text })
        this._refetch()
    }

    _refetch() {
        if (this.props.relay.isLoading() || !this.state.filter) {
            return
        }

        this.setState({ refreshing: true })

        this.props.relay.refetchConnection(
            30,
            () => {
                this.setState({ refreshing: false })
            },
            {
                filter: {
                    field: 'name',
                    value: this.state.filter,
                },
                count: 30,
                countryName: this.props.countryName,
                state: this.props.state,
            }
        )
    }

    render() {
        let agencies = this.props.agencies.findAgencies.edges.map((edge, i) => {
            return {
                label: edge.node.name,
                key: i,
            }
        })
        return (
            <View>
                <FormLabel>Agency</FormLabel>
                <TouchableOpacity
                    onPress={() => this.setState({ modalVisible: true })}>
                    <TextInput
                        style={{
                            borderWidth: 1,
                            borderColor: '#ccc',
                            padding: 10,
                            height: 40,
                        }}
                        editable={false}
                        placeholder="Name of the agency"
                        value={this.props.parentProps.value}
                        pointerEvents="none"
                    />
                </TouchableOpacity>

                <ModalFilterPicker
                    onFilterTextChanged={text => this._updateText(text)}
                    options={agencies}
                    visible={this.state.modalVisible}
                    onSelect={option =>
                        this.props.parentProps.onOptionSelected(option.label)
                    }
                    onCancel={() => this.setState({ modalVisible: false })}
                    flatListViewProps={{
                        onEndReached: () => this._loadMore(),
                        onEndReachedThreshold: 3,
                    }}
                />

                {this.props.error &&
                    this.props.parentProps.showError && (
                        <FormValidationMessage>
                            this.props.parentProps.error
                        </FormValidationMessage>
                    )}
            </View>
        )
    }
}

const AgenciesPaginationContainer = createPaginationContainer(
    AgencySelector,
    graphql`
        fragment AgencySelector_agencies on Query {
            findAgencies(
                after: $after
                first: $count
                filter: $filter
                countryName: $countryName
                state: $state
            ) @connection(key: "AgencySelector_findAgencies") {
                edges {
                    node {
                        name
                    }
                }
                pageInfo {
                    hasNextPage
                    endCursor
                }
            }
        }
    `,
    {
        direction: 'forward',
        query: graphql`
            query AgencySelectorPaginationQuery(
                $after: String
                $count: Int
                $countryName: String
                $state: String
                $filter: FilterInput
            ) {
                ...AgencySelector_agencies
            }
        `,
        getConnectionFromProps(props) {
            return props.agencies && props.agencies.findAgencies
        },
        getFragmentVariables(previousVariables, totalCount) {
            return {
                ...previousVariables,
                count: totalCount,
            }
        },
        getVariables(props, { count, cursor }, fragmentVariables) {
            return {
                after: cursor,
                count: count,
                countryName: fragmentVariables.countryName,
                state: fragmentVariables.state,
                filter: fragmentVariables.filter,
            }
        },
    }
)

const rootQuery = graphql`
    query AgencySelectorQuery(
        $count: Int
        $after: String
        $countryName: String
        $state: String
        $filter: FilterInput
    ) {
        ...AgencySelector_agencies
    }
`

const AgencySelectorQueryRenderer = props => {
    let parentProps = props
    return (
        <QueryRenderer
            environment={environment}
            query={rootQuery}
            variables={{
                count: 30,
                after: null,
                countryName: props.countryName,
                state: props.state,
                filter: {
                    field: '',
                    value: '',
                },
            }}
            render={({ error, props }) => {
                if (props) {
                    return (
                        <AgenciesPaginationContainer
                            agencies={props}
                            parentProps={parentProps}
                        />
                    )
                } else {
                    return <Text>Loading</Text>
                }
            }}
        />
    )
}

export default hoistStatics(AgencySelectorQueryRenderer, AgencySelector)

Thanks for your help @sibelius.

hisapy commented 6 years ago

I just came across a similar problem with the PaginationContainer. I've upgraded from 1.6.1 to 1.6.2 and my pagination stopped working. Finally, after some painful debugging and got to this post and as mentioned in https://github.com/facebook/relay/issues/2468#issuecomment-397610337 I added filters: [] to my connection directive.

@connection(key: "PeoplePaginationContainer_people", filters: [])

Now my component is back to live again.

Anyway, according to the docs, filters is optional, however, I believe that if you have to provide the empty array, then it is actually not optional.

sibelius commented 6 years ago

filters tell Relay if you want to create a different connection for each different arguments of the connection

Usually you only need one connection, so using filters:[] works fine.

However, if you have multiple components that is using this connection with different arguments, you should provide filters to make this work

hisapy commented 6 years ago

I have a couple or maybe 3 components using the same pagination container. It worked fine without the filters in 1.6.1, and it stopped working for components using the pagination container with $search arg in 1.6.2.

It wasn't easy to find out what was wrong because there was no error in the js console, but anyway, I hope google can point to this thread if other folks run into similar problem.

Yashika-Sorathia commented 4 years ago

Hey I've been trying to fetch next data using pagination container and refetching connection and i do not receive new data. Request to my graphql severe is made but it still fetches the data from given offset. How I am i suppose to fetch new data? Also I am using relay modern.

stan-sack commented 4 years ago

It sounds like your variables aren't setup correctly. In particular after. It's hard to say for sure without a snippet.

wholenews commented 3 years ago

I have a couple or maybe 3 components using the same pagination container. It worked fine without the filters in 1.6.1, and it stopped working for components using the pagination container with $search arg in 1.6.2.

It wasn't easy to find out what was wrong because there was no error in the js console, but anyway, I hope google can point to this thread if other folks run into similar problem.

Wow. Thanks! I was also using $search and could not for the life of me understand the difference between a failing pagination case and my working ones. Until I found your post!