Apollo mutations without React <Mutation> component - react-apollo

Apollo's <Mutation> component often works well, but sometimes you need to call mutations outside of the render() method.
In some cases you can simply pass along the mutation function like so:
import React, { Component } from "react";
import { DO_MUTATION } from "./mutations";
import { Mutation } from "react-apollo";
export default class MyComponent extends Component {
render() {
return (
<Mutation mutation={DO_MUTATION}>
{(doMutation) => (
<Button
onPress={() => {
this.handleSomething(doMutation);
}}
/>
)}
</Mutation>
);
}
handleSomething = (doMutation) => {
/* DO SOME STUFF */
doMutation();
};
}
But in other cases this is not a very reasonable option, for example:
import React, { Component } from "react";
import { DO_MUTATION } from "./mutations";
import { Mutation } from "react-apollo";
import SomeLibrary from "SomeLibrary";
export default class MyComponent extends Component {
render() {
return (
<Mutation mutation={DO_MUTATION}>
{(doMutation) => (
<Button
onPress={() => {
SomeLibrary.addListener(this.listenerHandler);
}}
/>
)}
</Mutation>
);
}
listenerHandler = () => {
/* HOW DO I DO MUTATIONS HERE? */
};
}
How can mutations be performed in these scenarios?

Update for React Hooks (2020-12-18):
If you are using Apollo v3+ and functional React components, there is now a much cleaner solution using the useMutation() hook provided by Apollo:
import React from "react";
import { useMutation } from "#apollo/client";
import SomeLibrary from "SomeLibrary";
import { DO_MUTATION } from "./mutations";
export default function MyComponent() {
const [doMutation, { data }] = useMutation(DO_MUTATION);
let listenerHandler = () => {
doMutation({
variables: {
some_var: "some_val",
},
});
};
return (
<button
onPress={() => {
SomeLibrary.addListener(listenerHandler);
}}
/>
);
}
Also, the official docs say:
The useMutation React hook is the primary API for executing mutations
in an Apollo application.
Using hooks is now preferred over HOCs in Apollo, so it is probably a good idea to use useMutation() if you can.
You can read the documentation for useMutation at:
https://www.apollographql.com/docs/react/data/mutations/
Original answer:
react-apollo includes two HOCs called graphql() and withApollo() that can be used to accomplish this.
The difference between the two is described in Apollo's documentation as:
If you are wondering when to use withApollo() and when to use graphql() the answer is that most of the time you will want to use graphql(). graphql() provides many of the advanced features you need to work with your GraphQL data. You should only use withApollo() if you want the GraphQL client without any of the other features.
When graphql() is provided a mutation it will add a this.props.mutate() function and can be used like this:
import React, { Component } from "react";
import { DO_MUTATION } from "./mutations";
import { graphql } from "react-apollo";
import SomeLibrary from "SomeLibrary";
export class MyComponent extends Component {
render() {
return (
<Button
onPress={() => {
SomeLibrary.addListener(this.listenerHandler);
}}
/>
);
}
listenerHandler = () => {
this.props.mutate({
variables: {
some_var: "some_val",
},
});
};
}
export default graphql(DO_MUTATION)(MyComponent);
withApollo() is similar but instead provides a this.props.client for you to use directly. A mutation can be performed like this:
import React, { Component } from "react";
import { DO_MUTATION } from "./mutations";
import { withApollo } from "react-apollo";
import SomeLibrary from "SomeLibrary";
export class MyComponent extends Component {
render() {
return (
<Button
onPress={() => {
SomeLibrary.addListener(this.listenerHandler);
}}
/>
);
}
listenerHandler = () => {
this.props.client.mutate({
mutation: DO_MUTATION,
variables: {
some_var: "some_val",
},
});
};
}
export default withApollo(MyComponent);

Related

Apollo GraphQL failing connection

My root component is already wrapped with an ApolloProvider tag, but the error message tells me it is not.
Error Message
Invariant Violation: Could not find "client" in the context or passed in as an option. Wrap the root component in an <ApolloProvider>, or pass an ApolloClient instance in via options.
This error is located at:
in App (created by ExpoRoot)
Problem is my root component is already wrapped with an ApolloProvider tag
React Native Code
IMPORT statements
import {
ApolloClient,
InMemoryCache,
useQuery,
ApolloProvider,
gql,
} from "#apollo/client";
CONNECTION WITH GraphQL
const client = new ApolloClient({
uri: "https://www.outvite.me/gql/gql",
cache: new InMemoryCache(),
defaultOptions: { watchQuery: { fetchPolicy: 'cache-and-network' } },
})
TEST QUERY
const USER_QUERY = gql`
query USER {
users {
nodes {
edge {
username
}
}
}
}
`
DEFAULT APP
THIS IS WHERE THE ERROR IS BEING THROWN
const { data, loading } = useQuery(USER_QUERY) is the line that traceback shows
export default function App() {
const { data, loading } = useQuery(USER_QUERY)
return (
<ApolloProvider client={client}>
<View>
<Text style={styles.text}>Open</Text>
<Text style={styles.text}>Another text</Text>
</View>
<Button title="Toggle Sidebar" onPress={() => toggleSidebarView()} />
<Button title="Change theme" onPress={() => toggleColorTheme()} />
</ApolloProvider>
);
}
If I'm not mistaken, the useQuery hook only works if you're in a component that is already wrapped in the ApolloProvider so you probably want to do something like this
export default function MainApp() {
const { data, loading } = useQuery(USER_QUERY)
return (
<View>
... use 'data' in here somewhere...
</View>
);
}
and then the top-level App component would look like
export default function App() {
return (
<ApolloProvider client={client}>
<MainApp />
</ApolloProvider>
);
}

Vuex-ORM GraphQL installation troubles

I installed the Vuex-ORM Graphql Plugin into an existing Nuxt project with Laravel/GraphQL API, so that I could try avoiding using the Apollo Cache. In one of my components though, I'm running:
<script>
import Notification from '~/data/models/notification';
export default {
computed: {
notifications: () => Notification.all()
},
async mounted () {
await Notification.fetch();
}
}
</script>
however I'm receiving the error [vuex] unknown action type: entities/notifications/fetch.
I looked through the debug log and found several available getters (entities/notifications/query, entities/notifications/all, entities/notifications/find, and entities/notifications/findIn). I tried running await Notification.all() in the mounted method which removed the error, however looking in Vuex the Notifications data object is empty.
Here is the rest of my setup:
nuxt.config.js
plugins: [
'~/plugins/vuex-orm',
'~/plugins/graphql'
],
plugins/vuex-orm.js
import VuexORM from '#vuex-orm/core';
import database from '~/data/database';
export default ({ store }) => {
VuexORM.install(database)(store);
};
plugins/graphql.js
/* eslint-disable import/no-named-as-default-member */
import VuexORM from '#vuex-orm/core';
import VuexORMGraphQL from '#vuex-orm/plugin-graphql';
import { HttpLink } from 'apollo-link-http';
import fetch from 'node-fetch';
import CustomAdapter from '~/data/adapter';
import database from '~/data/database';
// The url can be anything, in this example we use the value from dotenv
export default function ({ app, env }) {
const apolloClient = app?.apolloProvider?.defaultClient;
const options = {
adapter: new CustomAdapter(),
database,
url: env.NUXT_ENV_BACKEND_API_URL,
debug: process.env.NODE_ENV !== 'production'
};
if (apolloClient) {
options.apolloClient = apolloClient;
} else {
options.link = new HttpLink({ uri: options.url, fetch });
}
VuexORM.use(VuexORMGraphQL, options);
};
/data/adapter.js
import { DefaultAdapter, ConnectionMode, ArgumentMode } from '#vuex-orm/plugin-graphql';
export default class CustomAdapter extends DefaultAdapter {
getConnectionMode () {
return ConnectionMode.PLAIN;
}
getArgumentMode () {
return ArgumentMode.LIST;
}
};
/data/database.js
import { Database } from '#vuex-orm/core';
// import models
import Notification from '~/data/models/notification';
import User from '~/data/models/user';
const database = new Database();
database.register(User);
database.register(Notification);
export default database;
/data/models/user.js
import { Model } from '#vuex-orm/core';
import Notification from './notification';
export default class User extends Model {
static entity = 'users';
static eagerLoad = ['notifications'];
static fields () {
return {
id: this.attr(null),
email: this.string(''),
first_name: this.string(''),
last_name: this.string(''),
// relationships
notifications: this.hasMany(Notification, 'user_id')
};
}
};
/data/models/notification.js
import { Model } from '#vuex-orm/core';
import User from './user';
export default class Notification extends Model {
static entity = 'notifications';
static fields () {
return {
id: this.attr(null),
message: this.string(''),
viewed: this.boolean(false),
// relationships
user: this.belongsTo(User, 'user_id')
};
}
};
package.json
"#vuex-orm/plugin-graphql": "^1.0.0-rc.41"
So in a Hail Mary throw to get this working, I ended up making a couple of changes that actually worked!
If other people come across this having similar issues, here's what I did...
In my nuxt.config.js, swapped the order of the two plugins to this:
plugins: [
'~/plugins/graphql',
'~/plugins/vuex-orm',
],
In my graphql.js plugin, I rearranged the order of the options to this (database first, followed by adapter):
const options = {
database,
adapter: new CustomAdapter(),
url: env.NUXT_ENV_BACKEND_API_URL,
debug: process.env.NODE_ENV !== 'production'
};

React Redux can't display an array that is a property of an object. What am I doing wrong?

I'm trying to display an object that's passed as props. One of the object properties is an array. The array is seen in the Redux store, is seen on console.log, is seen in the React tools, but when I try to map over it and display it as a list I get TypeError: Cannot read property 'map' of undefined. What am I doing wrong?
I tried to pass the tickets array as a separate prop but I still get the same error. And all the other properties of this.props.event are accessible.
This is my rendering component:
render(){
return(
<div>
{console.log('New EventDetails props event ', this.props.event)}
{console.log('New EventDetails props tickets ', this.props.tickets)}
<h1>Event name: {this.props.event.name}</h1>
<i>{this.props.event.id}</i>
<p>Event description: {this.props.event.description}</p>
<ul><h3>Tickets</h3>
{this.props.event.tickets.map(ticket =>{
return <Link to={`${this.props.event.id}/tickets/${ticket.id}`}><li key={ticket.id}><p>Price: {ticket.price}</p>
<p>Description: {ticket.description}</p>
</li> </Link>
})}
</ul>
</div>
)
}
}
This is the reducer:
import {DISPLAY_EVENT} from '../actions/events'
const eventReducer = (state={}, action) => {
console.log("single event reducer test, actin.payload: ", action.payload) //shows correct payload
switch(action.type) {
case DISPLAY_EVENT:
return action.payload
default:
return state
}
}
I'm passing the props from another component:
import React from 'react'
import {connect} from 'react-redux'
import EventDetails from './EventDetails'
import {getEvent} from '../actions/events'
class EventDetailsContainer extends React.Component {
componentDidMount() {
console.log("Component Did Mount test");
console.log('EventDetailsContainer props:', this.props);
this.props.getEvent(Number(this.props.match.params.id))
}
render() {
return (
<div>
<EventDetails event={this.props.event} tickets={this.props.tickets}/>
</div>
)
}
}
const mapStateToProps = state => {
return {
event: state.event,
tickets:state.event.tickets
}
}
export default connect(mapStateToProps, {getEvent})(EventDetailsContainer)
This is what I get from the console.logs.
I expected that the event.tickets[] will be accessible just as the other properties but instead it gives this error.
you console.log this.props.tickets but then you map over this.props.event.tickets

ComponentWillMount gets called twice and render gets called twice. Also, render is being called before reducers finish. React and Redux

This is my console:
action: {type: "##redux/PROBE_UNKNOWN_ACTION_u.0.n.a.j.f"}
action: {type: "##redux/INIT2.4.j.c.2.m"}
in component will mount
inside the hangout_list render method
in component will mount
inside the hangout_list render method
Uncaught TypeError: Cannot read property 'map' of undefined
at HangoutList.render (bundle.js:22963)
at finishClassComponent (bundle.js:11048)
at updateClassComponent (bundle.js:11016)
at beginWork (bundle.js:11641)
at performUnitOfWork (bundle.js:14473)
at workLoop (bundle.js:14502)
at HTMLUnknownElement.callCallback (bundle.js:2759)
at Object.invokeGuardedCallbackDev (bundle.js:2797)
at invokeGuardedCallback (bundle.js:2846)
at replayUnitOfWork (bundle.js:13977)
...
bundle.js:12302 The above error occurred in the <HangoutList> component:
in HangoutList (created by Connect(HangoutList))
in Connect(HangoutList) (created by App)
in div (created by App)
in App
in Provider
...
action: {type: "FETCH_HANGOUTS", payload: {…}}
inside fetch hangouts in the reducer
action: {type: "FETCH_HANGOUTS", payload: {…}}
inside fetch hangouts in the reducer
As you can see, some console.logs are called twice and we have an undefined error which suggests some state data hasn't been set.
I have a react-redux app on localhost:8080 that uses ReduxPromise and is making an api call to localhost:3000 which succeeds... there data returns. It just never sets in time before the component tries to render. What can I do?
My code:
my main index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import ReduxPromise from 'redux-promise'
import App from './components/app';
import reducers from './reducers';
const createStoreWithMiddleware = applyMiddleware(ReduxPromise)(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<App />
</Provider>
, document.querySelector('.container'));
My action:
import axios from 'axios'
export const ROOT_URL = 'http://localhost:3000';
export const FETCH_HANGOUTS = 'FETCH_HANGOUTS';
export function fetchHangouts() {
const path = 'api/v1/hangouts'
const url = `${ROOT_URL}/${path}`;
const request = axios.get(url);
return {
type: FETCH_HANGOUTS,
payload: request
};
}
my App component:
import React, { Component } from 'react';
import HangoutList from '../containers/hangout_list'
export default class App extends Component {
render() {
return (
<div>
<HangoutList />
</div>
);
}
}
HangoutList container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fetchHangouts } from '../actions/index';
class HangoutList extends Component {
renderHangouts(hangoutData) {
const type = hangoutData.type;
const additional_info = hangoutData.additional_info;
const hangoutKey = hangoutData.id;
return (
<tr key={hangoutKey}>
<td> {type} </td>
<td> {additional_info} </td>
</tr>
)
}
componentWillMount() {
console.log("in component will mount");
this.props.fetchHangouts();
}
render() {
console.log("inside the hangout_list render method");
return (
<table className="table table-hover">
<thead>
<tr>
<th>Type</th>
<th>Details </th>
</tr>
</thead>
<tbody>
{this.props.hangouts.map(this.renderHangouts)}
</tbody>
</table>
)
}
}
function mapStateToProps({ hangouts }) { // es6 shorthand. It's the same as if state was the argument and { hangouts: state.hangouts } was in the return section.
return { hangouts };
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchHangouts }, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(HangoutList);
And finally my reducer:
import { FETCH_HANGOUTS } from "../actions/index";
export default function(state = [], action) {
console.log("action:", action);
switch (action.type) {
case FETCH_HANGOUTS:
// return state.concat([ action.payload.data ]); // don't use push. concat creates a new array, while push mutates the old one. YOu want to create a new array, not mutate the old one.
console.log("inside fetch hangouts in the reducer")
return action.payload.data
}
return state;
}
Anyone see what the issue is? I basically don't know why certain console.logs are running twice and why my api call (called in ComponentWillMount) won't finish before the container renders. I thought ReduxPromise was middleware that was supposed to handle this problem?

Update method in mutation not running

I have the following component that mutates data. Apollo provides functionality to update the store automatically. I would like to control the way the data is added to the store using the update function. The documentation is straightforward enough, but I can't get it working. What is wrong in the code below that would prevent the console.log from printing.
import React from 'react'
import { connect } from 'react-redux';
import { graphql, gql, compose } from 'react-apollo';
import { personCodeSelector } from '../../selectors/auth';
import UploadBankStatement from '../../components/eftFileUploads/UploadBankStatement.jsx';
const createEftFileUpload = gql`mutation createEftFileUpload(
$bankAccountCode: String!,
$uploadInput: UploadInput!,
$uploadedByPersonCode: String!) {
createEftFileUpload(
bankAccountCode: $bankAccountCode,
uploadInput: $uploadInput,
uploadedByPersonCode: $uploadedByPersonCode) {
id
bankAccountCode
fileName
numberOfProcessedItems
numberOfUnallocatedItems
createdAt
status
}
}`;
const mutationConfig = {
props: ({ ownProps, mutate }) => ({
createEftFileUpload: (bankAccountCode, uploadInput) => {
return mutate({
variables: {
bankAccountCode,
uploadInput,
uploadedByPersonCode: ownProps.personCode
},
update: (store, something) => {
console.log("ping");
console.log(store, something);
},
});
}
})
};
const mapStateToProps = state => {
return {
personCode: personCodeSelector(state)
};
};
export default compose(
connect(mapStateToProps),
graphql(createEftFileUpload, mutationConfig)
)(UploadBankStatement);
Note I have found a couple of similar issues, but it doesn't seem to shed any light on my situation.
Server restart fix my issue. Not sure why this was required with hot-reloading. The code was correct.

Resources