cancel
Showing results for 
Search instead for 
Did you mean: 

Head's Up! These forums are read-only. All users and content have migrated. Please join us at community.neo4j.com.

Using `useQuery`, `useEffect`, and `useMutation` between components

Hi,

I am working on implementing useQuery, useEffect, and useMutation to write to the graph and then update the UI to rerender a list of elements. I am having trouble finding a solid example to follow. Has anyone been working with these?

So I currently have one component <Rank /> that uses useQuery to pull data down into a table. In the component I have <CreateRank /> that uses useMutation to write to the graph. That aspect of it works perfectly fine as I can write to the graph. However, I am stuck at the moment on how/where to implement useEffect to update and rerender once I have submited and written to the graph.

1 ACCEPTED SOLUTION

This is a bit of a long response, but I am hopeful others might make sense of this for them to implement a similar solution.

So after following the code shown in here, this is what I did that is working:

This is in my Strike.js file

const GET_STRIKES = gql`
  {
    Strike {
      name
    }
  }
`;

export default function Strike() {
  const classes = useStyles();

  const [order, setOrder] = useState("asc");
  const [orderBy, setOrderBy] = useState("name");

  const handleSortRequest = property => {
    const newOrderBy = property;
    let newOrder = "desc";

    if (orderBy === property && order === "desc") {
      newOrder = "asc";
    }

    setOrder(newOrder);
    setOrderBy(newOrderBy);
  };

  const getSorting = (order, orderBy) => {
  return order === "desc"
    ? (a, b) => (b[orderBy] < a[orderBy] ? -1 : 1)
    : (a, b) => (a[orderBy] < b[orderBy] ? -1 : 1);
  };

  const { loading, error, data } = useQuery(GET_STRIKES);

  if (loading) return "Loading...";
  if (error) return `Error ${error.message}`;

  return (
    <div className={classes.root}>
      <Grid container spacing={3}>
        <Grid item /*xs={12}*/ sm={6}>
          <Table className={classes.table}>
            <TableHead>
              <TableRow>
                <TableCell
                  key="name"
                  sortDirection={orderBy === "name" ? order : false}
                >
                  <Tooltip title="Sort" placement="bottom-start" enterDelay={300}>
                    <TableSortLabel
                      active={orderBy === "name"}
                      direction={order}
                      onClick={() => handleSortRequest("name")}
                    >
                      Name
                    </TableSortLabel>
                  </Tooltip>
                </TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {data.Strike //.slice()
                .sort(getSorting(order, orderBy))
                .map(n => {
                  return (
                    <TableRow key={n.name}>
                      <TableCell component="th" scope="row">
                        {n.name}
                      </TableCell>
                    </TableRow>
                  );
                })}
            </TableBody>
          </Table>
        </Grid>
        <Grid item /*xs={12}*/ sm={6}>
          <RankSelectFilter />
          <CreateStrike data={data} GET_STRIKES={GET_STRIKES} />
        </Grid>
      </Grid>
    </div>
  );
}

This is in my CreateStrike.js file:

const CREATE_STRIKE = gql`
  mutation CreateStrike($name: String!) {
    CreateStrike(name: $name) {
      name
    }
  }
`;

export default function CreateStrike({ data, GET_STRIKES }) {
  const classes = useStyles();

  const [name, setName] = useState("");

  const [CreateStrike] = useMutation(
    CREATE_STRIKE,
    {
      update(cache, { data: { CreateStrike } }) {
        const { Strike } = cache.readQuery({ query: GET_STRIKES });
        cache.writeQuery({
          query: GET_STRIKES,
          data: { Strike: Strike.concat([CreateStrike]) },
        })
      }
    }
  );

  const handleClick = event => {
    CreateStrike({
      variables: { name: name }
    });
  }

  return (
    <div className={classes.root} /*justifyContent="flex-start"*/>
      <form className={classes.container} noValidate autoComplete="off">
        <CreateStrikeTextField
          name={name}
          setName={setName}
        />
      </form>
  
      <Button
        onClick={handleClick}

        className={classes.button}
        color="primary"
        variant="contained"
      >
        Submit
      </Button>
    </div>
  );
}

The key aspect in Strike.js was passing data and GET_STRIKES to the CreateStrike component.

<CreateStrike data={data} GET_STRIKES={GET_STRIKES} />

Then in CreateStrike.js I use the following to run the mutation and update data which then causes Strike to rerender.

const [CreateStrike] = useMutation(
    CREATE_STRIKE,
    {
      update(cache, { data: { CreateStrike } }) {
        const { Strike } = cache.readQuery({ query: GET_STRIKES });
        cache.writeQuery({
          query: GET_STRIKES,
          data: { Strike: Strike.concat([CreateStrike]) },
        })
      }
    }
  )

I am not entirely certain it is best practice to pass data like this, but in this instance it works well and I am not needing to use useEffect.

In summary, the code shown above rerenders a table upon submitting and adding an entry using useQuery and useMutation and that does not require useEffect.

View solution in original post

9 REPLIES 9

MuddyBootsCode
Graph Steward

If you're using the Grandstack and the useQuery and useMutation hooks there's not really any instances when you need to employ useEffect. I've got a production application created and it doesn't contain useEffect anywhere in the GraphQL/Apollo logic. That being said my use cases might be way different from what you're doing, but maybe give us an example of how/where you're wanting to place the useEffect hook and the community might be able to steer you in the correct direction. Also, here are the official Apollo GraphQL React Hooks docs that might help as well https://www.apollographql.com/docs/react/api/react-hooks/

Please note, the current layout/component nesting is temporary as I flush out the layout and functionality. If you have recommendations on how to adjust nesting of components to make this work more smoothly I am open doing that.

The layout/nesting of the components is as follows:

App.js
   > TabPanel.js
      > Strike.js
         > Table component (as seen on left of image above)
         > CreateStrike.js (as seen on right of image above)

I utilize the useQuery method to call the database and populate the Table component. In the nested CreateStrike.js I use the useMutation method to write to the database. It was my understanding that I would need to use useEffect to update the table on the left. I tried following several examples, and have read the React Hooks useEffect and looked over the link you sent. So I figure I am missing something simple.

MuddyBootsCode
Graph Steward

You should be able to manipulate the state of whatever you’re messing with via what’s returned from your mutation. So if you’re table values are updated by a mutation you can assign the returned values to your table. I’m in a bad spot to give code examples but let’s say your table values are in a useState variable then you can update them after the mutation completes and returns the updated values etc. I’ll try to do a better example when I can sit down but I hope that helps.

Yeah I am thinking something along a similar line. I have 2 flights ahead of me today so I think I will be working on this. I'll let you know if I figure it out.
Thanks

MuddyBootsCode
Graph Steward

Here's an example of some code in my application:

const DivisionOrder = ({ history, match, enqueueSnackbar }) => {
  const classes = WellFormStyles();

  const AddDOI = useMutation(ADD_DIVISION_ORDER);
  const RemoveDOI = useMutation(REMOVE_DIVISION_ORDER);
  const updateNotes = useMutation(EDIT_WELL);

  const { data, loading, error } = useQuery(DOIQuery, {
    variables: { id: match.params.wellID },
  });

  if (loading) {
    return <LoadingView />;
  }
  if (error) {
    return error.toString();
  }
  // Break well out of data returned from query
  const Well = data.Well[0] || [];

So that's the initial query used to get that division order, but could also easily be all division orders, etc. Then you can call your mutations:

onRowUpdate: (newData, oldData) => {
     return AddDOI({
              variables: {
              from: { id: Well.id },
                  to: { id: newData.Tract.id },
                         data: pushData,
                            },
                          })
                            .then(({ data: { AddWellDivisionOrder } }) => {
                              remove(oldData.tableData.id);
                              insert(oldData.tableData.id, {
                                ...pushData,
                                Tract: { id: newData.Tract.id },
                                mesDOI,
                                mpiDOI,
                                sroDOI,
                              });
                            })
                            .catch((error) => console.log(error));

I'm using a library(Formik) to insert and remove those new objects into a table. So I'm updating my state there. You could do something like create a variable:

const [tableValues, setTableValues] = useState({Default Object});

And then when you call your mutation and you get something return you could add that to your state:

return YourMutation({
    variables: {},
    }).then( data => { setTableValues[...tableValues, data.theDataYouNeed] })

and then you'll have updated your state without the need for useEffect, or you can find an example that will show you how to useState with useEffect if you want.

Thanks! I will see if I can get something like this to work for me. I will let you know if I have any additional questions.

The issue with finding an example with useEffect is that they seem overly simple and not really working with useMutation and useQuery. I am hopeful I can take what you shared and work it into my app. I will let you know.

Just wanted to let you know I got things working. I ended up reading over this section a bit and playing around. I will put together a post showing my code so other can possibly reference it. Stay tuned...

MuddyBootsCode
Graph Steward

Good deal. Yeah there's a lot you can do with the cache too. Optimistic UI, etc. Looking forward to seeing what you got done.

This is a bit of a long response, but I am hopeful others might make sense of this for them to implement a similar solution.

So after following the code shown in here, this is what I did that is working:

This is in my Strike.js file

const GET_STRIKES = gql`
  {
    Strike {
      name
    }
  }
`;

export default function Strike() {
  const classes = useStyles();

  const [order, setOrder] = useState("asc");
  const [orderBy, setOrderBy] = useState("name");

  const handleSortRequest = property => {
    const newOrderBy = property;
    let newOrder = "desc";

    if (orderBy === property && order === "desc") {
      newOrder = "asc";
    }

    setOrder(newOrder);
    setOrderBy(newOrderBy);
  };

  const getSorting = (order, orderBy) => {
  return order === "desc"
    ? (a, b) => (b[orderBy] < a[orderBy] ? -1 : 1)
    : (a, b) => (a[orderBy] < b[orderBy] ? -1 : 1);
  };

  const { loading, error, data } = useQuery(GET_STRIKES);

  if (loading) return "Loading...";
  if (error) return `Error ${error.message}`;

  return (
    <div className={classes.root}>
      <Grid container spacing={3}>
        <Grid item /*xs={12}*/ sm={6}>
          <Table className={classes.table}>
            <TableHead>
              <TableRow>
                <TableCell
                  key="name"
                  sortDirection={orderBy === "name" ? order : false}
                >
                  <Tooltip title="Sort" placement="bottom-start" enterDelay={300}>
                    <TableSortLabel
                      active={orderBy === "name"}
                      direction={order}
                      onClick={() => handleSortRequest("name")}
                    >
                      Name
                    </TableSortLabel>
                  </Tooltip>
                </TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {data.Strike //.slice()
                .sort(getSorting(order, orderBy))
                .map(n => {
                  return (
                    <TableRow key={n.name}>
                      <TableCell component="th" scope="row">
                        {n.name}
                      </TableCell>
                    </TableRow>
                  );
                })}
            </TableBody>
          </Table>
        </Grid>
        <Grid item /*xs={12}*/ sm={6}>
          <RankSelectFilter />
          <CreateStrike data={data} GET_STRIKES={GET_STRIKES} />
        </Grid>
      </Grid>
    </div>
  );
}

This is in my CreateStrike.js file:

const CREATE_STRIKE = gql`
  mutation CreateStrike($name: String!) {
    CreateStrike(name: $name) {
      name
    }
  }
`;

export default function CreateStrike({ data, GET_STRIKES }) {
  const classes = useStyles();

  const [name, setName] = useState("");

  const [CreateStrike] = useMutation(
    CREATE_STRIKE,
    {
      update(cache, { data: { CreateStrike } }) {
        const { Strike } = cache.readQuery({ query: GET_STRIKES });
        cache.writeQuery({
          query: GET_STRIKES,
          data: { Strike: Strike.concat([CreateStrike]) },
        })
      }
    }
  );

  const handleClick = event => {
    CreateStrike({
      variables: { name: name }
    });
  }

  return (
    <div className={classes.root} /*justifyContent="flex-start"*/>
      <form className={classes.container} noValidate autoComplete="off">
        <CreateStrikeTextField
          name={name}
          setName={setName}
        />
      </form>
  
      <Button
        onClick={handleClick}

        className={classes.button}
        color="primary"
        variant="contained"
      >
        Submit
      </Button>
    </div>
  );
}

The key aspect in Strike.js was passing data and GET_STRIKES to the CreateStrike component.

<CreateStrike data={data} GET_STRIKES={GET_STRIKES} />

Then in CreateStrike.js I use the following to run the mutation and update data which then causes Strike to rerender.

const [CreateStrike] = useMutation(
    CREATE_STRIKE,
    {
      update(cache, { data: { CreateStrike } }) {
        const { Strike } = cache.readQuery({ query: GET_STRIKES });
        cache.writeQuery({
          query: GET_STRIKES,
          data: { Strike: Strike.concat([CreateStrike]) },
        })
      }
    }
  )

I am not entirely certain it is best practice to pass data like this, but in this instance it works well and I am not needing to use useEffect.

In summary, the code shown above rerenders a table upon submitting and adding an entry using useQuery and useMutation and that does not require useEffect.

Nodes 2022
Nodes
NODES 2022, Neo4j Online Education Summit

All the sessions of the conference are now available online