reactjsgraphqlapollo-clientapollo-serverreact-sortable-hoc

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:

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;

Solution

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