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

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.

Related

React Navigation passing props to class component

The react navigation documentation is very clear on how to pass props to functional components, but what about class-based components?
This is a screen that scans a QR code and passes the data to a class-based component
export const ScreenQRCodeScanner = ({ navigation }) => {
...
const handleBarCodeScanned = ({ type, data }) => {
setScanned(true);
alert(`Bar code with type ${type} and data ${data} has been scanned!`);
var clientID = {
ClientID: data,
};
navigation.navigate("QRCodeResult", clientID);
};
...
};
QRCodeResult is this function:
export class QRCodeResult extends Component {
constructor() {
super();
this.state = {
loading: false,
clientInfo: {},
};
}
componentDidMount() {
this.setState({ loading: true });
// API call with a dynamic body based on data passed from the previous screen (clientID)
}
render() {
return (
<SafeAreaView>
<Text>Test QR Code Result Page {this.state.clientInfo.FullName}</Text>
<Text>{this.state.loading}</Text>
</SafeAreaView>
);
}
}
I am using react navigation v5 and I want to use the data passed on componentDidMount() function where I make an API call
You can access the navigation prop like below
this.props.navigation.navigate("QRCodeResult", {clientID});
And if the receiving component is also a class component you can retrieve it like below.
this.props.route.params.clientID

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

Invoking functions on nested components in react redux

Sorry if this question displays a lack of understanding of the react paradigm which I suspect it does however I am very new to the tech.
I want to dispatch an action from my 'actions' code which should ultimately call a function on a nested component. Up to now I have been simply modifying state by dispatching actions and catching them in the reducer which works fine.
I am working on a portlet as part of a wider framework and I can capture an onExport() message in the actions. From here I have no idea of the best way to call the nested component (I need access to the inner ag-grid in the nested component to export it).
I have considered introducing some new 'exportRequested' state flag and setting this in the reducer then using componentDidReceiveProps in the nested component. I have also been studying the 'connect' idea and this seems right in so far as it would allow me to expose the function and connect it to the store but I can't seem to join the dots and figure out how to invoke it from the reducer. Is there some way to sort of dispatch an action and catch it directly in the nested component?
some code:
Container:
import {initData} from '/actions';
export class MainComponent extends PureComponent {
static propTypes = {
initData: func.isRequired,
data: array.isRequired,
};
static defaultProps = {
data: [],
};
componentDidMount() {
this.props.initData();
}
render() {
const { data } = this.props;
return (
<div>
<ChildGrid data={data} />
</div>
);
}
}
export default connect(
state => ({
data: getData(state),
}),
{ initData }
)(MainComponent);
Nested Grid:
export class ChildGrid extends PureComponent {
static propTypes = {
data: array.isRequired,
};
static defaultProps = {
data: [],
};
exportData() {
// HOW TO MESSAGE THIS FROM ACTIONS. I want to call DataGrid.gridApi.exportAsCsv()
}
render() {
const { data } = this.props;
return (
<div>
<DataGrid
rowData={data}
/>
</div>
);
}
}
You thought of the correct solution to your problem, by creating a state flag in your redux store then listening to the change of that property in your nested component. Unfortunately in Redux we can't listen to specific events or specific state property changes of the Redux store.
The implementation of such a solution is as follows:
ChildGrid.jsx
class ChildGrid extends PureComponent {
static propTypes = {
data: array.isRequired,
};
static defaultProps = {
data: [],
};
componentWillReceiveProps(newProps) {
if (newProps.exportRequested) {
this.exportData();
}
}
exportData() {
DataGrid.gridApi.exportAsCsv();
}
render() {
const { data } = this.props;
return (
<div>
<DataGrid
rowData={data}
/>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
reduxExportRequested: state.exportRequested
};
};
export default connect(mapStateToProps)(ChildGrid);

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

Callback onLoad for React-Apollo 2.1

I want to know what's the best way to handle setting a parent's state when the Apollo <Query> component finishes loading? I have an id that I sometimes have to query for. I wonder what's the best way to handle this case?
Currently I have it where the child component will listen for prop changes and if I notice that the prop for the data I'm looking for changes I'll call a function to update the state.
Is there a better way to handle this without needing the child component to listen to updates?
This is a pseudo code of what I'm currently doing
import * as React from 'react';
import { Query } from 'react-apollo';
class FolderFetcher extends React.Component<Props, { id: ?string}> {
constructor(props) {
super(props);
this.state = {
id: props.id
}
}
setId = (id) => {
this.setState({ id });
};
render() {
const { id } = this.props;
return (
<Query skip={...} query={...}>
((data) => {
<ChildComponent id={id} newId={data.id} setId={this.setId} />
})
</Query>
);
}
}
class ChildComponent extends React.Component<Props> {
componentDidUpdate(prevProps) {
if (prevProps.newId !== this.props.newId &&
this.props.id !== this.props.newId) {
this.props.setId(this.props.newId);
}
}
render() {
...
}
}
you can export child as wrapped with HoC from apollo
import { graphql } from 'react-apollo'
class ChildComponent extends React.Component {
// inside props you have now handy `this.props.data` where you can check for `this.props.data.loading == true | false`, initially it's true, so when you will assert for false you have check if the loading was finished.
}
export default graphql(Query)(ChildComponent)
Another option would be to get client manually, and run client.query() which will return promise and you can chain for the then() method.

Resources