Reuse mutations without duplicating code in Apollo + React? - reactjs

I have the mutation below in a React Component. Im going to need the same mutation in multiple components and on different pages.
How can I reuse my mutation code without repeating it?
This example isn't that complex but some queries use optimistic UI and write to the store.
import React from 'react';
import { graphql, compose } from 'react-apollo';
import { gql } from 'apollo-boost';
const JoinLocation = props => {
if (props.ME.loading) return null;
const { locationMachineName } = props;
const me = props.ME.me;
const submit = () => {
props
.JOIN_LOCATION({
variables: {
userId: me.id,
locationMachine: locationMachineName,
},
})
.catch(err => {
console.error(err);
});
};
return <button onClick={() => submit()}>Join location</button>;
};
const ME = gql`
query me {
me {
id
}
}
`;
const JOIN_LOCATION = gql`
mutation joinLocation($userId: ID!, $locationId: ID!) {
joinLocation(userId: $userId, locationId: $locationId) {
id
}
}
`;
export default compose(
graphql(JOIN_LOCATION, { name: 'JOIN_LOCATION' }),
graphql(ME, { name: 'ME' }),
)(JoinLocation);

Create a higher-order component (HOC) for the mutation/query that contains the gql options and optimistic UI logic:
const JOIN_LOCATION = gql`
mutation joinLocation($userId: ID!, $locationId: ID!) {
joinLocation(userId: $userId, locationId: $locationId) {
id
}
}
`;
export const withJoinLocation = component => graphql(JOIN_LOCATION, { name: 'JOIN_LOCATION' })(component);
Then wrap your different components with it.
export default withJoinLocation(JoinLocation);
UPDATE: Based on your below comment, if you want to encapsulate the whole submit logic and not just the mutation as stated in your question, you can use a render prop like so:
import React from 'react';
import { graphql, compose } from 'react-apollo';
import { gql } from 'apollo-boost';
const JoinLocation = props => {
if (props.ME.loading) return null;
const { locationMachineName } = props;
const me = props.ME.me;
const submit = () => {
props
.JOIN_LOCATION({
variables: {
userId: me.id,
locationMachine: locationMachineName,
},
})
.catch(err => {
console.error(err);
});
};
return props.render(submit);
};
const ME = gql`
query me {
me {
id
}
}
`;
const JOIN_LOCATION = gql`
mutation joinLocation($userId: ID!, $locationId: ID!) {
joinLocation(userId: $userId, locationId: $locationId) {
id
}
}
`;
export default compose(
graphql(JOIN_LOCATION, { name: 'JOIN_LOCATION' }),
graphql(ME, { name: 'ME' }),
)(JoinLocation);
Now any component can consume the reusable submit logic. Assume you name the above component JoinLocation.js:
import JoinLocation from './JoinLocation';
const Comp = () => {
return <JoinLocation render={submit => <button onClick={() => submit()}>Join location</button>}/>
}

Related

Add to Cart Item functionality is not working in my react-redux

I'm working on react shopping cart using react-redux and My add to cart Functionality is not working. I've tried a lot with multiple approaches but those did not work. I'm unable to get data into my CartItem. Did stack over flow tried multiple things but still not worthy.
this is my CartActions.js
import React from "react";
import axios from "axios";
import { ADD_CART_ITEM } from "../constant/cartConstant";
const addToCart = (id, qty) => async (dispatch, getState) => {
const { data } = await axios.get(`/http://127.0.0.1:8000/products/${id}`);
console.log(data, "<<<<<<DATA");
dispatch({
type: ADD_CART_ITEM,
payload: {
product: data.id,
name: data.name,
image: data.image,
price: data.price,
countInStock: data.countInStock,
},
});
localStorage.setItem(
"cartItems",
JSON.stringify(getState().CartReducer.cartItems)
);
};
export default addToCart;
this is my Cart.js
import React, { useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { useLocation } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux'
import addToCart from '../actions/CartActions'
function Cart() {
const { id } = useParams();
const { search } = useLocation();
const ProductID = id;
const qty = search ? search.split("=") : 1
console.log(qty, ">>>QTY");
const dispatch = useDispatch()
console.log(ProductID, ">>>>PROduct ID");
useEffect(() => {
if (ProductID) {
dispatch(addToCart(ProductID, qty))
}
console.log("Hello world");
}, [dispatch, ProductID, qty])
return (
<div>Cart</div>
)
}
export default Cart
this is my CartReducer.js
import React from "react";
import { ADD_CART_ITEM } from "../constant/cartConstant";
function cartReducer(state = { cartItems: [] }, actions) {
switch (actions.type) {
case ADD_CART_ITEM:
const Item = actions.payload;
const exitsItem = state.cartItems((x) => x.product === Item.product);
if (exitsItem) {
return {
...state,
cartItems: state.cartItems.map((x) =>
x.product === exitsItem.product ? Item : x
),
};
} else {
return {
...state,
cartItems: [...state.cartItems, Item],
};
}
default:
return state;
}
}
export default cartReducer;
I have tried multiple approaches but those did not help me
Your API call URL has an unexpected forward-slash ('/') in the beginning.
Correct Pattern.
const { data } = await axios.get(`http://127.0.0.1:8000/products/${id}`);
Or if you want to use /products/${id} this pattern then you can add proxy into package.json like this.
//package.json
...
"proxy": "http://127.0.0.1:8000", // add this as proxy
...
After adding this you can use URLs patterns without using the hostname
http://127.0.0.1:8000. in all over the app.
e.g:
const { data } = await axios.get(`/products/${id}`);
This should work now. If still, it gives an error 404 then might be an issue with your URLs patterns in the urls.py in the Django URLs.

Run sequental graphql queries using Apollo Client

I am trying to load data using below queries in React.
export const GET_POSTS = gql`
query GetLaunches($limit: Int) {
launches(limit: $limit) {
name
id
date_utc
}
}
`;
I am using above query like below inside React component.
const {loading, error, data} = useQuery(GET_POSTS, {
variables: { limit },
skip: !limit
});
Above return data like below.
data = {
items: [
{
date_utc: "2014-01-06T18:06:00.000Z"
id: ['9D1B7E0']
name: "Thaicom 6"
},
{
date_utc: "2014-01-06T18:06:00.000Z"
id: ['1S1B5D0']
name: "Thai 0"
},
{
date_utc: "2014-01-06T18:06:00.000Z"
id: ['7F1B7E3']
name: "Sun 6"
},
{
date_utc: "2014-01-06T18:06:00.000Z"
id: ['4A1B5K7']
name: "Moon 0"
}
]
}
Now what I want to make another query where I will pass above id as a variable to another query and load description of each individual above list of items.
I tried below. Below is queries.ts file.
import { gql } from 'apollo-boost';
export const GET_POSTS = gql`
query GetLaunches($limit: Int) {
launches(limit: $limit) {
name
id
date_utc
}
}
`;
export const GET_DESCRIPTION = gql`
query GetDescription($id: ID!) {
mission(id: $id) {
description
}
}
`;
Here is Home.tsx file.
import React, { useEffect, useState } from 'react';
import { useQuery } from '#apollo/react-hooks';
import { HomeContainer } from '../Container/';
import { GET_POSTS, GET_DESCRIPTION } from '../Queries/';
export const Home: React.FC = () => {
const limit = 10;
const launches = useQuery(GET_LAUNCHES, {
variables: { limit },
skip: !limit
});
const launchDescription = useQuery(GET_DESCRIPTION, {
variables: { id: launches?.data?.launches?.mission_id?.[0] },
skip: launches.loading || !launches?.data?.launches?.mission_id?.[0],
});
useEffect(() => {
console.log(launchDescription.data); // returns undefined
console.log(launches.data); // this returns correct data
}, [launches]);
return (
<HomeContainer
loading={launches.loading}
error={launches.error}
data={launches.data}
/>
);
}
Inside useEffect hook, I want to add description to each individual launch in launches.data array. How can I make this work?
You need to use optional chaining for launches?.loading or simply just omit this to !launches?.data?.launches?.mission_id?.[0]
Both of your queries right now are fired at the same time. What you need to do is make the second query wait for the first one to be completed (using useLazyQuery), and only fire when the ID is available.
Here is how that should look:
import React, { useEffect, useState } from 'react';
import { useQuery, useLazyQuery } from '#apollo/react-hooks';
import { HomeContainer } from '../Container/';
import { GET_POSTS, GET_DESCRIPTION } from '../Queries/';
export const Home: React.FC = () => {
const limit = 10;
const launches = useQuery(GET_POSTS, {
variables: { limit },
skip: !limit
});
// useLazyQuery provides a function you can use to fire the query
// only when you have what you need for it
const [getLaunchDescription, launchDescription] = useLazyQuery(GET_DESCRIPTION)
useEffect(() => {
// check that you have the id before you ask for the description
if (launches?.data?.launches?.mission_id?.length) {
getLaunchDescription({
variables: { id: launches.data.launches.mission_id[0] }
});
}
}, [launches])
return (
<HomeContainer
loading={launches.loading}
error={launches.error}
data={launches.data}
/>
);
}

Using `react-apollo-hooks` and `useSubscription` hook

I'm building a simple todo app using React, Apollo and react-apollo-hooks for hooks support, but the useSubscription hook doesnt fire.
I know the actual backend stuff works, because I have a graphiql app set up, and whenever I save a todo, the todoCreated event shows up in graphiql. I also know that the websocket-setup is working properly, because the queries & mutations are going through the websocket. I'm using Elixir, Phoenix, Absinthe, by the way, for the backend stuff.
Here's the Todo-app component:
import React, { useState } from 'react';
import gql from 'graphql-tag';
import { useQuery, useMutation, useSubscription } from 'react-apollo-hooks';
import styles from 'styles.css';
const TODO_FRAGMENT = gql`
fragment TodoFields on Todo {
id
description
}
`;
const GET_TODOS = gql`
{
todos {
...TodoFields
}
}
${TODO_FRAGMENT}
`;
const SAVE_TODO = gql`
mutation createTodo($description: String!) {
createTodo(description: $description) {
...TodoFields
}
}
${TODO_FRAGMENT}
`;
const DELETE_TODO = gql`
mutation deleteTodo($id: ID!) {
deleteTodo(id: $id) {
id
}
}
`;
const NEW_TODO_SUBSCRIPTION = gql`
subscription {
todoCreated {
...TodoFields
}
}
${TODO_FRAGMENT}
`;
const Todos = () => {
const [inputValue, setInputValue] = useState('');
const { data, error, loading } = useQuery(GET_TODOS);
const saveTodo = useMutation(SAVE_TODO, {
update: (proxy, mutationResult) => {
proxy.writeQuery({
query: GET_TODOS,
data: { todos: data.todos.concat([mutationResult.data.createTodo]) },
});
},
});
const deleteTodo = useMutation(DELETE_TODO, {
update: (proxy, mutationResult) => {
const id = mutationResult.data.deleteTodo.id
proxy.writeQuery({
query: GET_TODOS,
data: { todos: data.todos.filter(item => item.id !== id) },
});
},
});
const subData = useSubscription(NEW_TODO_SUBSCRIPTION);
console.log(subData);
if (loading) {
return <div>Loading...</div>;
};
if (error) {
return <div>Error! {error.message}</div>;
};
return (
<>
<h1>Todos</h1>
{data.todos.map((item) => (
<div key={item.id} className={styles.item}>
<button onClick={() => {
deleteTodo({
variables: {
id: item.id,
},
});
}}>Delete</button>
{' '}
{item.description}
</div>
))}
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
type="text"
/>
<button onClick={() => {
saveTodo({
variables: {
description: inputValue,
},
});
setInputValue('');
}}>Save</button>
</>
);
};
export default Todos;
And here's the root component:
import React from 'react';
import { ApolloProvider } from 'react-apollo';
import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks';
import Todos from 'components/Todos';
import apolloClient from 'config/apolloClient';
const App = () => (
<ApolloHooksProvider client={apolloClient}>
<Todos />
</ApolloHooksProvider>
);
export default App;
Anyone have a clue on what I seem to be doing wrong?
Sorry, I figured it out, it was a silly mistake on my part. The problem seems to have been with my apolloClient setup:
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { ApolloLink } from 'apollo-link';
import absintheSocketLink from 'config/absintheSocketLink';
const apolloClient = new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
if (networkError) console.log(`[Network error]: ${networkError}`);
}),
split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
new HttpLink({
uri: 'http://localhost:4000/api/graphql',
credentials: 'same-origin'
}),
absintheSocketLink,
),
]),
cache: new InMemoryCache()
});
export default apolloClient;
The error in the code above is the fact that the line
absintheSocketLink,
is in the wrong place. It should've been before the HttpLink.
Silly me.
I had the same issue my subscription was always sending null data and i had a silly mistake as well.

React Apollo client prop refetchQueries after mutation

I have been reading the Apollo documentation and I can't find any examples on how to refetch after a mutation with the client props that is being passed by withApollo HOC.
My component:
import React, {Fragment} from 'react';
import gql from 'graphql-tag';
import { withApollo } from 'react-apollo';
...
const getPosts = gql`
{
posts {
_id
title
description
user {
_id
}
}
}`;
const deletePost = gql`
mutation deletePost($_id: String){
deletePost(_id: $_id)
}
`;
class PostList extends React.Component {
static propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
state = {posts: null};
componentDidMount() {
this.props.client.query({
query: getPosts,
}).then(({ data }) => {
this.setState({ posts: data.posts });
});
}
deletePost = postId => {
this.props.client
.mutate({
mutation: deletePost,
variables: {
_id: postId
},
})
.then(({ data }) => {
alert('Post deleted!');
});
};
render() {
const {posts} = this.state;
if (!posts) {
return <div>Loading....</div>
}
return (
<div className="post">
...stuff...
</div>
)
}
}
export default withApollo(PostList);
I want to refetch the posts each time one is deleted.
The posts data that you render is in your component state, but you only query posts in componentDidMount. This method is called only when your component is first rendered.
class PostList extends React.Component {
fetchPosts() {
this.props.client.query({
query: getPosts,
}).then(({ data }) => {
this.setState({ posts: data.posts });
});
}
componentDidMount() {
this.fetchPosts();
}
deletePost = postId => {
this.props.client
.mutate({
mutation: deletePost,
variables: {
_id: postId
},
})
.then(({ data }) => {
this.fetchPosts()
});
};
render() {
const {posts} = this.state;
if (!posts) {
return <div>Loading....</div>
}
return (
<div className="post">
...stuff...
</div>
)
}
}
In fact, you do not even need to have a local state, you can rely on Apollo client local cache and use the Query component as well as the Mutation component.

Next.js redirect inside of GraphQL mutation

In my Next.js component I made a mutation request to GraphQL server and after it successfully done I need to redirect to another page. How I do it now:
import React, { Component } from 'react';
import Router from 'next/router';
import { Mutation } from 'react-apollo';
import { gql } from 'apollo-boost';
const signInMutation = gql`
mutation signIn($accessToken: String!) {
signIn(accessToken: $accessToken)
}
`;
export default class extends Component {
static async getInitialProps({ query: { accessToken } }) {
return { accessToken };
}
render() {
const { accessToken } = this.props;
return (
<Mutation mutation={signInMutation} ignoreResults>
{signIn => {
signIn({ variables: { accessToken } }).then(() => {
Router.push({
pathname: '/user'
});
});
return null;
}}
</Mutation>
);
}
}
It works fine but Next.js throws an error: You should only use "next/router" inside the client side of your app.. So, what is the best way to fix the error?
Your signIn mutation is executed on render, and when NextJS renders your app on the server side, executes your mutation.
You should render a button and only trigger the mutation on click:
import React, { Component } from 'react';
import Router from 'next/router';
import { Mutation } from 'react-apollo';
import { gql } from 'apollo-boost';
const signInMutation = gql`
mutation signIn($accessToken: String!) {
signIn(accessToken: $accessToken)
}
`;
export default class extends Component {
static async getInitialProps({ query: { accessToken } }) {
return { accessToken };
}
render() {
const { accessToken } = this.props;
return (
<Mutation mutation={signInMutation} ignoreResults>
{signIn => {
return (
<button onClick={async () => {
await signIn({ variables: { accessToken } })
Router.push({ pathname: '/user' })
}}>Login</button>
)
}}
</Mutation>
);
}
}

Resources