How to determine mutation loading state with react-apollo graphql - reactjs

2018 Update: Apollo Client 2.1 added a new Mutation component that adds the loading property back. See #robin-wieruch's answer below and the announcement here https://dev-blog.apollodata.com/introducing-react-apollo-2-1-c837cc23d926 Read on for the original question which now only applies to earlier versions of Apollo.
Using the current version of the graphql higher-order-component in react-apollo (v0.5.2), I don't see a documented way to inform my UI that a mutation is awaiting server response. I can see that earlier versions of the package would send a property indicating loading.
Queries still receive a loading property as documented here: http://dev.apollodata.com/react/queries.html#default-result-props
My application is also using redux, so I think one way to do it is to connect my component to redux and pass down a function property that will put my UI into a loading state. Then when rewriting my graphql mutation to a property, I can make calls to update the redux store.
Something roughly like this:
function Form({ handleSubmit, loading, handleChange, value }) {
return (
<form onSubmit={handleSubmit}>
<input
name="something"
value={value}
onChange={handleChange}
disabled={loading}
/>
<button type="submit" disabled={loading}>
{loading ? 'Loading...' : 'Submit'}
</button>
</form>
);
}
const withSubmit = graphql(
gql`
mutation submit($something : String) {
submit(something : $something) {
id
something
}
}
`,
{
props: ({ ownProps, mutate }) => ({
async handleSubmit() {
ownProps.setLoading(true);
try {
const result = await mutate();
} catch (err) {
// #todo handle error here
}
ownProps.setLoading(false);
},
}),
}
);
const withLoading = connect(
(state) => ({ loading: state.loading }),
(dispatch) => ({
setLoading(loading) {
dispatch(loadingAction(loading));
},
})
);
export default withLoading(withSubmit(Form));
I'm curious if there is a more idiomatic approach to informing the UI that the mutation is "in-flight." Thanks.

Anyone who stumbles across this question, since Apollo Client 2.1 you have access to those properties in the Query and Mutation component's render props function.
import React from "react";
import { Mutation } from "react-apollo";
import gql from "graphql-tag";
const TOGGLE_TODO = gql`
mutation ToggleTodo($id: Int!) {
toggleTodo(id: $id) {
id
completed
}
}
`;
const Todo = ({ id, text }) => (
<Mutation mutation={TOGGLE_TODO} variables={{ id }}>
{(toggleTodo, { loading, error, data }) => (
<div>
<p onClick={toggleTodo}>
{text}
</p>
{loading && <p>Loading...</p>}
{error && <p>Error :( Please try again</p>}
</div>
)}
</Mutation>
);
Note: Example code taken from the Apollo Client 2.1 release blog post.

I have re-posted this question on github and the suggested solution was to use something like a react higher order component just as you proposed in your original question. I did a similar thing – without using redux though – as outlined in this gist.
To cite Tom Coleman's response from the github issue:
It doesn't really make sense to include loading state on the mutation
container; if you think about it you could call the mutation twice
simultaneously -- which loading state should get passed down to the child? My
feeling is in general it's not nice to mix imperative (this.mutate(x, y, z))
with declarative (props) things; it leads to irresolvable inconsistencies.

Related

Next.js Fetching Json to Display in Components

I'm trying to take the function MainMenu and getStaticProps from being in the same page (index.js) and break it up into components. Here is the index.js page below that is working good.
#index.js
import Link from 'next/link';
function MainMenu({ menuLists }) {
return (
<div>
{menuLists.map(menuItem => (
<div>
<Link href={menuItem.absolute}><a>{menuItem.title}</a></Link>
{menuItem.below && menuItem.below.map(childItem => (
<div>
<Link href={childItem.absolute}><a>{childItem.title}</a></Link>
</div>
))}
</div>
))}
</div>
)
}
export async function getStaticProps() {
const response = await fetch('http://localhost:8888/api/menu_items/main');
const menuLists = await response.json();
return {
props: {
menuLists: menuLists,
},
}
}
export default MainMenu
I have created fetch-mainmenu.js in a lib directory with the following code.
#fetch-mainmenu.js
export async function loadMainMenu() {
const response = await fetch('http://localhost:8888/api/menu_items/main')
const menuLists = await response.json()
return {
props: {
menuLists: menuLists,
},
}
}
I then created sidebar.js to show the menu system from the json file. The sidebar.js file is working because the hard coded menus are showing.
# sidebar.js
import Link from 'next/link'
import styles from './sidebar.module.css'
import { loadMainMenu } from '../lib/fetch-mainmenu'
export default function Sidebar( { menuLists } ) {
const menus = loadMainMenu()
return (
<nav className={styles.nav}>
<input className={styles.input} placeholder="Search..." />
<Link href="/">
<a>Home</a>
</Link>
<Link href="/about">
<a>About</a>
</Link>
<Link href="/contact">
<a>Contact</a>
</Link>
</nav>
)
}
Getting the following error "TypeError: Failed to fetch".
What is the best way of getting this done using components.
Solution
1. Prop Drilling
Easy. Just send down all the data from getStaticProps(). This is the safest bet at current stage but it may create some redundant props.
// I've omitted fetch().json() to ease the reading. just assume it's a proper code.
const MainMenuComponent = ({menuLists}) => {
return <div>{menuLists}</div>
}
const MainPage = ({menuLists}) => {
return <MainMenuComponent menuLists={menuLists} />
}
export async function getStaticProps() {
const req = await fetch('...');
return {
props: {
menuLists: req,
},
}
}
export default MainPage
2. React.useEffect
A React component can't have asynchronous code inside render code. It is pretty obvious in a class component but it's hard to tell in a functional component
// I've omitted fetch().json() to ease the reading. just assume it's a proper code.
// class component
class SampleComponent extends React.Component {
constructor(props) {
super(props);
this.state = { data: {} };
}
async getData() {
// ✅ this works
const data = await fetch('...');
// data has to be put in state because it's asynchronous.
this.setState({ ...this.state, data });
}
componentDidMount() {
this.getData();
}
render() {
// ❌ this can't happen here because render is synchronous
await fetch('...');
// use state to display asynchronous data.
return <h1>Hello, {JSON.stringify(this.state.data)}</h1>;
}
}
// functional component
function SampleComponent = () => {
// everything outside `useEffect, useLayoutEffect` is mostly assumed as render function.
// ❌ thus, this does not work here
await fetch('...');
const [data, setData] = useState({});
useEffect(async () => {
// everything inside here treated as componentDidMount()
// not the exact same thing though.
// ✅ this works!
setData(await fetch('...'))
}, []);
return <h1>Hello, {JSON.stringify(data)}</h1>
}
WARNING if there's getStaticProps inside your page, it means the component also has to be synchronous. If the rendered component changes its content in a very short time, in a fraction of second, then it may get rehydration error. It needs to be wrapped with dynamic() so that the Next.js can ignore the component when rendering server-side & rehydrating the component. Please refer to Next.js official document on Dynamic Import.
It does work but the code seems long.
3. TanStack Query(or React-Query) or useSWR
There are nice 3rd party libraries that help writing asynchronous data fetching code inside a react component; TanStack Query and SWR are the most well known. These libraries also implement caching and revalidation. It can help handling complex issues invoked due to asynchronous requests.
// example code from useSWR
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
4. State Management with Context
Most cases are easily dealt with the Query-SWR solution but if the app gets big enough, there could be a need to synchronize the data.
In that case, fetch the data in server code and share the data with a central state management library(a.k.a store libs). A good example is this github repo of Zustand + Next.js. A bare React.Context can be used as well.
However, this method can get very complicated later, maybe not suitable for an inexperienced team; it's basically similar to building another complex layer as big as backend. That's why the trend has moved to Query-SWR solution these days. Still, this comes handy in certain cases.
import { useStore } from "../lib/store";
const SampleComponent = () => {
const { data } = useStore();
return <div>{JSON.stringify(data)}</div>
}
const MainPage() {
return <SampleComponent />
}
export async function getStaticProps() {
// refer to the github repo for this store structure
const zustandStore = initializeStore();
// this is a POC. the actual code could be different.
// store data is updated, and can be used globally in other components in a synchronized state.
const data = await useStore.setData(await fetch('...'));
return {
props: {
initialZustandState: JSON.parse(JSON.stringify({ ...zustandStore.getState(), data })),
},
};
}
5. Server-side Component
With the emergence of React 18 server side component, Next.js is also working on Next.js Server Components.
This implementation is probably the closest implementation to code from the question. Nevertheless, the work is still in progress and highly unstable.
I've kept my eyes on this method for about a year but the implementation has been constantly changing. Until we get the stable release, this can wait.

How do I 'reset' a React component after a Mutation error with react-apollo?

I'm learning Apollo Server and Apollo Client and following some online examples. One paradigm I've seen is for apollo-react mutations to do something like this following
render() {
const {props: {history}} = this
const {state: {username, password}} = this
return <Mutation mutation={REGISTER}
onCompleted={() => history.push('/login')}>
{(register, {data, error, loading}) => {
if (error) {
return <div>{error.toString()}</div>
}
if (loading) return <div>Loading...</div>
return ({JSX form, etc})
It works fine, and when I get an error -- in my case with a duplicate registration, for example -- I get the error message displayed instead of the normal JSX. I'm stumped what to do then to 'reset' the page. That is, I'd like to be able to take some action which would 'clear' the Mutation's error and have it go back to rendering the JSX as it did before the error arrived (either the user could take the action or I'd time the error out and then 'reset' the page). If I refresh the page it works fine, but obviously I don't want to have to do that!
I've done some searching and immediately I find myself deep in discussions of the Apollo Client Cache, which is next on my list to learn. For the moment, I'm wondering whether there is a simpler way to do what I'm trying to do.
Any clues much appreciated!
The simplest solution might be to use the React component's state to hold the error returned from the mutation. And then provide the user action to clear it.
Expanding on you example, it might look something like this:
render() {
const {props: {history}} = this
const {state: {username, password, registrationError}} = this
if (registrationError) {
return (
<div>
{registrationError.toString()}
<button onClick={() => this.setState({registrationError: null})}>
Clear Error
</button>
</div>
);
}
return <Mutation mutation={REGISTER}
onCompleted={() => history.push('/login')}
onError={(error) => this.setState({registrationError: error})}>
{(register, {data, error, loading}) => {
if (error) {
// Just in case this is hit, return null
return null;
}
if (loading) return <div>Loading...</div>
return ({JSX form, etc})
Notice the error rendering was moved out of the Mutation callback.
I'm not entirely sure

Subscribing to a redux action in a react component

I have an async thunk that fetches some information from a web service, it can dispatch three types of actions
FETCH_REQUESTED
FETCH_SUCCEEDED
FETCH_FAILED
Finally, if it's succeeded; it returns the actual response, or an error object.
I have a component that should detect whether the operation has failed or not, preferably by subscribing to the FETCH_FAILED action and displaying an error message based on the type of the error (404/401 and other status codes)
export const fetchData = () => {
return async (dispatch, getState) => {
const appState = getState();
const { uid } = appState.appReducer;
await dispatch(fetchRequested());
try {
const response = await LookupApiFactory().fetch({ uid });
dispatch(fetchSucceeded(response));
return response;
} catch (error) {
dispatch(fetchFailed());
return error;
}
}
}
I'm quite new to redux and react, so I'm a bit unsure if I'm heading in the right direction, any help would be appreciated.
To implement a proper redux call back and storage mechanism you should have a store to keep all your data,
const store = createStore(todos, ['Use Redux'])
then, you dispatch data to store,
store.dispatch({
type: 'FETCH_FAILED',
text: reposnse.status //Here you should give the failed response from api
});
Then you can get the value from the store in any of your components using a subscribe function. It will be called any time an action is dispatched, and some part of the state tree may potentially have changed.
store.subscribe(()=>{
store.getState().some.deep.property
})
This is a simple implementation of Redux. As your app grows more complex, you'll want to split your reducing function into separate functions, each managing independent parts of the state using combineReducers. You can get more information from redux.js site
The most common approach is to use connect function from react-redux library. This is a HoC which subscribes to state changes. Take a look at this library, additionally it allows you to bind your action creators to dispatch, what gives you an ability to dispatch your actions from component.
You can use it like this:
import React from 'react';
import { connect } from 'react-redux';
const MyComponent = ({ data, error }) => (
<div>
{error && (
<span>Error occured: {error}</span>
)}
{!error && (
<pre>{JSON.stringify(data, null, 2)}</pre>
)}
</div>
);
const mapStateToProps = (state) => ({
data: state.appReducer.data,
error: state.appReducer.error
});
export default connect(mapStateToProps)(MyComponent);
You can use conditional rendering inside your jsx as I've shown above, or use guard clause, like this:
const MyComponent = ({ data, error }) => {
if (error) {
return (
<span>Error occured: {error}</span>
);
}
return (
<pre>
{JSON.stringify(data, null, 2)}
</pre>
);
}
Assuming reducers,
for FETCH_FAILED action,you can put some meaningful flag indicating
there are some failure.Based on that flag you can show error messages or do other action.
const testReducers =(state,actione)=>{
case 'FETCH_FAILED' : {
return {
...state,{ error_in_response : true }
}
};
default : return state;
}
In your container,you can get that flag and passed it to your component.
Assuming combineReducers used to combine reducers;
const mapStateToProps=(state)=>{
return {
error_in_response : state.testReducers.error_in_response
}
}
connect(mapStateToProps)(yourComponent)
In your component, this can be accessed using this.props.error_in_response

Apollo and React: refreshQueries not working in mutation fired from onClick()

I'm building a simple shopping app. On load, we'll make a query to check whether a cart exists. For now, that query always returns null (I haven't implemented logic yet to check whether a user has a shopping cart already). Then, when a user clicks "create cart", we do a mutation to create it. Finally, we'll use refreshQueries after the mutation finishes to fetch the cart (and its products) by ID, which is returned from the mutation. We then render those products in a pure component.
The problem I'm encountering is that nothing re-renders after users click the button and refreshQueries happens. I know the query is being sent and returning a created cart with products by looking at my developer tools' network tab. It's just that Apollo doesn't seem to notice the change.
Mongo is my back-end.
Here's the relevant code:
// query
import { gql } from 'react-apollo';
export const cartQuery = gql`
query CartQuery($cartId: ID) {
cart(cartId: $cartId) {
_id,
products {
_id,
name
}
}
}
`;
// mutation
import { gql } from 'react-apollo';
export const createCartMutation = gql`
mutation CreateCartMutation {
createCart {
_id
}
}
`;
// Apollo + React stuff
import React from 'react';
import { graphql } from 'react-apollo';
import { createCartMutation } from '../../mutations';
import { cartQuery } from '../../queries';
const BuildCart = ({ mutate }) => (
<button
onClick={() => {
// even when I hardcode the ID of a cart that exists and contains products, it doesn't matter
mutate({
refetchQueries: [{ query: cartQuery, variables: { cartId: '12345abcd' } }],
});
}}
>
Click
</button>
);
const BuildCartConnected = graphql(createCartMutation)(BuildCart);
const Cart = ({ data, data: { cart, loading } }) => {
console.log(cart); // always null, even after clicking button
return (
<div>
<BuildCartConnected />
{loading && <p>Loading...</p>}
// never gets to this step, even though the response is a properly formed cart with an array of products
{cart && cart.products.map(product => (
<p key={Math.random()}>{product.name}</p>
))}
</div>
);
};
const CartConnected = graphql(cartQuery)(Cart);
export default CartConnected;
If it's helpful, here's what the response from refetchQueries looks like in the network tab:
{"data":{"cart":{"_id":"12345abcd","products":[{"_id":"abcdef12345","name":"Product A","__typename":"Product"}],"__typename":"Cart"}}}
Your cart query takes a cartId variable. In the code above, you're not providing that variable, so its value is undefined. Apollo associates that combination of query plus variable(s) with your Cart component.
When you call refetch queries with a different variable than what is provided to the HOC, the results of that new query are fetched and persisted in the store. However, as far as Apollo knows, you still want the results of a query with undefined as the variable value for that component, not this new result you had it fetch.
refetchQueries shouldn't be utilized for what you're trying to do. Instead, the variable for the cart query should be derived from props:
const options = ({cartId}) => ({variables: {cartId}})
const CartConnected = graphql(cartQuery, {options})(Cart)
Then, when you call mutate, it returns a Promise that will resolve to the data returned by the mutation. We can grab the cartId from the response and store it within application state (by calling setState, firing off a Redux action, etc.). You can then pass that state down to your Cart component as a prop. When the state is updated, the prop changes and the query with the newly provided variable is fetched.

Simple - React and OData

I'm completely new to React and having a hard time understanding it.
I've been tasked with creating a really simple API fetch to an OData endpoint.
Now, I've come across this library https://www.npmjs.com/package/react-odata
Which looks fantastic! However I just do not understand how to even get something like this working.
I understand the very basic principles of how react works and have gone through many basic tutorials. But for whatever reason I can not get my head around this one.
So how could I use this library to simply query an OData endpoint and display the raw data?
So the issue with this, is that I didn't understand that I still have to explicitly make the call and return the data from that.
import React, { Component } from 'react';
import Fetch from 'react-fetch-component';
import OData from 'react-odata';
const baseUrl = 'http://services.odata.org/V4/TripPinService/People';
const query = { filter: { FirstName: 'Russell' } };
export default class App extends Component {
render() {
return (
<div>
<h1>Basic</h1>
<OData baseUrl={baseUrl} query={query}>
{({ loading, data, error }) => (
<div>
{loading && <span>Loading... (()=>{console.log(loading)}) </span>}
{data && data.value.map((d, i) => <div key={i} id={i}>{d.FirstName}</div>)}
</div>
)}
</OData>
</div>
);
}
/* Setup consistent fetch responses */
componentWillMount() {
fetch('http://services.odata.org/V4/TripPinService/People')
.then((response) => response.json())
.then((responseJson) => {
return responseJson.value[0].FirstName
})
.catch((error) => {console.error(error)});
}
}
from the given link in the question I found that this component used the react-fetch-component as a base to make the call.
It seems that the package you linked would expose a React component that you would use to wrap your own components so you would have access to the fetched data and could pass it down as properties. At least that is what I understand from its README.
I imagine it would be something like this:
<OData baseUrl={baseUrl} query={query}>
{ ({ loading, error, data }) => (
<div>
<YourComponent data={data} />
</div>
)}
</OData>
This would be using react-odata, but you don't need that package to do what you want. You could just do a regular AJAX call on the URL and feed your components with the returned data.
This post may help: http://andrewhfarmer.com/react-ajax-best-practices/

Resources