I'm kinda lost with this issue so any help would be appreciated! So here is the query that I would like to send:
query {
viewer{
search_places(lat: "40.7127", lng: "-74.0059", searchType:"all", searchTerm:"shopping"){
edges{
node{
id,
name,
lat,
lng
}
}
}
}
}
so far so good, this query is working when I try it on GraphiQL. This query returns a PlaceConnection object. Now I tried to implement the same on React Native with Relay:
class SearchPlacesRoute extends Route {
static paramDefinitions = {
lat : { required: true },
lng : { required: true },
searchType : { required: true },
searchTerm : { required: true }
}
static queries = {
search_places: () => Relay.QL`
query {
viewer{
search_places(lat: $lat, lng: $lng, searchType: $searchType, searchTerm: $searchTerm)
}
}
`
}
static routeName = 'SearchPlacesRoute'
}
class SearchPlacesComponent extends Component {
render () {
const places = this.props.search_places;
console.log(places);
for (var index = 0; index < places.length; index++) {
var element = places[index];
console.log(element);
}
return (
<View>
<Text>email: {places[0].name}</Text>
</View>
)
}
}
SearchPlacesComponent = Relay.createContainer(SearchPlacesComponent, {
fragments: {
search_places: () => Relay.QL`
fragment on PlaceConnection {
edges
{
node
{
id,
name,
lat,
lng
}
}
}
`
}
})
<RootContainer
Component={SearchPlacesComponent}
route={new SearchPlacesRoute({lat:"40.7127", lng: "-74.0059", searchType:"all",searchTerm: "shopping"})}
renderFetched={(data) => <SearchPlaces {...this.props} {...data} />}/>
but when I try to grab the data with this I get the following error:
1. Objects must have selections (field 'search_places' returns PlaceConnection but has no selections)
_search_places40zPNn:search_places(lat:"40.7127",lng:"-7
^^^
2. Fragment F0 on PlaceConnection can't be spread inside Viewer
...F0
^^^
So I examined what's actually sent to the server:
and it seems to me that the fragment is sent as a separate query instead of a selection. Any idea why is this happening or how can I avoid this behaviour?
EDIT:
here is my final code - hope it'll be helpful for some:
https://gist.github.com/adamivancza/586c42ff8b30cdf70a30153944d6ace9
I think the problem is that your Relay Route is too deep. Instead, define your route to just query against viewer:
class SearchPlacesRoute extends Route {
static queries = {
viewer: () => Relay.QL`
query {
viewer
}
`
}
static routeName = 'SearchPlacesRoute'
}
Then, inside of your component, define it as a fragment on viewer, instead of on PlaceConnection:
SearchPlacesComponent = Relay.createContainer(SearchPlacesComponent, {
initialVariables: { lat: "..." /* TODO */ },
fragments: {
viewer: () => Relay.QL`
fragment on viewer {
search_places(lat: $lat, lng: $lng, searchType: $searchType, searchTerm: $searchTerm) {
edges
{
node
{
id,
name,
lat,
lng
}
}
}
}
`
}
})
You'll need to move the variables ($lag, $lng, etc) to be defined as part of the container, instead of being defined as part of the route. But I think that should fix your problem.
Related
I'm making an SSR with GraphQL and Next.js but I have a problem when I try to map some data.
I'm bringing some data from Github GraphQL API (Followers, Following, Public Repos) and when I pass this data to the code all my data works fine. But when I try to map my Showcase Repos to print on screen, the problems starts, anyone can help me?
My stack at this moment is GraphQL (GraphQl-request), Next.js, styled-components and TypeScript.
This is my query:
fragment totalFollowing on User {
following(first: 1) {
totalCount
}
}
fragment totalFollowers on User {
followers(first: 1) {
totalCount
}
}
fragment totalRepositories on User {
repositories(affiliations: OWNER, first: 1) {
totalCount
}
}
fragment showcase on User {
itemShowcase {
items(first: 10) {
nodes {
... on Repository {
name
description
url
}
}
}
}
}
query GET_GITHUB_QUERIES {
viewer {
...totalFollowing
...totalFollowers
...totalRepositories
...showcase
}
}
and this is my types at moment:
export type FollowingProps = {
totalCount: number
}
export type FollowersProps = {
totalCount: number
}
export type RepositoriesProps = {
totalCount: number
}
export type itemShowCaseProps = {
itemShowcase: {
items: {
nodes: [
{
name: string
description: string
url: string
}
]
}
}
}
export type QueryProps = {
following: FollowingProps
followers: FollowersProps
repositories: RepositoriesProps
itemShowcase: itemShowCaseProps
}
My call on index.tsx
export const getStaticProps: GetStaticProps = async () => {
const { viewer } = await client.request(query)
return {
props: {
...viewer
}
}
}
Still on my index.tsx, i have this component where i need to pass the data:
<S.MyWorkSection id="my-work" data-aos="fade-up">
<MyWorkGithub
repositories={repositories}
following={following}
followers={followers}
{...{ itemShowcase }}
/>
</S.MyWorkSection>
And finaly this is my component:
const MyWorkGithub: React.FC<QueryProps> = ({
following,
followers,
repositories,
itemShowcase
}) => (
<>
<Title text="Works" icon={true} />
<S.HighLightWrapper>
<HighLightCard
hlNumber={repositories.totalCount}
hlDescription={'repositories'}
/>
<HighLightCard
hlNumber={followers.totalCount}
hlDescription={'followers'}
/>
<HighLightCard
hlNumber={following.totalCount}
hlDescription={'following'}
/>
</S.HighLightWrapper>
<S.CardWrapper>
{itemShowcase.itemShowcase.items.nodes.map((repo, index) => (
<CardComponent
key={index}
url={repo.url}
name={repo.name}
description={repo.description}
/>
))}
</S.CardWrapper>
</>
)
this is my error:
TypeError: cannot read property 'items' of undefined
<SCardWrapper>
{itemShowcase.itemShowcase.items.node.map((repo, index) => (
...
https://i.imgur.com/LzOmb2z.png
Other datas like repositories, followers and following still works fine.
Edit: When I put on Github Explorer, all data is fine.
I am trying to generate pages of each of the category's data from contentful ...
but there have some concerns.
Success
I could have got the data from contentful ..such as (title, image, and so on ) on the home page(localhost:8000)
Fail
when I clicked the read more button, Card on (localhost:8000), and switched into another page (localhost:8000/blog/tech/(content that I created on contentful)) that has not succeeded and 404pages has appeared(the only slag has changed and appeared that I hoped to).
Error
warn The GraphQL query in the non-page component "C:/Users/taiga/Github/Gatsby-new-development-blog/my-blog/src/templates/blog.js" will not
Exported queries are only executed for Page components. It's possible you're
trying to create pages in your gatsby-node.js and that's failing for some
reason.
If the failing component(s) is a regular component and not intended to be a page
component, you generally want to use a <StaticQuery> (https://gatsbyjs.org/docs/static-query)
instead of exporting a page query.
my-repo
enter link description here
gatsby.node.js
const path = require(`path`);
const makeRequest = (graphql, request) => new Promise((resolve, reject) => {
// Query for nodes to use in creating pages.
resolve(
graphql(request).then(result => {
if (result.errors) {
reject(result.errors)
}
return result;
})
)
});
// Implement the Gatsby API "createPages". This is called once the
// data layer is bootstrapped to let plugins create pages from data.
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
// Create pages for each blog.
const getBlog = makeRequest(graphql, `
{
allContentfulBlog (
sort: { fields: [createdAt], order: DESC }
)
edges {
node {
id
slug
}
}
}
}
`).then(result => {
result.data.allContentfulBlog.edges.forEach(({ node }) => {
createPage({
path: `blog/${node.slug}`,
component: path.resolve(`src/templates/blog.js`),
context: {
id: node.id,
},
})
})
});
const getTech = makeRequest(graphql,`
{
allContentfulBlog (
sort: { fields: [createdAt], order: DESC }
filter: {
categories: {elemMatch: {category: {eq: "tech"}}}
},)
{
edges {
node {
id
slug
}
}
}
}
`).then(result => {
const blogs = result.data.allContentfulBlog.edges
const blogsPerPage = 9
const numPages = Math.ceil(blogs.length / blogsPerPage)
Array.from({ length: numPages }).forEach((_, i) => {
createPage({
path: i === 0 ? `/category/tech` : `/category/tech/${i + 1}`,
component: path.resolve("./src/templates/tech.js"),
context: {
limit: blogsPerPage,
skip: i * blogsPerPage,
numPages,
currentPage: i + 1
},
})
})
});
return Promise.all([
getBlog,
getTech
])
};
My component works fine and I can see it is working properly with the maps being formed in UI and markers coming up properly, however I am still get this error for the challenge :
Challenge Not yet complete... here's what's wrong:
We can't find the correct settings for the createMapMarkers() function in the component boatsNearMe JavaScript file. Make sure the component was created according to the requirements, including the right mapMarkers, title, Latitude (Geolocation__Latitude__s), Longitude (Geolocation__Longitude__s), the correct constants, stopping the loading spinner, and using the proper case-sensitivity and consistent quotation.
This is the Challenge 7 Statement as per the trailhead:
"Build the component boatsNearMe and display boats near you
Create the component boatsNearMe and show boats that are near the user, using the browser location and boat type. Display them on a map, always with the consent of the user (and only while the page is open)."
https://trailhead.salesforce.com/en/content/learn/superbadges/superbadge_lwc_specialist
Here is my boatsNearMe LWC code
import { LightningElement, wire, api } from 'lwc';
import getBoatsByLocation from '#salesforce/apex/BoatDataService.getBoatsByLocation';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
const LABEL_YOU_ARE_HERE = 'You are here!';
const ICON_STANDARD_USER = 'standard:user';
const ERROR_TITLE = 'Error loading Boats Near Me';
const ERROR_VARIANT = 'error';
export default class BoatsNearMe extends LightningElement {
#api boatTypeId;
mapMarkers = [];
isLoading = true;
isRendered = false;
latitude;
longitude;
#wire(getBoatsByLocation, { latitude: '$latitude', longitude: '$longitude', boatTypeId: '$boatTypeId' })
wiredBoatsJSON({ error, data }) {
if (data) {
this.isLoading = true;
this.createMapMarkers(JSON.parse(data));
} else if (error) {
this.dispatchEvent(
new ShowToastEvent({
title: ERROR_TITLE,
message: error.body.message,
variant: ERROR_VARIANT
})
);
// this.isLoading = false;
}
}
getLocationFromBrowser() {
navigator.geolocation.getCurrentPosition(
position => {
this.latitude = position.coords.latitude;
this.longitude = position.coords.longitude;
},
(error) => {}
);
}
createMapMarkers(boatData) {
const newMarkers = boatData.map((boat) => {
return {
location: {
Latitude: boat.Geolocation__Latitude__s,
Longitude: boat.Geolocation__Latitude__s
},
title: boat.Name,
};
});
newMarkers.unshift({
location: {
Latitude: this.latitude,
Longitude: this.longitude
},
title: LABEL_YOU_ARE_HERE,
icon: ICON_STANDARD_USER
});
this.mapMarkers = newMarkers;
this.isLoading = false;
}
renderedCallback() {
if (this.isRendered == false) {
this.getLocationFromBrowser();
}
this.isRendered = true;
}
}
I think you have Geolocation__Latitude__s twice in this part:
createMapMarkers(boatData) {
const newMarkers = boatData.map((boat) => {
return {
location: {
Latitude: boat.Geolocation__Latitude__s,
Longitude: boat.Geolocation__Latitude__s // <<-- should be Longitude
},
title: boat.Name,
};
});
For frontend I am using React + Relay. I have some connection at the backend that could be queried like:
query Query {
node(id: 123456) {
teams(first: 10) {
node {
id
name
}
page_info {
start_cursor
end_cursor
}
}
}
}
So for the traditional approach, I can use skip PAGE_SIZE * curr limit PAGE_SIZE to query for next page, prev page and first page and last page (In fact I can query for random page)
But how should I implement the frontend to make these requests elegantly?
#Junchao, what Vincent said is correct. Also, you must have a re-fetch query and send refetchVariables with your first value updated. I will try to provide you an example:
export default createRefetchContainer(
TeamsComponent,
{
query: graphql`
fragment TeamsComponent_query on Query
#argumentDefinitions(
first: { type: Int }
last: { type: Int }
before: { type: String }
after: { type: String }
) {
teams(
id: { type: "ID!" }
first: { type: Int }
last: { type: Int }
before: { type: String }
after: { type: String }
) #connection(key: "TeamsComponent_teams", filters: []) {
count
pageInfo {
endCursor
hasNextPage
}
edges {
node {
id
name
}
}
}
}
`,
},
graphql`
query TeamsComponent(
$after: String
$before: String
$first: Int
$last: Int
) {
...TeamsComponent_query
#arguments(
first: $first
last: $last
after: $after
before: $before
)
}
`,
);
I tried to build an example based on your code. This is basically the idea. The bottom query is the re-fetch one. Alongside with that, you must trigger this re-fetch somehow by calling this.props.relay.refetch passing your renderVaribles. Take a deep looker into the docs about this.
Hope is helps :)
UPDATE:
Just to add something, you could have a handleLoadMore function with something like this:
handleLoadMore = () => {
const { relay, connection } = this.props;
const { isFetching } = this.state;
if (!connection) return;
const { edges, pageInfo } = connection;
if (!pageInfo.hasNextPage) return;
const total = edges.length + TOTAL_REFETCH_ITEMS;
const fragmentRenderVariables = this.getRenderVariables() || {};
const renderVariables = { first: total, ...fragmentRenderVariables };
if (isFetching) {
// do not loadMore if it is still loading more or searching
return;
}
this.setState({
isFetching: true,
});
const refetchVariables = fragmentVariables => ({
first: TOTAL_REFETCH_ITEMS,
after: pageInfo.endCursor,
});
relay.refetch(
refetchVariables,
null,
() => {
this.setState({ isFetching: false });
},
{
force: false,
},
);
};
UPDATE 2:
For going backwards, you could have something like:
loadPageBackwardsVars = () => {
const { connection, quantityPerPage } = this.props;
const { quantity } = getFormatedQuery(location);
const { endCursorOffset, startCursorOffset } = connection;
const firstItem = connection.edges.slice(startCursorOffset, endCursorOffset)[0].cursor;
const refetchVariables = fragmentVariables => ({
...fragmentVariables,
...this.getFragmentVariables(),
last: parseInt(quantity || quantityPerPage, 10) || 10,
first: null,
before: firstItem,
});
return refetchVariables;
};
I'm messing with ag-grid, react-apollo, and everything seems to be working fine. The goal here is to click a check box and have a mutation / network request occur modifying some data. The issue i'm having is that it redraws the entire row which can be really slow but im really just trying to update the cell itself so its quick and the user experience is better. One thought i had was to do a optimistic update and just update my cache / utilize my cache. What are some approach you guys have taken.
Both the columns and row data are grabbed via a apollo query.
Heres some code:
CheckboxRenderer
import React, { Component } from "react";
import Checkbox from "#material-ui/core/Checkbox";
import _ from "lodash";
class CheckboxItem extends Component {
constructor(props) {
super(props);
this.state = {
value: false
};
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
}
componentDidMount() {
this.setDefaultState();
}
setDefaultState() {
const { data, colDef, api } = this.props;
const { externalData } = api;
if (externalData && externalData.length > 0) {
if (_.find(data.roles, _.matchesProperty("name", colDef.headerName))) {
this.setState({
value: true
});
}
}
}
updateGridAssociation(checked) {
const { data, colDef } = this.props;
// const { externalData, entitySpec, fieldSpec } = this.props.api;
// console.log(data);
// console.log(colDef);
if (checked) {
this.props.api.assign(data.id, colDef.id);
return;
}
this.props.api.unassign(data.id, colDef.id);
return;
}
handleCheckboxChange(event) {
const checked = !this.state.value;
this.updateGridAssociation(checked);
this.setState({ value: checked });
}
render() {
return (
<Checkbox
checked={this.state.value}
onChange={this.handleCheckboxChange}
/>
);
}
}
export default CheckboxItem;
Grid itself:
import React, { Component } from "react";
import { graphql, compose } from "react-apollo";
import gql from "graphql-tag";
import Grid from "#material-ui/core/Grid";
import _ from "lodash";
import { AgGridReact } from "ag-grid-react";
import { CheckboxItem } from "../Grid";
import "ag-grid/dist/styles/ag-grid.css";
import "ag-grid/dist/styles/ag-theme-material.css";
class UserRole extends Component {
constructor(props) {
super(props);
this.api = null;
}
generateColumns = roles => {
const columns = [];
const initialColumn = {
headerName: "User Email",
editable: false,
field: "email"
};
columns.push(initialColumn);
_.forEach(roles, role => {
const roleColumn = {
headerName: role.name,
editable: false,
cellRendererFramework: CheckboxItem,
id: role.id,
suppressMenu: true,
suppressSorting: true
};
columns.push(roleColumn);
});
if (this.api.setColumnDefs && roles) {
this.api.setColumnDefs(columns);
}
return columns;
};
onGridReady = params => {
this.api = params.api;
this.columnApi = params.columnApi;
this.api.assign = (userId, roleId) => {
this.props.assignRole({
variables: { userId, roleId },
refetchQueries: () => ["allUserRoles", "isAuthenticated"]
});
};
this.api.unassign = (userId, roleId) => {
this.props.unassignRole({
variables: { userId, roleId },
refetchQueries: () => ["allUserRoles", "isAuthenticated"]
});
};
params.api.sizeColumnsToFit();
};
onGridSizeChanged = params => {
const gridWidth = document.getElementById("grid-wrapper").offsetWidth;
const columnsToShow = [];
const columnsToHide = [];
let totalColsWidth = 0;
const allColumns = params.columnApi.getAllColumns();
for (let i = 0; i < allColumns.length; i++) {
const column = allColumns[i];
totalColsWidth += column.getMinWidth();
if (totalColsWidth > gridWidth) {
columnsToHide.push(column.colId);
} else {
columnsToShow.push(column.colId);
}
}
params.columnApi.setColumnsVisible(columnsToShow, true);
params.columnApi.setColumnsVisible(columnsToHide, false);
params.api.sizeColumnsToFit();
};
onCellValueChanged = params => {};
render() {
console.log(this.props);
const { users, roles } = this.props.userRoles;
if (this.api) {
this.api.setColumnDefs(this.generateColumns(roles));
this.api.sizeColumnsToFit();
this.api.externalData = roles;
this.api.setRowData(_.cloneDeep(users));
}
return (
<Grid
item
xs={12}
sm={12}
className="ag-theme-material"
style={{
height: "80vh",
width: "100vh"
}}
>
<AgGridReact
onGridReady={this.onGridReady}
onGridSizeChanged={this.onGridSizeChanged}
columnDefs={[]}
enableSorting
pagination
paginationAutoPageSize
enableFilter
enableCellChangeFlash
rowData={_.cloneDeep(users)}
deltaRowDataMode={true}
getRowNodeId={data => data.id}
onCellValueChanged={this.onCellValueChanged}
/>
</Grid>
);
}
}
const userRolesQuery = gql`
query allUserRoles {
users {
id
email
roles {
id
name
}
}
roles {
id
name
}
}
`;
const unassignRole = gql`
mutation($userId: String!, $roleId: String!) {
unassignUserRole(userId: $userId, roleId: $roleId) {
id
email
roles {
id
name
}
}
}
`;
const assignRole = gql`
mutation($userId: String!, $roleId: String!) {
assignUserRole(userId: $userId, roleId: $roleId) {
id
email
roles {
id
name
}
}
}
`;
export default compose(
graphql(userRolesQuery, {
name: "userRoles",
options: { fetchPolicy: "cache-and-network" }
}),
graphql(unassignRole, {
name: "unassignRole"
}),
graphql(assignRole, {
name: "assignRole"
})
)(UserRole);
I don't know ag-grid but ... in this case making requests results in entire grid (UserRole component) redraw.
This is normal when you pass actions (to childs) affecting entire parent state (new data arrived in props => redraw).
You can avoid this by shouldComponentUpdate() - f.e. redraw only if rows amount changes.
But there is another problem - you're making optimistic changes (change checkbox state) - what if mutation fails? You have to handle apollo error and force redraw of entire grid - change was local (cell). This can be done f.e. by setting flag (using setState) and additional condition in shouldComponentUpdate.
The best way for me to deal with this was to do a shouldComponentUpdate with network statuses in apollo, which took some digging around to see what was happening:
/**
* Important to understand that we use network statuses given to us by apollo to take over, if either are 4 (refetch) we hack around it by not updating
* IF the statuses are also equal it indicates some sort of refetching is trying to take place
* #param {obj} nextProps [Next props passed into react lifecycle]
* #return {[boolean]} [true if should update, else its false to not]
*/
shouldComponentUpdate = nextProps => {
const prevNetStatus = this.props.userRoles.networkStatus;
const netStatus = nextProps.userRoles.networkStatus;
const error = nextProps.userRoles.networkStatus === 8;
if (error) {
return true;
}
return (
prevNetStatus !== netStatus && prevNetStatus !== 4 && netStatus !== 4
);
};
It basically says if there is a error, just rerender to be accurate (and i think this ok assuming that errors wont happen much but you never know) then I check to see if any of the network statuses are not 4 (refetch) if they are I dont want a rerender, let me do what I want without react interfering at that level. (Like updating a child component).
prevNetStatus !== netStatus
This part of the code is just saying I want the initial load only to cause a UI update. I believe it works from loading -> success as a network status and then if you refetch from success -> refetch -> success or something of that nature.
Essentially I just looked in my props for the query and saw what I could work with.