React, Apollo - Update & Remove Mutation Issues - reactjs

I am currently working and study on "CRUD" mutation with GraphQL. Using Apollo Client and React to control mutation at front-end. Using Express/mongoose as back-end to run server and fetch/store data from local database.
Working/Test with GraphQL (Middle/Back-end). I am able to fetch and mutate (create, update, and remove) data from local server (mongodb) through localhost:4000/graphiql tool.
Here's mutation schema...
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
addUser: {
...
},
updateUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLString) },
name: { type: new GraphQLNonNull(GraphQLString) },
age: { type: new GraphQLNonNull(GraphQLInt) }
},
resolve(parent, args) {
let updateIser = User.findByIdAndUpdate(
args.id,
{
$set: { name: args.name, age: args.age }
},
{ new: true }
).catch(err => new Error(err));
return updateUser;
}
},
deleteUser: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLString) } },
resolve(parent, args) {
let removeUser = User.findByIdAndRemove(args.id).exec();
if (!removeUser) {
throw new Error('Error, Cannot Delete!');
}
return removeUser;
}
}
}
});
Now on the the Front-End part using React and Apollo Client, fetching data and rendering iteration map was a success!
I also use react-router for page transition, When clicking on Add button from user list, it will bring me to form page to fill in user name and age. After complete, it will Submit and redirect (react-router redirect method) back to list page with a newly created user into list (refetch). This process was also a success for me!
On the Update (Edit) or Remove (Delete) part, It was unfortunate. I am facing several issues...Unable to update (Edit) user
I am guessing... It appear having something to do with not be able to fetch specific ID after clicking on Edit Button, or could it be some conflicting issue between fetching and react-router? As much as I have tried to find solutions, I ran out of ideas. So I was hoping I could use some help from you to solve on what I am facing.
If this helps... Here's React-Apollo syntax called List.js - This display the list of users including Edit and Remove button (unsuccessful attempts)
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import { Link } from 'react-router-dom';
import { getUsersQuery } from '../queries/queries';
class UserList extends Component {
state = { selected: null };
displayUser = () => {
let data = this.props.data;
if (data.loading) {
return <div>Loading...</div>;
} else {
return data.examples.map(user => (
<li
key={user.id}
onClick={e => { this.setState({ selected: user.id });}}
>
{user.name} {user.age}
<div className="choices">
<Link
to="/updateuser"
onClick={e => {
this.setState({ selected: user.id });
}}
>
<button>Edit</button>
</Link>
<Link to="/removeuser">
<button>Remove</button>
</Link>
</div>
</li>
));
}
};
render() {
console.log(this.state.selected);
return (
<div id="listContent">
<h3>List</h3>
<ul id="userList"> {this.displayUser()}</ul>
<Link to="/adduser">
<button>+</button>
</Link>
<div>{this.state.selected}</div>
</div>
);
}
}
export default graphql(getUsersQuery)(UserList);
The UpdateForm.js - A syntax that render update form. But couldn't succeed mutate it. possibility fetching issues?
import React, { Component } from 'react';
import { graphql, compose } from 'react-apollo';
import { Redirect, Link } from 'react-router-dom';
import { getUsersQuery, updateUserMutation } from '../queries/queries';
class UpdateUserForm extends Component {
state = {
name: '',
age: '',
redirect: false
};
onChangeName = e => {
this.setState({ name: e.target.value });
};
onChangeAge = e => {
this.setState({ age: e.target.value });
};
onSubmitForm = e => {
e.preventDefault();
//console.log(this.state);
this.props.updateUserMutation({
variables: {
id: this.state.id,
name: this.state.name,
age: this.state.age
},
refetchQueries: [ { query: getUsersQuery } ]
});
this.setState({ redirect: true });
};
render() {
const user = this.props.getUsersQuery;
if (this.state.redirect) {
return <Redirect to="/" />;
}
return (
<div id="formContainer">
<form id="form" onSubmit={this.onSubmitForm}>
<div className="field">
<label>Name:</label>
<input
type="text"
onChange={this.onChangeName}
placeholder={user.name}
/>
</div>
<div className="field">
<label>Age:</label>
<input
type="text"
onChange={this.onChangeAge}
placeholder={user.age}
/>
</div>
<button>Update</button>
<Link to="/">Cancel</Link>
</form>
</div>
);
}
}
export default compose(
graphql(getUsersQuery, { name: 'getUsersQuery' }),
graphql(updateUserMutation, { name: 'updateUserMutation' })
)(UpdateUserForm);
Not sure about remove page -- Hoping just simply hit remove button, from list page will fetch specific ID and mutate (remove) user.
Here's the Apollo Client query code called queries.js.
// Fetch Users
const getUsersQuery = gql`
{
users {
name
age
id
}
}
`;
// Update User Mutation
const updateUserMutation = gql`
mutation($id: ID!, $name: String!, $age: Int!) {
updateUser(id: $id, name: $name, age: $age) {
id
name
age
}
}
`;
// Remove User Mutation
const removeUserMutation = gql`
mutation($id: ID!) {
deleteUser(id: $id) {
id
}
}
`;
Thanks in advance...

Related

How to submit a react form with chakra react select (v4), react hook form and prisma

I am trying to figure out how to make a form in a react, apollo, prisma app, using chakra-react-select.
This is my attempt:
import * as React from "react"
import { Box, Center, Heading, Button } from "#chakra-ui/react"
import { Select } from "chakra-react-select"
import { gql } from "#apollo/client"
import Head from "next/head"
import { IssueInput, useAllIssuesQuery, useCreateIssueMutation } from "lib/graphql"
import { useRouter } from "next/router"
import { REDIRECT_PATH } from "lib/config"
import * as c from "#chakra-ui/react"
import { Input } from "components/Input"
import { HomeLayout } from "components/HomeLayout"
import { Limiter } from "components/Limiter"
import { Form } from "components/Form"
import { withAuth } from "components/hoc/withAuth"
import Yup from "lib/yup"
import { useForm } from "lib/hooks/useForm"
import { useMe } from "lib/hooks/useMe"
import { useToast } from "lib/hooks/useToast"
import { useController } from "react-hook-form"
const _ = gql`
mutation CreateIssue($data: IssueInput!) {
createIssue(data: $data) {
id
title
issueCategory
description
}
# userId
}
query AllIssues {
allIssues {
id
title
# issueId
description
issueCategory
userId
}
}
`
const IssueSchema = Yup.object().shape({
title: Yup.string().required("Title is required"),
issueCategory: Yup.object().required(),
description: Yup.string().required("Description is required"),
})
function Issue() {
const toast = useToast()
const { me, loading: meLoading } = useMe()
const [createIssue] = useCreateIssueMutation()
const { data: issues, refetch } = useAllIssuesQuery()
const router = useRouter()
const redirect = router.query[REDIRECT_PATH] as string | undefined
const form = useForm({ schema: IssueSchema })
const {
field: { onChange, onBlur, value, ref },
fieldState: { error },
} = useController({
name: "issueCategory",
control: form.control,
rules: { required: "Issue category is required" },
})
const onSubmit = (data: IssueInput) => {
console.log("hello onsubmit", data) /// I can see this in my console when I try to submit this form but instead of storing the data it says undefined
return form.handler(
() =>
createIssue({
variables: {
data: {
...data,
// #ts-ignore
issueCategory: data.issueCategory.value,
userId: me?.id || "",
},
},
}),
{
onSuccess: async (data) => {
console.log(data)
await fetch("api/issue", { method: "post" }),
toast({
title: "Issue created",
description: "Your issue has been created",
status: "success",
})
refetch()
form.reset()
router.replace(redirect || "/")
},
},
)
}
if (meLoading)
return (
<c.Center>
<c.Spinner />
</c.Center>
)
if (!me) return null
console.log("select error", error)
return (
<Box>
<Head>
<title>Create</title>
</Head>
<Limiter pt={20} minH="calc(100vh - 65px)">
<Center flexDir="column">
<Heading
as="h1"
size="3xl"
>
Create Issue
</Heading>
<Form onSubmit={onSubmit} {...form}>
<c.Stack spacing={2}>
<c.Heading>Issues</c.Heading>
<Input autoFocus name="title" label="Title" placeholder="Eg: change" />
<Input
name="description"
label="Description"
placeholder="Eg: change"
/>
<Select
options={[
{
label: "I can't be removed",
value: "fixed",
isFixed: true,
},
{
label: "I can be removed",
value: "not-fixed",
},
]}
placeholder="Select issue category"
closeMenuOnSelect={true}
onChange={onChange}
onBlur={onBlur}
value={value}
ref={ref}
/>
<p>{error?.message}</p>
<Button
color="brand.orange"
type="submit"
// isFullWidth
isDisabled={form.formState.isSubmitting || !form.formState.isDirty}
isLoading={form.formState.isSubmitting}
>
Create Issue
</Button>
<c.List>
{issues?.allIssues.map((issue) => (
<c.ListItem key={issue.id}>
{issue.title}
{/* {issue.issueCategory} */} - if I uncomment this line, then i get an error as described below
{issue.description}
</c.ListItem>
))}
</c.List>
</c.Stack>
</Form>
</Center>
</Limiter>
</Box>
)
}
Issue.getLayout = (page: React.ReactNode) => <HomeLayout>{page}</HomeLayout>
export default withAuth(Issue)
I can't understand why my terminal is giving me an error message that says that the property named issueCategory does not exist on type Issue. I can see that a type has been generated for IssueCategory:
export type Issue = {
__typename?: 'Issue';
createdAt: Scalars['DateTime'];
description: Scalars['String'];
id: Scalars['String'];
issueCategory: Category;
title: Scalars['String'];
updatedAt: Scalars['DateTime'];
userId: Scalars['String'];
};
My prisma schema also has an attribute for issueCategory, as follows:
model Issue {
id String #id #default(dbgenerated("gen_random_uuid()")) #db.Uuid
title String
description String
issueCategory Category
// Template Template[]
createdAt DateTime #default(now()) #db.Timestamptz(6)
updatedAt DateTime #default(now()) #updatedAt #db.Timestamptz(6)
User User[]
userId String #db.Uuid
}
Category is also defined in graphql.tsx as:
export enum Category {
Future = 'FUTURE',
Natural = 'NATURAL',
Social = 'SOCIAL'
}
Apart from this strange error message, I don't have any other errors in my terminal. I don't get any information about the error in my console either - it just says 'select error undefined'. I can see in the entry above the error line, that the json packet includes the selection i made when the form loads.
What is the step required to submit this form? Currently my console logs that the data submitted is undefined.

Why does my child component render multiple times?

I am using Stripe API on my project and when the payment is successful I want to add the order placed to my collection. When I run the below code it adds the order multiple times and there is no specific pattern. It also adds multiple orders to the collection:
Success.js:
import React from "react";
import AddOrder from "../orders/AddOrder";
import { useNavigate, useParams } from "react-router-dom";
import { useMutation, useQuery } from "#apollo/client";
import queries from "../../queries";
import { Alert, Button } from "react-bootstrap";
function Success() {
const logo = require("../../assets/delivery-package.gif");
const navigate = useNavigate();
const { secret } = useParams();
const { loading, error, data } = useQuery(queries.GET_SESSION, { variables: { id: secret } });
const [deleteSession] = useMutation(queries.DELETE_SESSION);
if (loading) {
return <div>Loading...</div>;
} else if (error) {
navigate("/notfound");
} else if (data.session) {
deleteSession({
variables: {
id: secret,
},
});
return (
<div>
<AddOrder />
<Alert variant="success" style={{ fontSize: "25px" }}>
Order Placed Successfully
</Alert>
<img alt="order success" id="logo" src={logo} style={{ width: "70%", height: "70%", marginTop: "30px" }} />
<br />
<br />
<Button onClick={() => navigate("/")}>Home</Button>
</div>
);
} else {
navigate("/notfound");
}
}
export default Success;
AddOrder:
import { useMutation, useQuery } from "#apollo/client";
import { AuthContext } from "../../Firebase/Auth";
import queries from "../../queries";
import { useContext, useEffect } from "react";
import { reactLocalStorage } from "reactjs-localstorage";
let add = reactLocalStorage.getObject("addressDetails");
function AddOrder() {
const d = new Date();
let text = d.toString();
const [addOrder] = useMutation(queries.ADD_ORDER);
const [editUser] = useMutation(queries.EDIT_USER_CART);
const { currentUser } = useContext(AuthContext);
const { data } = useQuery(queries.GET_USER_BY_ID, {
fetchPolicy: "network-only",
variables: {
id: currentUser.uid,
},
});
const getUserOrders = useQuery(queries.GET_USER_ORDERS, {
fetchPolicy: "network-only",
variables: {
userId: currentUser.uid,
},
});
useEffect(() => {
if (data && getUserOrders.data && currentUser && data.getUser.cart.length > 0) {
let newCart = [];
let total = 0;
for (let i = 0; i < data.getUser.cart.length; i++) {
total += data.getUser.cart[i].price * data.getUser.cart[i].quantity;
newCart.push({
orderedQuantity: data.getUser.cart[i].quantity,
_id: data.getUser.cart[i]._id,
name: data.getUser.cart[i].name,
image: data.getUser.cart[i].image,
price: data.getUser.cart[i].price,
});
}
addOrder({
variables: {
userId: currentUser.uid,
userEmail: currentUser.email,
status: "ordered",
createdAt: text,
products: newCart,
total: total,
flag: getUserOrders.data.userOrders.length + 1,
zip: add.zip.val ? add.zip.val : add.zip,
state: add.state.val ? add.state.val : add.state,
city: add.city.val ? add.city.val : add.city,
apt: add.apt.val ? add.apt.val : add.apt,
addressStreet: add.addressStreet.val ? add.addressStreet.val : add.addressStreet,
},
});
editUser({
variables: {
id: currentUser.uid,
cart: [],
},
});
}
}, [addOrder, currentUser, data, editUser, getUserOrders, text]);
}
export default AddOrder;
There is only one return so don't know if the issue is with the success page or the Addorder function
Possible reason:
Your component must be being rendered by some parent component, for example, by calls to setState, and when the component is re-rendered the Success child component (which in turn does not return a JSX.Element) is also re-rendered
Cause:
Inside your AddOrder component there is a useEffect that serves to prevent re-renders when the passed parameters were not changed, and among them it has a "new Date().getTime()", I believe this is one of the main reasons why useEffect is always returning true, and requesting a re-render. Not only that, but also:
new Date().toString as parameter to useEffect
Passing functions (addOrder, editOrder) to useEffect params that are likely to always be new functions when reredendering.
currentUser is probably a new object every render, luckily you only need the userid, which in turn is a primitive type, and will always be the same regardless of re-rendering.
Solution:
There is no need to analyze if the addOrder or editOrder function has changed, as they are just functions, and do not add any specific "value", for example, if your intention is to rederize (to be able to add) only when the items change, you can just leave the getUserOrders.data and data (useQuery(queries.GET_USER_BY_ID)...)
New code:
import { useMutation, useQuery } from "#apollo/client";
import { AuthContext } from "../../Firebase/Auth";
import queries from "../../queries";
import { useContext, useEffect } from "react";
import { reactLocalStorage } from "reactjs-localstorage";
let add = reactLocalStorage.getObject("addressDetails");
function AddOrder() {
const d = new Date();
let text = d.toString();
const [addOrder] = useMutation(queries.ADD_ORDER);
const [editUser] = useMutation(queries.EDIT_USER_CART);
const { currentUser } = useContext(AuthContext);
const { data } = useQuery(queries.GET_USER_BY_ID, {
fetchPolicy: "network-only",
variables: {
id: currentUser.uid,
},
});
const getUserOrders = useQuery(queries.GET_USER_ORDERS, {
fetchPolicy: "network-only",
variables: {
userId: currentUser.uid,
},
});
useEffect(() => {
if (data && getUserOrders.data && currentUser && data.getUser.cart.length > 0) {
let newCart = [];
let total = 0;
for (let i = 0; i < data.getUser.cart.length; i++) {
total += data.getUser.cart[i].price * data.getUser.cart[i].quantity;
newCart.push({
orderedQuantity: data.getUser.cart[i].quantity,
_id: data.getUser.cart[i]._id,
name: data.getUser.cart[i].name,
image: data.getUser.cart[i].image,
price: data.getUser.cart[i].price,
});
}
addOrder({
variables: {
userId: currentUser.uid,
userEmail: currentUser.email,
status: "ordered",
createdAt: text,
products: newCart,
total: total,
flag: getUserOrders.data.userOrders.length + 1,
zip: add.zip.val ? add.zip.val : add.zip,
state: add.state.val ? add.state.val : add.state,
city: add.city.val ? add.city.val : add.city,
apt: add.apt.val ? add.apt.val : add.apt,
addressStreet: add.addressStreet.val ? add.addressStreet.val : add.addressStreet,
},
});
editUser({
variables: {
id: currentUser.uid,
cart: [],
},
});
}
}, [currentUser.uid, data, getUserOrders.data]);
}
export default AddOrder;
Please check if the code works, anything please comment so I can correct my answer (I haven't tested it yet, but I believe it will work).
Although it is not recommended, as the good practice recommends the use of memos, you can try in the last case (only to verify if this is in fact the problem) to use as follows:
[currentUser.uid, JSON.stringify(data), JSON.stringify(getUserOrders.data)]
In AddOrder component, you are not returning any JSX. You are actually sending order request in the body of the component ( during rendering, which is super bad since it is a side-effect).
Don't return addOrder, editUser calls. Place them inside of useEffect or event handlers depending on your business logic.

React GraphQL mutation returning Invalid input

Background: I'm building a Shopify app using React, NextJS, and GraphQL. The functionality is to add an extra privateMetafield with a value for each selected product.
The problem: I'm trying to create or update (if privateMetafield exists) with Mutation using React-Apollo. I have tried to run the GraphQL mutation in Insomnia (like postman for GraphQL) and it works. But when I add it to my code I don't get the GraphQL to receive the mutation data. Instead, I get this error message:
Unhandled Runtime Error
Error: GraphQL error: Variable $input of type PrivateMetafieldInput! was provided invalid value for privateMetafields (Field is not defined on PrivateMetafieldInput), namespace (Expected value to not be null), key (Expected value to not be null), valueInput (Expected value to not be null)
Insomnia Successful GraphQL test (what it should be like)
edit-products.js
...
import gql from 'graphql-tag';
import { Mutation } from 'react-apollo';
...
const UPDATE_EMISSION = gql`
mutation($input: PrivateMetafieldInput!) {
privateMetafieldUpsert(input: $input) {
privateMetafield {
namespace
key
value
}
}
}
`;
...
class EditEmission extends React.Component {
state = {
emission: '',
id: '',
showToast: false,
};
render() {
const { name, emission, id } = this.state;
return (
<Mutation
mutation={UPDATE_EMISSION}
>
{(handleSubmit, { error, data }) => {
const showError = error && (
<Banner status="critical">{error.message}</Banner>
);
const showToast = data && data.productUpdate && (
<Toast
content="Sucessfully updated"
onDismiss={() => this.setState({ showToast: false })}
/>
);
return (
<Frame>
... <Form>
<TextField
prefix="kg"
value={emission}
onChange={this.handleChange('emission')}
label="Set New Co2 Emission"
type="emission"
/>
...
<PageActions
primaryAction={[
{
content: 'Save',
onAction: () => {
const item = store.get('item');
const id = item.id;
const emissionVariableInput = {
owner: id,
privateMetafields: [
{
namespace: "Emission Co2",
key: "Co2",
valueInput: {
value: emission,
valueType: "INTEGER"
}
}
]
};
console.log(emissionVariableInput)
handleSubmit({
variables: { input: emissionVariableInput },
});
}
}
]}
secondaryActions={[
{
content: 'Remove emission'
}
]}
/>
</Form>
...
</Mutation>
);
}
handleChange = (field) => {
return (value) => this.setState({ [field]: value });
};
}
export default EditEmission;
I get this in the console when I log emissionVariableInput, which looks correct. But why is the data not passed properly to the GraphQL mutation and how do I fix it?
I expect the GraphQL mutation to be successful and create/update a privateMetafield owned by a product.
Thanks in advance!
You have different shapes for input in your examples. In Insomnia you pass:
{
owner: id,
namespace: "Emission Co2",
key: "Co2",
valueInput: {
value: emission,
valueType: "INTEGER"
}
}
While in the code your input looks like:
{
owner: id,
privateMetafields: [{
namespace: "Emission Co2",
key: "Co2",
valueInput: {
value: emission,
valueType: "INTEGER"
}
}]
};

How to pass state array with multiple objects as a parameter to graphql mutation?

I am creating an app that allows users to create lists (all data is stored on mongoDB and accessed via Apollo GraphQL); each list has a property, listItems, an array that stores all the items in the list.
Currently I am now able to move the listItems around and have them setState in an array accessed via state.items as shown in this video. The list movement is controlled by components found within react-sortable-hoc library
The issue I am having is taking this state.items array of listItems and using a mutation to have the state.items array replace the backend respective list.listItems array in the database.
I believe I may have not properly setup the typeDefs for input ListItems (I was getting Error: The type of Mutation.editListItems(listItems:) must be Input Type but got: [ListItem]!. so I changed it to input instead of type which may be an issue?)
Lastly I attempt to call the editListItems mutation in the RankList functional component below (contained in RankList.js) 'onChange'. I am not sure where the issue and when moving items, it seems the mutation is not called. Please advise (let me know if I should include more info)!
Overall, I wonder if the following could be an issue:
Is the typeDef for editListItems properly implemented?
Is onChange the right thing to use here?
Am I calling the mutation properly (in RankList component)?
Am I properly passing state.items in the editListItems mutation (in RankList component)?
typeDefs.js
const { gql } = require("apollo-server");
//schema
module.exports = gql`
type List {
id: ID!
title: String!
createdAt: String!
username: String!
listItems: [ListItem]!
comments: [Comment]!
likes: [Like]!
likeCount: Int!
commentCount: Int!
}
type ListItem {
id: ID!
createdAt: String!
username: String!
body: String!
}
input ListItems {
id: ID!
createdAt: String!
username: String!
body: String!
}
type Comment {
id: ID!
createdAt: String!
username: String!
body: String!
}
type Like {
id: ID!
createdAt: String!
username: String!
}
type User {
id: ID!
email: String!
token: String!
username: String!
createdAt: String!
}
input RegisterInput {
username: String!
password: String!
confirmPassword: String!
email: String!
}
type Query {
getLists: [List]
getList(listId: ID!): List
}
type Mutation {
register(registerInput: RegisterInput): User!
login(username: String!, password: String!): User!
createList(title: String!): List!
editListItems(listId: ID!, listItems: ListItems!): List!
deleteList(listId: ID!): String!
createListItem(listId: ID!, body: String!): List!
deleteListItem(listId: ID!, listItemId: ID!): List!
createComment(listId: ID!, body: String!): List!
deleteComment(listId: ID!, commentId: ID!): List!
likeList(listId: ID!): List!
},
type Subscription{
newList: List!
}
`;
list.js (resolvers)
async editListItems(_, { listId, listItems }, context) {
console.log("editListItems Mutation activated!");
const user = checkAuth(context);
const list = await List.findById(listId);
if (list) {
if (user.username === list.username) {
list.listItems = listItems;
await list.save();
return list;
} else {
throw new AuthenticationError("Action not allowed");
}
} else {
throw new UserInputError("List not found");
}
},
RankList.js (List component shown in the video above)
import React, { useContext, useEffect, useRef, useState } from "react";
import gql from "graphql-tag";
import { useQuery, useMutation } from "#apollo/react-hooks";
import { Form } from "semantic-ui-react";
import moment from "moment";
import { AuthContext } from "../context/auth";
import { SortableContainer, SortableElement } from "react-sortable-hoc";
import arrayMove from "array-move";
import "../RankList.css";
import { CSSTransitionGroup } from "react-transition-group";
const SortableItem = SortableElement(({ value }) => (
<li className="listLI">{value}</li>
));
const SortableList = SortableContainer(({ items }) => {
return (
<ol className="theList">
<CSSTransitionGroup
transitionName="ranklist"
transitionEnterTimeout={500}
transitionLeaveTimeout={300}
>
{items.map((item, index) => (
<SortableItem
key={`item-${item.id}`}
index={index}
value={item.body}
/>
))}
</CSSTransitionGroup>
</ol>
);
});
function RankList(props) {
const listId = props.match.params.listId;
const { user } = useContext(AuthContext);
const listItemInputRef = useRef(null);
const [state, setState] = useState({ items: [] });
const [listItem, setListItem] = useState("");
const { loading, error, data } = useQuery(FETCH_LIST_QUERY, {
variables: {
listId,
},
onError(err) {
console.log(err.graphQLErrors[0].extensions.exception.errors);
// setErrors(err.graphQLErrors[0].extensions.exception.errors);
}
});
useEffect(() => {
if (data && data.getList && data.getList.listItems) {
setState(() => ({ items: data.getList.listItems }));
}
}, [data]);
// const [state, setState] = useState({ items: data.getList.listItems });
const [submitListItem] = useMutation(SUBMIT_LIST_ITEM_MUTATION, {
update() {
setListItem("");
listItemInputRef.current.blur();
},
variables: {
listId,
body: listItem,
},
});
const [editListItems] = useMutation(EDIT_LIST_ITEMS_MUTATION, {
variables: {
listId,
listItems: state.items,
},
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error..</p>;
function deleteListCallback() {
props.history.push("/");
}
function onSortEnd({ oldIndex, newIndex }) {
setState(({ items }) => ({
items: arrayMove(items, oldIndex, newIndex),
}));
//THIS MAY BE WHERE THE ISSUE LIES
editListItems();
}
let listMarkup;
if (!data.getList) {
listMarkup = <p>Loading list...</p>;
} else {
const {
id,
title,
createdAt,
username,
listItems,
comments,
likes,
likeCount,
commentCount,
} = data.getList;
listMarkup = user ? (
<div className="todoListMain">
<div className="rankListMain">
<div className="rankItemInput">
<h3>{title}</h3>
<Form>
<div className="ui action input fluid">
<input
type="text"
placeholder="Choose rank item.."
name="listItem"
value={listItem}
onChange={(event) => setListItem(event.target.value)}
ref={listItemInputRef}
/>
<button
type="submit"
className="ui button teal"
disabled={listItem.trim() === ""}
onClick={submitListItem}
>
Submit
</button>
</div>
</Form>
</div>
<SortableList
items={state.items}
onSortEnd={onSortEnd}
helperClass="helperLI"
/>
</div>
</div>
) : (
<div className="todoListMain">
<div className="rankListMain">
<div className="rankItemInput">
<h3>{props.title}</h3>
</div>
<SortableList
items={listItems}
onSortEnd={onSortEnd}
helperClass="helperLI"
/>
</div>
</div>
);
}
return listMarkup;
}
const EDIT_LIST_ITEMS_MUTATION = gql`
mutation($listId: ID!, $listItems: ListItems!) {
editListItems(listId: $listId, listItems: $listItems) {
id
listItems {
id
body
createdAt
username
}
}
}
`;
const SUBMIT_LIST_ITEM_MUTATION = gql`
mutation($listId: ID!, $body: String!) {
createListItem(listId: $listId, body: $body) {
id
listItems {
id
body
createdAt
username
}
comments {
id
body
createdAt
username
}
commentCount
}
}
`;
const FETCH_LIST_QUERY = gql`
query($listId: ID!) {
getList(listId: $listId) {
id
title
createdAt
username
listItems {
id
createdAt
username
body
}
likeCount
likes {
username
}
commentCount
comments {
id
username
createdAt
body
}
}
}
`;
export default RankList;
too many issues ... briefly:
const [state, setState] = useState({ items: [] });
Don't use setState in hooks ... it's missleading, setState is for class components.
Use some meaningful name, f.e. itemList:
const [itemList, setItemList] = useState( [] ); // just array
Adjust query result saving:
useEffect(() => {
if (data && data.getList && data.getList.listItems) {
// setState(() => ({ items: data.getList.listItems }));
setItemList( data.getList.listItems );
}
}, [data]);
Adjust sortable update handler:
const onSortEnd = ({oldIndex, newIndex}) => {
// this.setState(({items}) => ({
// items: arrayMove(items, oldIndex, newIndex),
// }));
const newListOrder = arrayMove( itemList, oldIndex, newIndex);
// console.log("array sorted", newListOrder );
// update order in local state
// kind of optimistic update
setItemList( newListOrder );
// update remote data
editListItems( {
variables: {
listId,
listItems: newListOrder,
}
});
Pass onSortEnd handler into sortable:
<SortableList items={itemList} onSortEnd={onSortEnd} />
Mutation should have update handler (writeQuery - read docs) to force query update/rerendering. Not quite required in this case (array already sorted in sortable, we're updating local array, it will rerender) but it should be here (mutation fail/errors - handle error from useMutation).
Types are good ... but api detects wrong type ... every item in array contains __typename property ... iterate over newListOrder elements (f.e. using .map()) and remove __typename. It should be visible in console.log(newListOrder).
Try backend/API in playground (/graphiql) ... try mutations using variables before coding frontend. Compare network requests/response details.

Accessing id field of query returned object changes it to undefined

Im working on a react/mondgodb/graphql/apollo web application. I have a query set up to get a member using a fbid (its a firebase Id that I insert as fbid in order to find a logged in member profile in my database by the firebase id). When I do the query, I can console.log the query returned data as a whole. If I try to access members of the data, I get that the object is undefined. Here is the component where I query:
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom'
import { graphql, compose } from 'react-apollo';
import { withFirebase } from '../Firebase';
import { getMemberByFBidQuery } from '../../queries/queries';
import DataList from '../wishes/MyDataList';
const INITIAL_STATE = {
memberProfile: {},
error: null
}
class DashboardGuts extends Component {
constructor(props) {
super(props);
this.state = { ...INITIAL_STATE };
}
render() {
const memberProfile = this.props.getMemberByFBidQuery.memberByFBid;
console.log('memberProfile: ', memberProfile.id);
return (
<div className="dashboard container">
<div className="row">
<div className="col s12 m6">
<h4>My Data List</h4>
<DataList memberProfile={memberProfile} />
</div>
</div>
</div>
)
}
}
const Dashboard = withFirebase(DashboardGuts)
export default compose(
graphql(getMemberByFBidQuery, {
name: "getMemberByFBidQuery",
options: (props) => {
return {
variables: {
fbid: props.firebase.auth.O
}
}
}
}),
)(Dashboard)
When I console.log the memberProfile in the code above, I get all the data I expect returned from the query. The console.log output looks like the following:
memberProfile:
{firstname: "jo", lastname: "blo", id: "1234",
fbid: "5678", __typename: "Member"}
fbid: "5678"
firstname: "jo"
id: "1234"
lastname: "smith"
__typename: "Member"
__proto__: Object
}
The problem is when I try to access any of the fields within the memberProfile object - like id, I get errors. For example if I change this line:
console.log('memberProfile: ', memberProfile);
To this:
console.log('memberProfile: ', memberProfile.id);
Then I get the error that I can't access id of undefined object.
Any ideas on how to access that id property? In case it helps, here is the actual graphQL query:
const getMemberByFBidQuery = gql`
query($fbid: String){
memberByFBid(fbid: $fbid) {
firstname,
lastname,
id,
fbid
}
}
`
And here's the schema for that query:
memberByFBid: {
type: MemberType,
args: { fbid: { type: GraphQLString } },
resolve(parent, args) {
return Member.findOne({ fbid: args.fbid });
}
},
And here is the schema for MemberType:
const MemberType = new GraphQLObjectType({
name: "Member",
fields: () => ({
id: { type: GraphQLID },
firstname: { type: GraphQLString },
lastname: { type: GraphQLString },
email: { type: GraphQLString },
fbid: { type: GraphQLString },
wishes: {
type: GraphQLList(WishType),
resolve(parent, args) {
return Wish.find({ memberId: parent.id })
}
},
groups: {
type: GraphQLList(MemberToGroupMapType),
resolve(parent, args) {
return MemberToGroupMap.find({ memberId: parent.id });
}
}
})
});
Thanks in advance for any help!
Per TLadd's suggestions, I changed my render function but Im still getting error. Her it is currently:
render() {
console.log('look for loading: ', this.props);
const { memberByFBid, loading, error } = this.props.getMemberByFBidQuery;
if (loading) {
return (<p>Loading...</p>);
} else if (error) {
return (<p>Error!</p>);
}
console.log('memberProfilexx: ', memberByFBid);
return (
<div className="dashboard container">
<div className="row">
<div className="col s12 m6">
<h4>My Wish List</h4>
<WishList memberProfile={memberByFBid} />
</div>
<div className="col s12 m5 offset-m1">
<div>Hello</div>
</div>
</div>
</div>
)
}
But I still get the error if I try to console.log memberByFBid.id instead of just memberByFBid.
You are hitting this error because you query has not loaded yet when your DashboardGuts component first attempts to render. From the react-apollo docs on the data property (getMemberByFBidQuery in this case, since you specify a custom name config option in the graphql HOC):
Make sure to always check data.loading and data.error in your components before rendering. Properties like data.todos which contain your app’s data may be undefined while your component is performing its initial fetch. Checking data.loading and data.error helps you avoid any issues with undefined data.
Following this advice, your render method becomes something like
render() {
const { memberByFBid, loading, error } = this.props.getMemberByFBidQuery;
if (loading) {
return <p>Loading...</p>;
} else if (error) {
return <p>Error!</p>;
}
console.log('memberProfile: ', memberProfile.id);
return (
<div className="dashboard container">
<div className="row">
<div className="col s12 m6">
<h4>My Data List</h4>
<DataList memberProfile={memberProfile} />
</div>
</div>
</div>
)
}
You would likely want to handle loading/error state in a more appealing fashion, but the main idea is that you need to account for the data not being loaded yet in the render method, since it won't be in the initial render.

Resources