How data.refetch() function from react-apollo works - reactjs

On the frontend, I am using ReactJS and trying to build-in a filtering option to a list view. The list view correctly getting data from graphql endpoint by issuing this graphql query:
query getVideos($filterByBook: ID, $limit: Int!, $after: ID) {
videosQuery(filterByBook: $filterByBook, limit: $limit, after: $after) {
totalCount
edges {
cursor
node {
id
title
ytDefaultThumbnail
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
On the initial load $filterByBook variable is set to null, so the query correctly returns all pages for all nodes (query returns a paginated result). Then, by clicking on the filter (filter by book) another graphql query is issuing, but it always returns the same data. Here is a code snippet for filtering component
renderFilters() {
const { listOfBooksWithChapters, refetch } = this.props;
return (
<Row>
<FilterBooks
onBookTitleClickParam={(onBookTitleClickParam) => {
return refetch({
variables: {
limit: 3,
after: 0,
filterByBook: onBookTitleClickParam
}
})
}}
listOfBooksWithChapters={listOfBooksWithChapters}
/>
</Row>
)
}
And, here is complete code without imports for the list view component
class VideoList extends React.Component {
constructor(props) {
super(props);
this.subscription = null;
}
componentWillUnmount() {
if (this.subscription) {
// unsubscribe
this.subscription();
}
}
renderVideos() {
const { videosQuery } = this.props;
return videosQuery.edges.map(({ node: { id, title, ytDefaultThumbnail } }) => {
return (
<Col sm="4" key={id}>
<Card>
<CardImg top width="100%" src={ytDefaultThumbnail} alt="video image" />
<CardBlock>
<CardTitle>
<Link
className="post-link"
to={`/video/${id}`}>
{title}
</Link>
</CardTitle>
</CardBlock>
</Card>
</Col>
);
});
}
renderLoadMore() {
const { videosQuery, loadMoreRows } = this.props;
if (videosQuery.pageInfo.hasNextPage) {
return (
<Button id="load-more" color="primary" onClick={loadMoreRows}>
Load more ...
</Button>
);
}
}
renderFilters() {
const { listOfBooksWithChapters, refetch } = this.props;
return (
<Row>
<FilterBooks
onBookTitleClickParam={(onBookTitleClickParam) => {
return refetch({
variables: {
limit: 3,
after: 0,
filterByBook: onBookTitleClickParam
}
})
}}
listOfBooksWithChapters={listOfBooksWithChapters}
/>
</Row>
)
}
render() {
const { loading, videosQuery } = this.props;
if (loading && !videosQuery) {
return (
<div>{ /* loading... */}</div>
);
} else {
return (
<div>
<Helmet
title="Videos list"
meta={[{
name: 'description',
content: 'List of all videos'
}]} />
<h2>Videos</h2>
{this.renderFilters()}
<Row>
{this.renderVideos()}
</Row>
<div>
<small>({videosQuery.edges.length} / {videosQuery.totalCount})</small>
</div>
{this.renderLoadMore()}
</div>
);
}
}
}
export default compose(
graphql(VIDEOS_QUERY, {
options: () => {
return {
variables: {
limit: 3,
after: 0,
filterByBook: null
},
};
},
props: ({ data }) => {
const { loading, videosQuery, fetchMore, subscribeToMore, refetch } = data;
const loadMoreRows = () => {
return fetchMore({
variables: {
after: videosQuery.pageInfo.endCursor,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const totalCount = fetchMoreResult.videosQuery.totalCount;
const newEdges = fetchMoreResult.videosQuery.edges;
const pageInfo = fetchMoreResult.videosQuery.pageInfo;
return {
videosQuery: {
totalCount,
edges: [...previousResult.videosQuery.edges, ...newEdges],
pageInfo,
__typename: "VideosQuery"
}
};
}
});
};
return { loading, videosQuery, subscribeToMore, loadMoreRows, refetch };
}
}),
graphql(LIST_BOOKS_QUERY, {
props: ({ data }) => {
const { listOfBooksWithChapters } = data;
return { listOfBooksWithChapters };
}
}),
)(VideoList);
Question:
Why refetch function returns data without taking into account new variable filterByBook? How to check which variables object I supplied to the refetch function? Do I need to remap data that I receive from refetch function back to the component props?
EDIT:
I found the way to find what variable object I supplied to the query and found that variable object on filtering event returns this data
variables:Object
after:0
limit:3
variables:Object
after:0
filterByBook:"2"
limit:3

you can do it by adding refetch in useQuery
const {loading, data, error,refetch} = useQuery(GET_ALL_PROJECTS,
{
variables: {id: JSON.parse(localStorage.getItem("user")).id}
});
then call function refetch()
const saveChange = input => {
refetch();
};
OR
const saveChange = input => {
setIsOpen(false);
addProject({
variables: {
createBacklogInput: {
backlogTitle: backlogInput,
project:id
}
}
}).then(refetch);

It seem that refetch function is not meant to refetch data with different variables set (see this discussion).
I finally and successfully solved my issue with the help from this article

Related

infinite scroll relay modern (v11) using the pagination container and loadMore

Using react 17 and relay-modern 11 I want to build a list, with which, when one reaches the end, they can click a button that says load more and it adds more entries to the list. Here is what I have so far. The rows are name and cursor
see I should click load more and it should add an extra 5 rows of data. This is what happens when I click load more
See it did fetch 5 new nodes. I can tell because the cursors are unique. However it did not append it to the list like I wanted.
How can I get this list to continue to build all the time as I keep clicking load more, until there is no more data?
Here is my code:
the root of the component:
// index.js
import React, { useState } from "react";
import { Text, View } from "react-native";
import { QueryRenderer, useRelayEnvironment } from "react-relay";
import PaginatedProfilesListContainer from "./PaginatedProfilesListContainer";
const ProfilesList = () => {
const environment = useRelayEnvironment();
const [count, setCount] = useState(5);
const [name, setName] = useState("");
const query = graphql`
query ProfilesListQuery(
$count: Int!
$cursor: String
$filter: ProfileFilter
) {
...PaginatedProfilesListContainer_list
}
`;
const filter = {
name,
};
const variables = {
count: 5,
cursor: null,
filter,
};
const render = ({ error, props, retry }) => {
if (error) {
return (
<View>
<Text>{error.message}</Text>
</View>
);
} else if (props) {
return (
<View>
<PaginatedProfilesListContainer
pagination={props}
count={count}
setCount={setCount}
name={name}
setName={setName}
filter={filter}
/>
</View>
);
} else {
return (
<View>
<Text>loading profiles list...</Text>
</View>
);
}
};
console.log("vriable", variables)
return (
<QueryRenderer
environment={environment}
query={query}
variables={variables}
render={render}
/>
);
};
export default ProfilesList;
here is the component that should be listing the object
// PaginatedProfilesListContainer.js
import React from "react";
import { Text, View } from "react-native";
import { Button } from "react-native-paper";
import { createPaginationContainer } from "react-relay";
import { FadoTextInput } from "../forms/fadoTextInput";
const PaginatedProfilesListContainer = (props) => {
console.log(props);
console.log("createPaginationContainer", createPaginationContainer)
// console.log(pagination)
const { pagination, count, setCount, name, setName, relay, filter } = props;
const { hasMore, loadMore, refetchConnection } = relay;
console.log(loadMore)
const { profiles } = pagination;
const { edges, pageInfo } = profiles;
return (
<View>
<View>
<FadoTextInput
dense={true}
isNumeric={true}
graphqlErrors={[]}
label="count"
errorKey="count"
// savedValue={price.amount}
value={count}
onChangeText={setCount}
/>
<FadoTextInput
dense={true}
isNumeric={false}
graphqlErrors={[]}
label="name"
errorKey="name"
// savedValue={price.amount}
value={name}
onChangeText={setName}
/>
</View>
{edges.map(({ cursor, node: { name } }) => (
<View key={cursor} style={{ display: "flex", flexDirection: "row"}}>
<Text>{name}</Text>
<Text>{cursor}</Text>
</View>
))}
<Button disabled={!hasMore()} onPress={() => loadMore(count, (error) => { error && console.log("error", error); })}>
Load More
</Button>
</View>
);
};
export default createPaginationContainer(
PaginatedProfilesListContainer,
{
pagination: graphql`
fragment PaginatedProfilesListContainer_list on RootQueryType {
profiles(first: $count, after: $cursor, filter: $filter)
#connection(key: "PaginatedProfilesListContainer_profiles") {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
edges {
cursor
node {
name
}
}
}
}
`,
},
{
direction: 'forward',
query: graphql`
query PaginatedProfilesListContainerQuery(
$count: Int!
$cursor: String
$filter: ProfileFilter
) {
...PaginatedProfilesListContainer_list
}
`,
getConnectionFromProps(props) {
console.log(props)
return props.pagination?.profiles
},
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
filter: {},
};
},
}
);
I got the inspiration for this approach from https://github.com/facebook/relay/issues/1705
Note: I tried to use the #stream_connection, but the Elixir Absinthe on the backend doesn't seem tpo support it.
I know this is long so please any help appreciated :)

React.js error: The service worker navigation preload request was cancelled before 'preloadResponse' settled

I have an issue with my React application (with Redux Saga), I'm getting the console error:
The service worker navigation preload request was cancelled before 'preloadResponse' settled. If you intend to use 'preloadResponse', use waitUntil() or respondWith() to wait for the promise to settle.
I see this error on the console only on Chrome, not in Firefox or Edge.
This error does not affect my application.
The following steps reproduce the error:
1. Main page upload.
2. Go to movie details page.
3. Go back to main page.
Main.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { mainActions } from '../../store/actions/actions';
import './Main.scss';
import { MoviesList, SearchPanel } from '../../components';
const propTypes = {};
const defaultProps = {};
class Main extends Component {
constructor(props) {
super(props);
this.handleSearchTextChange = this.handleSearchTextChange.bind(this);
this.handleLoadMoreButtonClick = this.handleLoadMoreButtonClick.bind(this);
this.handleMovieClick = this.handleMovieClick.bind(this);
this.handleFavoriteMovieClick = this.handleFavoriteMovieClick.bind(this);
}
componentDidMount() {
this.handleComponentDidMount();
}
handleComponentDidMount() {
const { moviesList } = this.props;
if (!moviesList || moviesList.length <= 0) {
this.getMovies(null, false);
}
}
handleLoadMoreButtonClick() {
this.getMovies(null, false);
}
handleMovieClick(e) {
if (e.target.className === 'movie') {
this.props.history.push(`/details/${e.currentTarget.dataset.id}`);
}
}
handleSearchTextChange(e) {
const { pageNumber, favoriteMoviesList } = this.props;
this.props.onSearchTextChange({
searchText: e.target.value,
pageNumber: pageNumber,
favoriteMoviesList: favoriteMoviesList
});
}
handleFavoriteMovieClick(e) {
const { id, name, posterId } = e.currentTarget.dataset;
const { moviesList, favoriteMoviesList } = this.props;
this.props.onUpdateFavoriteMovies({
updatedMovie: { id: id, name: name, posterId: posterId },
favoriteMoviesList: favoriteMoviesList,
moviesList: moviesList
});
}
getMovies(updatedSearchText, isSearchChange) {
const { searchText, pageNumber, favoriteMoviesList } = this.props;
this.props.onLoadMovies({
pageNumber: pageNumber,
favoriteMoviesList: favoriteMoviesList,
updatedSearchText: isSearchChange ? updatedSearchText : searchText,
isSearchChange: isSearchChange
});
}
render() {
const { searchText, isLoadingMoreMovies, isPager, moviesList } = this.props;
return (
<div className="main-area">
<SearchPanel
searchText={searchText}
onSearchTextChange={this.handleSearchTextChange}
/>
<MoviesList
pageName='movies'
moviesList={moviesList}
isLoadingMoreMovies={isLoadingMoreMovies}
isPager={isPager}
onLoadMoreClick={this.handleLoadMoreButtonClick}
onMovieClick={this.handleMovieClick}
onFavoriteMovieClick={this.handleFavoriteMovieClick}
/>
</div>
);
}
}
Main.propTypes = propTypes;
Main.defaultProps = defaultProps;
const mapStateToProps = (state) => {
return {
searchText: state.main.searchText,
pageNumber: state.main.pageNumber,
isLoadingMoreMovies: state.main.isLoadingMoreMovies,
isPager: state.main.isPager,
moviesList: state.main.moviesList,
favoriteMoviesList: state.main.favoriteMoviesList
};
};
const mapDispatchToProps = (dispatch) => {
return {
onLoadMovies: (request) => dispatch(mainActions.loadMovies(request)),
onSearchTextChange: (request) => dispatch(mainActions.searchTextChange(request)),
onUpdateFavoriteMovies: (request) => dispatch(mainActions.updateFavoriteMovies(request))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Main);
Details.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { detailsActions, mainActions } from '../../store/actions/actions';
import './Details.scss';
import { ActorsList, ButtonClick, CrewsList, FeaturesList, PageTitle, ProductionsList, Rating, Trailer } from '../../components';
import movieUtils from '../../utils/movie.utils';
const propTypes = {};
const defaultProps = {};
class Details extends Component {
constructor(props) {
super(props);
this.handleBackClick = this.handleBackClick.bind(this);
this.handleFavoriteMovieClick = this.handleFavoriteMovieClick.bind(this);
this.isFavorite = false;
}
componentDidMount() {
this.handleComponentDidMount();
}
handleComponentDidMount() {
if (this.props.moviesList.length <= 0) {
this.handleBackClick();
return;
}
const movieId = this.props.match.params.id;
if (!movieId) {
this.handleBackClick();
return;
}
this.props.onLoadMovieDetails(movieId);
this.updateIsFavorite(movieId);
}
numberWithCommas(number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
updateIsFavorite(movieId) {
this.isFavorite = this.props.favoriteMoviesList.findIndex(movie => parseInt(movie.id) === parseInt(movieId)) > -1;
}
handleBackClick() {
this.props.history.push(`/`);
}
handleFavoriteMovieClick() {
const { movie, moviesList, favoriteMoviesList } = this.props;
this.props.onUpdateFavoriteMovies({
updatedMovie: { id: movie.id, name: movie.title, posterId: movie.poster_path },
favoriteMoviesList: favoriteMoviesList,
moviesList: moviesList
});
this.updateIsFavorite(movie.id);
}
render() {
const { movie, youtubeKey, credits } = this.props;
if (!movie) {
return null;
}
const { adult, poster_path, budget, genres, homepage, imdb_id, original_language, original_title,
overview, popularity, production_companies, production_countries, release_date, revenue, runtime, spoken_languages,
status, tagline, title, video, vote_average, vote_count } = movie;
const genresText = genres.map(genre => genre.name).join(', ');
const countriesText = production_countries.map(country => country.name).join(', ');
const languagesText = spoken_languages.map(language => language.name).join(', ');
const featuresList = [
{ item: 'Release Date', value: release_date },
{ item: 'Budget', value: `$${this.numberWithCommas(budget)}` },
{ item: 'Revenue', value: `$${this.numberWithCommas(revenue)}` },
{ item: 'Length', value: `${runtime} minutes` },
{ item: 'Popularity', value: popularity },
{ item: 'Original Title', value: original_title },
{ item: 'For Adults', value: adult ? 'Yes' : 'No' },
{ item: 'Original Language', value: original_language },
{ item: 'Spoken Languages', value: languagesText },
{ item: 'Countries', value: countriesText },
{ item: 'Status', value: status },
{ item: 'Is Video', value: video ? 'Yes' : 'No' }
];
const linksList = [];
if (homepage) {
linksList.push({ id: 1, name: 'Homepage', url: homepage });
}
if (imdb_id) {
linksList.push({ id: 2, name: 'IMDB', url: `https://www.imdb.com/title/${imdb_id}` });
}
const actorsList = movieUtils.removeDuplicates(credits ? credits.cast ? credits.cast : null : null, 'name');
const crewsList = movieUtils.removeDuplicates(credits ? credits.crew ? credits.crew : null : null, 'name');
return (
<div>
<section className="details-area">
<PageTitle
pageName='details'
pageTitle='Details'
/>
<ul className="details-content">
<li className="details-left" style={{ backgroundImage: `url('https://image.tmdb.org/t/p/original${poster_path}')` }}></li>
<li className="details-right">
<h2>{title} ({release_date.substr(0, 4)})</h2>
<p className="genres">{genresText}</p>
<p className="description short">{tagline}</p>
<Rating
rating={vote_average}
votesCount={this.numberWithCommas(vote_count)}
/>
<p className="description full">{overview}</p>
<div className="extra">
<FeaturesList
featuresList={featuresList.slice(0, 5)}
linksList={null}
isFavorite={this.isFavorite}
onFavoriteMovieClick={this.handleFavoriteMovieClick}
/>
{youtubeKey && <Trailer
youtubeKey={youtubeKey}
/>}
</div>
</li>
<div className="extra-features">
<FeaturesList
featuresList={featuresList.slice(5, featuresList.length)}
linksList={linksList}
isFavorite={null}
onFavoriteMovieClick={null}
/>
<ProductionsList
productionsList={production_companies}
/>
</div>
</ul>
</section>
<section className="actors-area">
<PageTitle
pageName='actors'
pageTitle='Cast'
/>
<ActorsList
actorsList={actorsList}
/>
</section>
<section className="crew-area">
<PageTitle
pageName='crew'
pageTitle='Crew'
/>
<CrewsList
crewsList={crewsList}
/>
</section>
<ButtonClick
buttonText={'Back'}
buttonTitle={'Back'}
isLoading={false}
onClick={this.handleBackClick}
/>
</div>
);
}
}
Details.propTypes = propTypes;
Details.defaultProps = defaultProps;
const mapStateToProps = (state) => {
return {
movie: state.details.movie,
youtubeKey: state.details.youtubeKey,
credits: state.details.credits,
moviesList: state.main.moviesList,
favoriteMoviesList: state.main.favoriteMoviesList
};
};
const mapDispatchToProps = (dispatch) => {
return {
onLoadMovieDetails: (movieId) => dispatch(detailsActions.loadDetails(movieId)),
onUpdateFavoriteMovies: (request) => dispatch(mainActions.updateFavoriteMovies(request))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Details);
What I already looked in:
Getting The service worker navigation preload request was cancelled before 'preloadResponse' settled
https://learn.microsoft.com/en-us/answers/questions/108004/getting-the-service-worker-navigation-preload-requ.html
https://support.google.com/mail/thread/4055804?hl=en
https://love2dev.com/pwa/service-worker-preload/
I tried to put this on Details.jsx page, but it didn't work:
self.addEventListener('fetch', event => {
event.respondWith(async function () {
// Respond from the cache if we can
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse; // Else, use the preloaded response, if it's there
const response = await event.preloadResponse;
if (response) return response; // Else try the network.
return fetch(event.request);
}());
});
self.addEventListener('activate', event => {
event.waitUntil(async function () {
// Feature-detect
if (self.registration.navigationPreload) { // Enable navigation preloads!
console.log('Enable navigation preloads!');
await self.registration.navigationPreload.enable();
} return;
})();
});
How can I solve this issue? Thanks.
Had same error, even my iframe wasn't loading..whatever video you are using from youtube use nocookie/embed in url. It's working for me.
Try changing https://www.youtube.com/watch?v=i8eBBG46H8A to
https://www.youtube-nocookie.com/embed/i8eBBG46H8A
Hope nocookie & embed helps..!!

stop relay modern useLazyLoadQuery refetching in a pagination when data is available in cache?

i have this pagination in relay modern:
const CategoryContent = () => {
const { categoryQuery } = useRoute<CategoryContentScreenRouteProp>().params;
const { viewer } = useLazyLoadQuery<CategoryContentQuery>(
graphql`
query CategoryContentQuery(
$count: Int
$cursor: String
$category: String
) {
viewer {
...InfiniteCategories_viewer
#arguments(count: $count, cursor: $cursor, category: $category)
}
}
`,
{ count: 7, category: categoryQuery }
);
//console.log("CategoryContent viewer", viewer);
return (
<Suspense fallback={<LoadingView />}>
<View>
<Text>CategoryContent</Text>
</View>
<InfiniteCategories viewer={viewer} />
</Suspense>
);
};
and this is the infinite pagination:
const InfiniteCategories = ({
viewer,
}: {
viewer: InfiniteCategories_viewer$key;
}) => {
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
InfiniteCategoriesPaginationQuery,
any
>(
graphql`
fragment InfiniteCategories_viewer on Viewer
#argumentDefinitions(
count: { type: "Int", defaultValue: 7 }
cursor: { type: "String", defaultValue: null }
category: { type: "String" }
)
#refetchable(queryName: "InfiniteCategoriesPaginationQuery") {
merchants(first: $count, after: $cursor, category: $category)
#connection(key: "InfiniteCategories_viewer_merchants") {
pageInfo {
startCursor
endCursor
}
edges {
node {
id
category
logo
createdAt
isFavorite
pk
name
}
}
}
}
`,
viewer
);
console.log("data InfiniteCategories", data);
return (
<StyledFlatList
{...{
data:
data && data.merchants && data.merchants.edges
? data.merchants.edges
: [],
contentContainerStyle: styles.contentContainerStyle,
showsVerticalScrollIndicator: false,
keyExtractor: ({ cursor }) => cursor,
renderItem: ({ item }) => (
<View>
<Text>{item.node.name}</Text>
</View>
),
ListFooterComponent: () => {
if (isLoadingNext) return <ActivityIndicator />;
if (hasNext)
return (
<LoadMoreButton
onPress={() => {
loadNext(7);
}}
/>
);
return null;
},
}}
/>
);
};
however my problem is in every render like when i get back to the screen, it's being loaded again? how can i stop it from happening since it's pagination, user would have to do load more again to regain its' data, and i don't want that?

Relay cache and pagination causes error RelayConnectionHandler: Unexpected after cursor

Description
I have a container that has nested pagination on a file system. I recently added a caching layer to my relay environment to reduce the number of requests we are making. Opening a folder triggers a queryRender, that query fetches 8 edges at a time until the folder is fully loaded. Opening the folder on first fetch loads all the edges as expected. Closing the folder and opening it only loads the first 8 edges and fails on the second request.
I get this error: index.js:1 Warning: RelayConnectionHandler: Unexpected after cursor 'MTo3', edges must be fetched from the end of the list ('Mjow').
Adding { force: true } resolves the issue, but it causes a refetch of data that has already been requested and stored.
Alternative is to track the file count loaded and when a QueryRenderer is fired after the first load, i can pass in the loaded file count.
I'm just wondering if there is a cleaner way to achieve what I want? or does relay not support our schema the way I think it should?
Images
successful first load
First load gets 9 edges
failed second load
2nd load fails to paginate from the cache
Code
FileBrowser.js
Root query for the file browser
export default createPaginationContainer(
FileBrowser,
{
repository: graphql`
fragment FileBrowser_repository on Project {
files(
first: $first,
after: $cursor
) #connection(
key: "FileBrowser_files",
filters: [$cursor]
) #skip (if: $fileSkip) {
edges {
node {
... on ProjectFile {
id
namespace
repositoryName
key
modifiedAt
sizeBytes
downloadURL
}
... on ProjectDirectory {
id
key
}
}
}
pageInfo{
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
__typename
}`,
},
{
direction: 'forward',
getConnectionFromProps(props) {
return props.repository && props.repository.files;
},
getFragmentVariables(prevVars, first) {
return {
...prevVars,
first,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
const { namespace, repositoryName } = props;
return {
...fragmentVariables,
first: count,
cursor,
repositoryName,
namespace,
filter: [],
};
},
query: graphql`
query FileBrowserQuery(
$namespace: String!,
$repositoryName: String!,
$first: Int!,enter code here
$cursor: String,
$fileSkip: Boolean!
) {
repository(
namespace: $namespace,
repositoryName: $repositoryName
) {
...FileBrowser_repository
}
}`,
},
);
FolderQuery.js
Pagination container for folder
// #flow
// vendor
import React, { Component } from 'react';
import {
createPaginationContainer,
graphql,
} from 'react-relay';
// components
import Folder from './Folder';
// assets
import './Folder.scss';
const FolderPaginationContainer = createPaginationContainer(
Folder,
{
contents: graphql`
fragment Folder_contents on ProjectDirectory #relay(mask: false) {
id
key
contents(
first: $first,
after: $cursor,
) #connection(
key: "Folder_contents",
filters: []
) {
edges {
node {
... on ProjectFile {
id
namespace
repositoryName
key
modifiedAt
sizeBytes
}
... on ProjectDirectory {
id
key
}
}
cursor
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}`,
},
{
direction: 'forward',
getConnectionFromProps(props) {
return props.node && props.node.contents;
},
getFragmentVariables(prevVars, first) {
return {
...prevVars,
first,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
const { id } = props.node;
console.log(props, fragmentVariables);
return {
...fragmentVariables,
id,
first: count,
cursor,
filter: [],
};
},
query: graphql`
query FolderQuery(
$id: ID!,
$first: Int!,
$cursor: String,
) {
node(
id: $id,
) {
... on ProjectDirectory {
id
key
namespace
repositoryName
...Folder_contents
}
}
}`,
},
);
export default FolderPaginationContainer;
FolderSection.js
// #flow
// vendor
import React, { Component, Fragment } from 'react';
// components
import File from './File';
import RefetchFolder from './RefetchFolder';
type Props = {
namespace: string,
repositoryName: string,
sort: string,
reverse: bool,
index: number,
files: {
edges: Array<Object>,
},
query: Object,
__typename: string,
skipSort: boolean,
linkedDatasets: Object,
}
class FolderSection extends Component<Props> {
render() {
const {
namespace,
repositoryName,
sort,
reverse,
index,
files,
__typename,
query,
skipSort,
linkedDatasets,
} = this.props;
return (
<Fragment>
{ files.map((edge) => {
const {
key,
} = edge.node;
const isDir = (edge.node.__typename === `${__typename}Directory`);
const isFile = (edge.node.__typename === `${__typename}File`);
if (isDir) {
return (
<RefetchFolder
FolderSection={FolderSection}
name={key}
key={key}
edge={edge}
setState={this._setState}
rowStyle={{}}
sort={sort}
reverse={reverse}
rootFolder
index={index}
node={edge.node}
query={query}
__typename={__typename}
linkedDatasets={linkedDatasets}
namespace={namespace}
repositoryName={repositoryName}
/>
);
}
if (isFile) {
return (
<File
name={key}
key={key}
edge={edge}
isExpanded
index={index}
__typename={__typename}
/>
);
}
return (
<div>
Loading
</div>
);
})}
</Fragment>
);
}
}
export default FolderSection;
RefetchFolder.js
Contents of each folder is requested in a QueryRenderer
// #flow
// vendor
import React, { Component } from 'react';
import uuidv4 from 'uuid/v4';
import {
QueryRenderer,
graphql,
} from 'react-relay';
// environment
import environment from 'JS/createRelayEnvironment';
// component
import Folder from './FolderProjectWrapper';
type Props = {
node: {
id: String
},
relay: {
refetch: Function,
},
edge: {
node: Object
},
FolderSection: React.Node,
index: Number,
__typename: string,
sort: string,
linkedDatasets: Object,
reverse: boolean,
}
const RefetchFolderQuery = graphql`query RefetchFolderQuery(
$id: ID!,
$first: Int!,
$cursor: String,
) {
node(
id: $id,
) {
...Folder_contents #relay(mask: false)
}
}`;
class RefetchFolder extends Component<Props> {
state = {
isExpanded: false,
id: null,
progress: 0,
currentProgress: 0,
step: 0.01,
}
static getDerivedStateFromProps(props, state) {
const childNode = (props.node && props.node.contents)
? props.node
: props.edge.node;
return {
...state,
node: childNode,
};
}
/**
* #param {Object} evt
* sets item to expanded
* #return {}
*/
_expandFolder = () => {
this.setState((state) => {
const isExpanded = !state.isExpanded;
const id = (state.id === null)
? uuidv4()
: state.id;
return {
isExpanded,
id,
};
});
}
render() {
const {
FolderSection,
index,
sort,
reverse,
linkedDatasets,
} = this.props;
const { isExpanded, node, progress } = this.state;
const { __typename } = this.props;
if (!isExpanded) {
return (
<Folder
{...this.props}
FolderSection={FolderSection}
expandFolder={this._expandFolder}
node={node}
id={node.id}
index={index}
isExpanded={isExpanded}
__typename={__typename}
/>
);
}
return (
<QueryRenderer
query={RefetchFolderQuery}
environment={environment}
fetchPolicy="store-and-network"
variables={{
id: node.id,
first: 8,
cursor: null,
}}
render={({ props, error }) => {
if (props) {
const { hasNextPage } = props && props.node
&& props.node.contents && props.node.contents.pageInfo;
if (!hasNextPage) {
clearInterval(this.timeout);
}
return (
<Folder
FolderSection={FolderSection}
expandFolder={this._expandFolder}
node={props.node}
index={index}
isExpanded={isExpanded}
sort={sort}
reverse={reverse}
{...props}
__typename={__typename}
isLoading={hasNextPage}
progress={progress}
linkedDatasets={linkedDatasets}
/>
);
}
if (error) {
return (
<div>Error</div>
);
}
return (
<Folder
FolderSection={FolderSection}
expandFolder={this._expandFolder}
node={node}
index={index}
isExpanded={isExpanded}
{...this.props}
__typename={__typename}
isLoading
progress={progress}
linkedDatasets={linkedDatasets}
/>
);
}}
/>
);
}
}
export default RefetchFolder;
Folder.js
// vendor
import React, { Component } from 'react';
import classNames from 'classnames';
// ga
import ga from 'JS/ga';
// components
import LinkedDataset from './LinkedDataset';
// assets
import './Folder.scss';
type Props = {
namespace: string,
repositoryName: string,
index: number,
node: Object,
relay: {
refetchConnection: Function,
environment: Object,
loadMore: Function,
isLoading: Function,
},
expandFolder: Function,
isExpanded: Boolean,
FolderSection: React.node,
__typename: string,
}
class Folder extends Component<Props> {
componentDidMount() {
const { node } = this.props;
if (node.contents && node.contents.pageInfo && node.contents.pageInfo.hasNextPage) {
this._loadMore();
}
}
componentDidUpdate() {
const { node } = this.props;
if (node.contents && node.contents.pageInfo && node.contents.pageInfo.hasNextPage) {
this._loadMore();
}
}
/**
* #param {string} key
* #param {string || boolean} value - updates key value in state
* update state of component for a given key value pair
* #return {}
*/
_setState = (key, value) => {
this.setState({ [key]: value });
}
/**
* #param {}
* refetches component looking for new edges to insert at the top of the activity feed
* #return {}
*/
_loadMore = () => {
const { loadMore, isLoading } = this.props.relay;
if (!isLoading()) {
loadMore(
8,
() => {},
// { force: true } commented out because it causes the cache to be ignored, but
// fixes the issue
);
}
}
/**
* #param {Object} evt
* sets item to expanded
* #return {}
*/
_refetch = () => {
const { refetchConnection } = this.props.relay;
this.setState((state) => {
const isExpanded = !state.isExpanded;
return {
isExpanded,
};
});
refetchConnection(
8,
() => {
},
{
id: this.props.node.id,
},
);
}
render() {
const {
namespace,
repositoryName,
index,
node,
expandFolder,
isExpanded,
FolderSection,
__typename,
} = this.props;
const newIndex = index + 2;
const { key } = node;
const folderName = getName(key);
// declare css here
const folderRowCSS = classNames({
Folder__row: true,
});
const folderChildCSS = classNames({
Folder__child: true,
hidden: !isExpanded,
});
const folderNameCSS = classNames({
'Folder__cell Folder__cell--name': true,
'Folder__cell--open': isExpanded,
});
return (
<div className="Folder relative">
<div
className={folderRowCSS}
onClick={evt => expandFolder(evt)}
role="presentation"
>
<div className={folderNameCSS}>
<div className="Folder__icon" />
<div className="Folder__name">
{folderName}
</div>
</div>
<div className="Folder__cell Folder__cell--size" />
<div className="Folder__cell Folder__cell--date" />
</div>
<div className={folderChildCSS}>
<FolderSection
namespace={namespace}
repositoryName={repositoryName}
files={node.contents}
node={node}
index={newIndex}
linkedDatasets={linkedDatasets}
__typename={__typename}
/>
</div>
</div>
);
}
}
export default Folder;

Interaction with Apollo GraphQL Store not Working

I'm Trying to Learn GraphQL by Developing a Simple To-do List App Using React for the FrontEnd with Material-UI. I Need to Now Update the Information on the Web App in Real-time After the Query Gets Executed. I've Written the Code to Update the Store, But for Some Reason it Doesn't Work. This is the Code for App.js.
const TodosQuery = gql`{
todos {
id
text
complete
}
}`;
const UpdateMutation = gql`mutation($id: ID!, $complete: Boolean!) {
updateTodo(id: $id, complete: $complete)
}`;
const RemoveMutation = gql`mutation($id: ID!) {
removeTodo(id: $id)
}`;
const CreateMutation = gql`mutation($text: String!) {
createTodo(text: $text) {
id
text
complete
}
}`;
class App extends Component {
updateTodo = async todo => {
await this.props.updateTodo({
variables: {
id: todo.id,
complete: !todo.complete,
},
update: (store) => {
const data = store.readQuery({ query: TodosQuery });
data.todos = data.todos.map(existingTodo => existingTodo.id === todo.id ? {
...todo,
complete: !todo.complete,
} : existingTodo);
store.writeQuery({ query: TodosQuery, data })
}
});
};
removeTodo = async todo => {
await this.props.removeTodo({
variables: {
id: todo.id,
},
update: (store) => {
const data = store.readQuery({ query: TodosQuery });
data.todos = data.todos.filter(existingTodo => existingTodo.id !== todo.id);
store.writeQuery({ query: TodosQuery, data })
}
});
};
createTodo = async (text) => {
await this.props.createTodo({
variables: {
text,
},
update: (store, { data: { createTodo } }) => {
const data = store.readQuery({ query: TodosQuery });
data.todos.unshift(createTodo);
store.writeQuery({ query: TodosQuery, data })
},
});
}
render() {
const { data: { loading, error, todos } } = this.props;
if(loading) return <p>Loading...</p>;
if(error) return <p>Error...</p>;
return(
<div style={{ display: 'flex' }}>
<div style={{ margin: 'auto', width: 400 }}>
<Paper elevation={3}>
<Form submit={this.createTodo} />
<List>
{todos.map(todo =>
<ListItem key={todo.id} role={undefined} dense button onClick={() => this.updateTodo(todo)}>
<ListItemIcon>
<Checkbox checked={todo.complete} tabIndex={-1} disableRipple />
</ListItemIcon>
<ListItemText primary={todo.text} />
<ListItemSecondaryAction>
<IconButton onClick={() => this.removeTodo(todo)}>
<CloseIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
)}
</List>
</Paper>
</div>
</div>
);
}
}
export default compose(
graphql(CreateMutation, { name: 'createTodo' }),
graphql(UpdateMutation, { name: 'updateTodo' }),
graphql(RemoveMutation, { name: 'removeTodo' }),
graphql(TodosQuery)
)(App);
Also, i Want to Create Some List Items but that Doesn't Work Either. I'm Trying to get the Text Entered in the Input Field in Real-time Using a Handler Function handleOnKeyDown() in onKeyDown of the Input Field. I Pass in a event e as a Parameter to handleOnKeyDown(e) and when i console.log(e) it, instead of logging the Text Entered, it Returns a Weird Object that i Do Not Need. This is the Code that Handles Form Actions:
export default class Form extends React.Component{
state = {
text: '',
}
handleChange = (e) => {
const newText = e.target.value;
this.setState({
text: newText,
});
};
handleKeyDown = (e) => {
console.log(e);
if(e.key === 'enter') {
this.props.submit(this.state.text);
this.setState({ text: '' });
}
};
render() {
const { text } = this.state;
return (<TextField onChange={this.handleChange} onKeyDown={this.handleKeyDown} label="To-Do" margin='normal' value={text} fullWidth />);
}
}
This above Code File Gets Included in my App.js.
I Cannot Figure out the Issues. Please Help.
I was stuck with a similar problem. What resolved it for me was replacing the update with refetchQueries as:
updateTodo = async todo => {
await this.props.updateTodo({
variables: {
id: todo.id,
complete: !todo.complete
},
refetchQueries: [{
query: TodosQuery,
variables: {
id: todo.id,
complete: !todo.complete
}
}]
});
};
For your second problem, try capitalizing the 'e' in 'enter' as 'Enter'.
Hope this helps!

Resources