onCompleted handler not firing with Apollo Client Query - reactjs

I'm having issues getting the onCompleted callback to fire when executing an Apollo Client query.
The query has no problems running, and it returns the results I would expect, but the onCompleted handler never fires. I have tried multiple things:
a) I have tried using HOC instead of the React component (see
comment at end of gist)
b) I've tried invalidating the cache and setting fetchPolicy to 'network-only'
I've tried setting the handler to "async"
There is an Github open issue related to what I'm experiencing, however the people in this thread only experience the problem when loading from cache. I'm experiencing the callback not firing all the time. https://github.com/apollographql/react-apollo/issues/2177
Here is a trimmed example of my code:
import React from 'react';
import { graphql, Query } from 'react-apollo';
import { ProductQuery } from '../../graphql/Products.graphql';
class EditProductVisualsPage extends React.Component {
constructor() {
super();
}
render() {
const { productId } = this.props;
return (
<Query
query={ProductQuery}
variables={{ id: productId }}
onCompleted={data => console.log("Hi World")}>
{({ loading, data: { product } }) => (
/* ... */
)}
</Query>
);
}
}
export default EditProductVisualsPage;
/*
export default graphql(ProductQuery, {
options: props => ({
variables: {
id: props.productId,
},
fetchPolicy: "cache-and-network",
onCompleted: function() {
debugger;
},
}),
})(EditProductVisualsPage);
*/
At this point I'm completely stumped. Any help would be appreciated.
Library versions
react-apollo (2.1.4)
apollo-client (2.3.1)
react(16.3.32)

(Answering this question since it has received a large number of views).
As of April, 2019, the onCompleted handler still remains broken. However, it can be worked around by utilizing the withApollo HOC. https://www.apollographql.com/docs/react/api/react-apollo#withApollo
Here is a sample integration:
import React from 'react';
import { withApollo } from 'react-apollo';
class Page extends React.Component {
constructor(props) {
super();
this.state = {
loading: true,
data: {},
};
this.fetchData(props);
}
async fetchData(props) {
const { client } = props;
const result = await client.query({
query: YOUR_QUERY_HERE, /* other options, e.g. variables: {} */
});
this.setState({
data: result.data,
loading: false,
});
}
render() {
const { data, loading } = this.state;
/*
... manipulate data and loading from state in here ...
*/
}
}
export default withApollo(Page);

Another solution, this worked for me. onCompleted is broken, but what you need is to call a function after X is completed. Just use good old .then(), here's an example for any newcomers to front-end.
fireFunction().then(() => {
console.log('fired')
});

Related

React - what are the steps to get data from api and render it?

I am building a site just like stackoverflow.com. I want my home page to display top questions. For that, I have sample questions on the backed. Now, I want to display only the question and tags from the questions array.
The code is in the image
I have made axios connection for that:
const instance = axios.create({
baseURL: "https://2w2knta9ag.execute-api.ap-south-1.amazonaws.com/dev", });
instance.defaults.headers.post["Content-Type"] = "application/json";
To connect it, I wrote the command: instance.get("/questions)
Now, how do I display only the question and tags??
EDIT:
On using the code given bellow, my js file now becomes:
import React from 'react';
import instance from '../../api';
class QuestionList extends React {
componentDidMount() {
instance
.get("/questions")
.then((res) => {
this.setState({ data: res.data });
});
}
render () {
const { data } = this.state;
return <div>
{
data && data.map(d => {
return <div>question: {d.question}, tags: {d.tags}</div>;
})
}
</div>
}
}
export default QuestionList;
But, this is just making my site in a loading state, and it gets hanged!!
If I understood correctly, you want to get an array only with the tags and the question. if so, you can use Array.prototype.map for this
const questions = result.map(({ question, tags }) => ({ question, tags }))
First you export the axios instance so that it can be used from other components.
Now you can send the api request in componentDidMount and update your component's state with the data.
And in render function, you just get the value from state and display.
If you are new to react, learn React Hooks and know that componentDidMount method is the best place to send api requests.
For Example:
import React from 'react';
import instance from '../../api';
class QuestionList extends React.Component {
constructor() {
super();
this.state = {
data: [],
};
}
componentDidMount() {
instance.get('/questions').then((res) => {
this.setState({ data: res.data });
});
}
render() {
const { data } = this.state;
return (
<div>
{data &&
data.map((d) => {
return (
<div>
question: {d.question}, tags: {d.tags}
</div>
);
})}
</div>
);
}
}
export default QuestionList;

Updating react context from consumer componentDidMount causes infinite re-renders

I'm trying out some state management in React using the Context API; what I want to achieve is that when I reach a specific route I load data from the server, store it in the context, and display it in the page itself. This is causing an infinite loop where the request to the server is done over and over (and never stops).
I'm trying to use higher order components for the provider and consumer logic:
import React, { Component, createContext } from 'react';
import RequestStatus from '../RequestStatus';
import { getData } from '../Api';
const dataCtx = createContext({
data: [],
getData: () => {},
requestStatus: RequestStatus.INACTIVE,
});
export default dataCtx;
export function dataContextProvider(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
getData: this.getData.bind(this),
requestStatus: RequestStatus.INACTIVE,
};
}
async getData() {
this.setState({ requestStatus: RequestStatus.RUNNING });
try {
const data = await getData();
this.setState({ data, requestStatus: RequestStatus.INACTIVE });
} catch (error) {
this.setState({ requestStatus: RequestStatus.FAILED });
}
}
render() {
return (
<dataCtx.Provider value={this.state}>
<WrappedComponent {...this.props} />
</dataCtx.Provider>
);
}
};
}
export function dataContextConsumer(WrappedComponent) {
return function component(props) {
return (
<dataCtx.Consumer>
{dataContext => <WrappedComponent dataCtx={dataContext} {...props} />}
</dataCtx.Consumer>
);
};
}
the provider is the App component itself:
import React, { Fragment } from 'react';
import { dataContextProvider } from './contexts/DataContext';
import { userContextProvider } from './contexts/UserContext';
import AppRoutes from './AppRoutes';
function App() {
return (
<Fragment>
<main>
<AppRoutes />
</main>
</Fragment>
);
}
export default userContextProvider(dataContextProvider(App));
and here's the consumer that causes the loop:
import React, { Component } from 'react';
import RequestStatus from './RequestStatus';
import { dataContextConsumer } from './contexts/DataContext';
class DataList extends Component {
async componentDidMount() {
const { dataCtx: { getData } } = this.props;
await getData();
}
render() {
const { dataCtx: { data, requestStatus } } = this.props;
return (
{/* display the data here */}
);
}
}
export default dataContextConsumer(DataList);
I've tried switching away from the HOC for the consumer, but it didn't help:
import React, { Component } from 'react';
import RequestStatus from './RequestStatus';
import dataCtx from './contexts/DataContext';
class DataList extends Component {
async componentDidMount() {
const { getData } = this.context;
await getData();
}
render() {
const { data, requestStatus } = this.context;
return (
{/* display the data here */}
);
}
}
DataList.contextType = dataCtx;
export default DataList;
The DataList is only one of the pages from where I'd like to trigger a context update.
I'm guessing that the Provider is causing a re-render of the whole App, but why? Where am I going wrong, and how can I fix this?
Ok, after trying to replicate the problem in a sandbox I realized what the problem was: I was wrapping a parent component in a HOC inside a render function, like so:
<Route exact path="/datapage" component={requireLoggedInUser(Page)} />
which forced the DataList component to be destroyed + recreated every time the App re-rendered.
the request loop happens because the DataList component gets re-rendered, calling ComponentDidMount, which calls getData() after each render.
A component renders if there is a change to the props or state of the component.
getData() sets the state property requestStatus (which is why your whole app gets re-rendered) which is a prop of DataList - causing a re-render of DataList.
you should not use requestStatus as a prop of DataList as you are getting that from the context anyway.
This could be because of the fact that your provider (dataContextProvider) level function getData has the same namespace as your function that you are importing from ../Api.
And then I believe that when the following line const data = await getData(); runs within the code block below, it actually calls the providers getData function, thus causing a loop.
async getData() {
this.setState({ requestStatus: RequestStatus.RUNNING });
try {
const data = await getData();
this.setState({ data, requestStatus: RequestStatus.INACTIVE });
} catch (error) {
this.setState({ requestStatus: RequestStatus.FAILED });
}
}

Testing Loading state in async operation in Jest

I have the following React component, that on componentDidMount() performs an async operation and once data is received, updates the state with the result.
import * as React from "react";
export interface IAppProp {
App: any
}
export interface IAppProp {
App: any
}
export class App extends React.Component<IAppProp, IAppState> {
constructor(props: IAppProp) {
super(props);
this.state = { App: undefined };
}
public componentDidMount(){
// Some async operation
// Once data comes in update state as follows
this.setState({ App: data returned from async operation });
}
public render() {
if (this.state && this.state.App) {
return (
<SomeComponent />
)
} else {
return <span>Loading...</span>
}
}
}
However, while I wait for data to come back, I return a Loading... message in the render() function.
I'm trying to test this Loading state in my Jest test. This is what I have so far:
it("Should display loading when state is undefined", () => {
const inputControl = enzyme.mount(<MyApp App={pass in the right prop} />);
expect(inputControl.find("span").text()).toEqual("Loading...");
});
I know the above is wrong because it never finds the Loading span. I also tried passing undefined in the props but that crashes the test because the async operation in componentDidMount() expects a legit prop.
How can I test this? Thanks!
Here is a working example based on your code (modified since I don't have SomeComponent, the async function wasn't specified, etc.).
Given this component defined in App.tsx:
import * as React from "react";
const getMessageById = (id: number): Promise<string> => {
return Promise.resolve('Message ' + id);
}
interface IAppProp {
messageid: number
}
interface IAppState {
message: string
};
export class App extends React.Component<IAppProp, IAppState> {
constructor(props: IAppProp) {
super(props);
this.state = { message: '' };
}
public componentDidMount(){
// Some async operation
// Once data comes in update state as follows
getMessageById(this.props.messageid).then((message) => {
this.setState({ message });
});
}
public render() {
if (this.state && this.state.message && this.state.message.length > 0) {
return (
<div>The message: {this.state.message}</div>
)
} else {
return <span>Loading...</span>
}
}
}
A test App.test.tsx can be created as follows:
import { mount } from 'enzyme';
import * as React from 'react';
import { App } from './App';
describe('App', () => {
it ('Should display loading until data arrives', async () => {
const inputControl = mount(<App messageid={1} />);
expect(inputControl.html()).toBe('<span>Loading...</span>');
// Let the event loop cycle so the callback queued by 'then'
// in 'componentDidMount()' has a chance to execute
await Promise.resolve();
expect(inputControl.html()).toBe('<div>The message: Message 1</div>');
});
});
For your actual component you will likely need to mock the async function that is getting the data (so you avoid making network requests, etc.), but this should provide a solid start for what you are trying to do and is the best I can do with the information provided.
Sorry for necroing (just a little)
I find that it's hard to test everything from business logic to rendering all in 1 go.
So I split the tests to:
Testing side effects, i.e. prop/state changes, from user interactions and/or async operations
Testing snapshots of exact prop/state combinations that I expect to receive.

Apollo + React: data not appearing in componentDidMount lifecycle

I've got a React app that uses Redux for some in-app state management and Apollo for fetching data from a server. In my network tab, my graphql queries are succeeding and the response is what I expect, but when I try to reference the data in the componentDidMount lifecycle of the React Component, the data isn't there and the loading state is 'true'.
If I move my code to a different lifecycle function, like render(), the data does appear, but I need it to work in componentDidMount. I'm new to Apollo.
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import SdkMap from "#boundlessgeo/sdk/components/map";
import SdkZoomControl from "#boundlessgeo/sdk/components/map/zoom-control";
import * as mapActions from "#boundlessgeo/sdk/actions/map";
import { graphql } from "react-apollo";
import gql from "graphql-tag";
function mapStateToProps(state) {
return {
map: state.map
};
}
class Map extends Component {
static contextTypes = {
store: PropTypes.object.isRequired
};
componentDidMount() {
const store = this.context.store;
store.dispatch(mapActions.setView([-95.7129, 37.0902], 3));
/* ADD SITES LAYER */
store.dispatch(
mapActions.addSource("sites_src", {
type: "geojson",
data: {
type: "FeatureCollection",
features: []
}
})
);
store.dispatch(
mapActions.addLayer({
id: "sites",
source: "sites_src",
type: "circle",
paint: {
"circle-radius": 3,
"circle-color": "blue",
"circle-stroke-color": "white"
}
})
);
console.log(this.props.data); //response doesn't include query fields
if (this.props.data.allSites) {
let sites = this.props.data.allSites.edges;
for (let i = 0; i < sites.length; i++) {
let site = sites[i].node;
let geojson = site.geojson;
if (geojson) {
console.log(site);
const feature = {
type: "Feature",
geometry: geojson,
properties: {
id: site.id
}
};
store.dispatch(mapActions.addFeatures("sites_src", feature));
}
}
}
}
render() {
const store = this.context.store;
return (
<SdkMap store={store} >
<SdkZoomControl />
</SdkMap>
);
}
}
const query = graphql(
gql`
query {
allSites {
edges {
node {
id
projectId
location
areaAcres
geojson
}
}
}
}
`
);
const MapWithRedux = connect(mapStateToProps)(Map);
const MapWithApollo = query(MapWithRedux);
export default MapWithApollo;
First of all there is no need to access this.context by yourself. This is an anti-pattern. Always use connect(). If you need parts of your state in your component use mapStateToProps. If you want to dispatch actions from your component use mapDispatchToProps to pass functions into it that do the dispatching for you. This is the second parameter that connect() accepts.
Also there is no reason to pass down the store to child components because you can connect every component individually that needs anything from the store.
That being said your problem is that fetching data is asynchronous and your request is probably not completed when componentDidMount() is called. So the information that loading is true just means, that your fetch did not finish yet. Either you display that to the user by e.g. showing some kind of spinner or you fetch the required data before you render your component.

react-apollo - how to set query variables from wrapped component?

react-apollo provides the ability to convert component props to query variables:
<MyComponentWithData propsForQueryVariables={...} />
But I need start query with variables from wrapped component.
Something like:
class MyComponent extends React.Component {
//...
onReady() {
// should be first request to server
this.props.refetch({
// variables here
})
}
onFilterChanged() {
this.props.refetch({
// new variables here
})
}
}
const MyComponentWithData = graphql(QUERY, {
options: {
waitUntilComponentStartQuery: true
// pollInterval:...
},
props({ data: { items, refetch } }) {
return {
items: items,
refetch: refetch
};
}
})(MyComponent);
UPDATE
QUERY for MyComponent looks like
query getItems($filter: JSON!) {
items(filter: $filter) {
id
name
}
}
filter is not nullable. So the first request should have valid variable filter, and this variable should be created in wrapped component.
You can pass the parent props to the variables of the initial fetch in the graphql HoC, like this:
ParentComponent.jsx
import ChildComponent from './ChildComponent';
const ParentComponent = () => <ChildComponent filterPropValue="myDefaultFilterValue" />;
export default ParentComponent;
ChildComponent.jsx
class ChildComponent extends React.Component {
refresh() {
this.props.refetch({
filter: 'mynewFilterValue'
});
}
render() {
return (
<div>I am a child component with {this.props.items.length} items.</div>
);
}
}
export default graphql(MyQuery, {
options: (props) => ({
variables: {
filter: props.filterPropValue
}
}),
props: ({ data: { items, error, refetch }) => ({
items,
error,
refetch
})
})(ChildComponent);
Any subsequent refetches with new parameters can then be dealt with via refetch().
refetch accepts a variables object as argument, see the documentation.

Resources