Apollo + React: data not appearing in componentDidMount lifecycle - reactjs

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.

Related

Show ApolloClient mutation result

This is my first time using Apollo and React so I'll try my best.
I have a GraphQl API from which I consume some data through ApolloClient mutations. The problem is that I don't know how to show the resulting information outside of the .result. I've tried to do so with a class that has a function to consume some data and a render to show it.
The mutation works and shows the data on the console but the page remains blank when the page is loaded, so the problem I've been stuck on is, how do I show this data?
Btw, if there's any advice on how to insert data from a form using this same mutation method I'd pretty much appreciate it.
Thanks in advance.
import React, { useEffect, useState, Component } from 'react';
import { graphql } from 'react-apollo';
import './modalSignUp.css';
import{header} from './Header.js';
import ReactDOM from "react-dom";
import { ApolloProvider, Query, mutation } from "react-apollo";
import { ApolloClient, InMemoryCache, gql, useMutation } from '#apollo/client';
export const client = new ApolloClient({
uri: 'http://localhost:4011/api',
cache: new InMemoryCache(),
});
client.mutate({
mutation: gql`
mutation signin{
login(data:{
username:"elasdfg",
password:"12345678"}){
id,roles,email,username}
}
`
}).then(result => console.log(result));
export class UserList extends Component {
displayUsers() {
console.log(this.result)
var data = this.props.data;
return data.login.map((user) => {
return (
<li>{user.email}</li>
);
})
}
render() {
return (
<div>
<li>
{this.displayUsers()}
</li>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Header />);
Mutation result
I've tried to use a class to fetch the data given by the mutation and later render it in the component. I've also tried passing the result to a variable but I had no success with that.
I'm just expecting to see the data resulting from the mutation
You should request data inside the component and then save it to the state.
export class UserList extends Component {
constructor() {
this.state = {
newData: null,
};
this.mutateData = this.mutateData.bind(this);
}
mutateData() {
client
.mutate({
mutation: gql`
mutation signin {
login(data: { username: "elasdfg", password: "12345678" }) {
id
roles
email
username
}
}
`,
})
.then((result) => {
this.setState({ newData: result });
});
}
componentDidMount() {
this.mutateData();
}
render() {
// do something with new data
}
}

Is it possible to use redux and axios in react component library?

I am creating a page in React. Lets say for eg. "Conatct us" page. This whole component must be reusable. So that other teams can use it as it is. This component will have its own redux store and api calls using axios.
What I want to confirm that if I export this "Contact Us" module as npm package, will it work fine for other teams? Why I am asking this is because other teams project will have their own redux store and axios instance. And I think we can have only one redux store in an app and maybe one axios interceptors (I may be wrong about axios though)
Could anyone help me out, what can be done in this case? One thing is sure that I will have to export this whole component as npm package.
I'm going to answer here to give you more details:
Let's say your component looks like this:
AboutUs:
import React, { Component } from "react";
import PropTypes from "prop-types";
export class AboutUs extends Component {
componentDidMount() {
const { fetchData } = this.props;
fetchData();
}
render() {
const { data, loading, error } = this.props;
if (loading) return <p>Loading</p>;
if (error) return <p>{error}</p>;
return (
// whatever you want to do with the data prop that comes from the fetch.
)
}
}
AboutUs.defaultProps = {
error: null,
};
// Here you declare what is needed for your component to work.
AboutUs.propTypes = {
error: PropTypes.string,
data: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
}),
fetchData: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
};
This component just takes a few props in order to work and the fetchData function will be a dispatch of any redux action.
So in one of the apps that are going to use the component library, assuming that they have their own store, you could do something like this.
In the component where you're planning to use the AboutUs component.
import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
// this is the action that performs the data fetching flow.
import { fetchAboutUs } from "redux-modules/aboutUs/actions";
// The component that is above
import { AboutUs } from "your-component-library";
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
fetchData: fetchDashboard,
},
dispatch
);
};
const mapStateToProps = state => ({
loading: state.aboutUsReducer.loading,
error: state.aboutUsReducer.error,
data: state.aboutUsReducer.data,
});
const ReduxAboutUs = connect(
mapStateToProps,
mapDispatchToProps
)(AboutUs);
// Use your connected redux component in the app.
const SampleComponent = () => {
return <ReduxAboutUs />
}
This ensures that your component can work out of the box without redux, because you can explicitly use it without the redux dependency and just pass regular props and it will continue working. Also if you have different applications where you are going to use it you will have the control of which part of the store you want to use to inject the props for this component. Proptypes are quite useful here, because we're enforcing a few props in order let the devs what do we need to pass in order for the component to work properly.

onCompleted handler not firing with Apollo Client Query

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')
});

Attaching / Detaching Listeners in React

I have a component which, depending on its prop (listId) listens to a different document in a Firestore database.
However, when I update the component to use a new listId, it still uses the previous listener.
What's the correct way to detach the old listener and start a new one when the component receives new props?
Some code:
import React from 'react';
import PropTypes from 'prop-types';
import { db } from '../api/firebase';
class TodoList extends React.Component {
state = {
todos: [],
};
componentWillMount() {
const { listId } = this.props;
db.collection(`lists/${listId}/todos`).onSnapshot((doc) => {
const todos = [];
doc.forEach((t) => {
todos.push(t.data());
});
this.setState({ todos });
});
};
render() {
const { todos } = this.state;
return (
{todos.map(t => <li>{t.title}</li>)}
);
}
}
TodoList.propTypes = {
listId: PropTypes.object.isRequired,
};
export default TodoList;
I've tried using componentWillUnmount() but the component never actually unmounts, it just receives new props from the parent.
I suspect that I need something like getDerivedStateFromProps(), but I'm not sure how to handle attaching / detaching the listener correctly.
Passing a key prop to the TodoList lets the component behave as it should.

Is there a way to pass a dynamic GraphQL query to a graphql() decorated Component when using Apollo Client 2.0?

I encounter this issue sometimes when I am working with dynamic data. It's an issue with higher-order components which are mounted before the data they need is available.
I am looking to decorate a component with the graphql() HOC in Apollo Client, like this:
export default compose(
connect(),
graphql(QUERY_NAME), <-- I want QUERY_NAME to be determined at run-time
)(List)
The problem is I don't know how to get Apollo to use a query that is determined by the wrapped component at run-time.
I have a file that exports queries based on type:
import listFoo from './foo'
import listBar from './bar'
import listBaz from './baz'
export default {
foo,
bar,
baz,
}
I can access them by listQueries[type], but type is only known inside the component, and it is available as this.props.fromRouter.type.
Is there a strategy I can use to achieve:
export default compose(
connect(),
graphql(listQueries[type]),
)(List)
I think there might be a way to do it like this:
export default compose(
connect(),
graphql((props) => ({
query: listQueries[props.fromRouter.type],
})),
)(List)
Am I on the right track?
Another possible solution could be to make the Component generate its own sub-component that is wrapped with graphql() because the query would be known then.
For example:
const tableWithQuery = graphql(listQueries[props.fromRouter.type])((props) => {
return <Table list={props.data} />
})
I think I figured it out.
I have a Router Component that reads this.props.match.params to get the type of view and requested action.
With this information, I can create just one List, Create, Edit, and View Component and supply each with whatever queries are needed.
I created a function that gets all queries and mutations for a supplied type.
It was actually quite simple to just take the component such as <List /> and wrap it with graphql() and give it the correct query or mutation that was just determined.
Now, the components mount with this.props.data being populated with the correct data
I spread in all the queries and mutations just in case I need them. I suspect I will need them when I go to read this.props.data[listQueryName]. (which will grab the data in, for example, this.props.data.getAllPeople)
Here is the logic (I will include all of it, to minimize confusion of future searchers):
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { compose, graphql, withApollo } from 'react-apollo'
import listQueries from './list/queries'
import createMutations from './forms/create/mutations'
import editMutations from './forms/edit/mutations'
import viewQueries from './forms/view/queries'
import List from './list/List'
import Create from './forms/create/Create'
import Edit from './forms/edit/Edit'
import View from './forms/view/View'
// import Delete from './delete/Delete'
class Router extends Component {
constructor(props) {
super(props)
this.state = {
serverErrors: [],
}
}
getGraphQL = (type) => {
console.log('LIST QUERY', listQueries[type])
console.log('LIST QUERY NAME', listQueries[type].definitions[0].name.value)
console.log('CREATE MUTATION', createMutations[type])
console.log('CREATE MUTATION NAME', createMutations[type].definitions[0].name.value)
console.log('EDIT MUTATION', editMutations[type])
console.log('EDIT MUTATION NAME', editMutations[type].definitions[0].name.value)
console.log('VIEW QUERY', viewQueries[type])
console.log('VIEW QUERY NAME', viewQueries[type].definitions[0].name.value)
return {
listQuery: listQueries[type],
listQueryName: listQueries[type].definitions[0].name.value,
createMutation: createMutations[type],
createMutationName: createMutations[type].definitions[0].name.value,
editMutation: editMutations[type],
editMutationName: editMutations[type].definitions[0].name.value,
viewQuery: viewQueries[type],
viewQueryName: viewQueries[type].definitions[0].name.value,
}
}
renderComponentForAction = (params) => {
const { type, action } = params
const GQL = this.getGraphQL(type)
const {
listQuery, createMutation, editMutation, viewQuery,
} = GQL
// ADD QUERIES BASED ON URL
const ListWithGraphQL = graphql(listQuery)(List)
const CreateWithGraphQL = graphql(createMutation)(Create)
const EditWithGraphQL = compose(
graphql(viewQuery),
graphql(editMutation),
)(Edit)
const ViewWithGraphQL = graphql(viewQuery)(View)
if (!action) {
console.log('DEBUG: No action in URL, defaulting to ListView.')
return <ListWithGraphQL fromRouter={params} {...GQL} />
}
const componentFor = {
list: <ListWithGraphQL fromRouter={params} {...GQL} />,
create: <CreateWithGraphQL fromRouter={params} {...GQL} />,
edit: <EditWithGraphQL fromRouter={params} {...GQL} />,
view: <ViewWithGraphQL fromRouter={params} {...GQL} />,
// delete: <Delete fromRouter={params} {...GQL} />,
}
if (!componentFor[action]) {
console.log('DEBUG: No component found, defaulting to ListView.')
return <ListWithGraphQL fromRouter={params} {...GQL} />
}
return componentFor[action]
}
render() {
return this.renderComponentForAction(this.props.match.params)
}
}
Router.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({ type: PropTypes.string }),
}).isRequired,
}
export default compose(connect())(withApollo(Router))
If this code becomes useful for someone later. I recommend commenting everything out except code necessary to render the List View. Start with verifying the props are coming in to a little "hello world" view. Then, you will be done the hard part once you get correct data in there.

Resources