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:
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.