local state management for graphql vue apollo - graphql

I am trying to make local state management with vue apollo, but even after following the docs I am getting no result. There is no error in the console so I am not sure what is wrong.
Here is my setup:
// main.js file the initializing part
const client = new ApolloClient({
link: ApolloLink.from([
errorLink,
authMiddleware,
link,
]),
cache,
typeDefs,
resolvers,
connectToDevTools: true,
});
// resolvers file
import gql from 'graphql-tag';
import { todoItemsQuery } from './task.queries';
export const typeDefs = gql`
type Item {
id: ID!
text: String!
done: Boolean!
}
type Mutation {
changeItem(id: ID!): Boolean
deleteItem(id: ID!): Boolean
addItem(text: String!): Item
}
`;
export const resolvers = {
Mutation: {
checkItem: (_, { id }, { cache }) => {
const data = cache.readQuery({ query: todoItemsQuery });
console.log('data res', data);
const currentItem = data.todoItems.find(item => item.id === id);
currentItem.done = !currentItem.done;
cache.writeQuery({ query: todoItemsQuery, data });
return currentItem.done;
},
},
};
//queries file
import gql from 'graphql-tag';
export const todoItemsQuery = gql`
{
todoItems #client {
id
text
done
}
}
`;
export const checkItemMutation = gql`
mutation($id: ID!) {
checkItem(id: $id) #client
}
`;
// component where I call it
apollo: {
todoItems: {
query: todoItemsQuery
}
},
checkItem(id) {
this.$apollo
.mutate({
mutation: checkItemMutation,
variables: { id }
})
.then(({ data }) => {
console.log("CACHE", data);
});
},
I get empty todoItems, no errors.
Please let me know what am I missing, I am not grasping some concept I think, and if there is a way to use vuex with apollo then I can do that too.

Foreword: I'm not an Apollo specialist, just starting to use it.
Just in case, these thoughts may help you. If they don't, well, I'm sorry.
In main.js: Apollo documentation provides a slightly different set up when using Apollo Boost (https://www.apollographql.com/docs/link/links/state/#with-apollo-boost). Just in case, this is how I have set up my implementation so far.
import VueApollo from 'vue-apollo'
import ApolloClient from 'apollo-boost';
import { InMemoryCache } from 'apollo-cache-inmemory';
const cache = new InMemoryCache();
Vue.use(VueApollo)
const apolloClient = new ApolloClient({
//...whatever you may already have,
clientState: {
// "defaults" is your initial state - if empty, I think it might error out when your app launches but is not hydrated yet.
defaults: {
todoItems: [],
}
cache,
typeDefs: {...yourTypeDefs},
resolvers: {...yourResolvers},
},
});
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
});
In your typeDefs: I can not see a Query Type for your todoItems:
type Query {
todoItemsQuery: [Item]
}
In your component: my own implementation was not working until I added an update method to the apollo request:
apollo: {
todoItems: {
query: todoItemsQuery,
update: data => data.todoItems
}
},

Related

Apollo GraphQL react client: subscription

I was trying to make a little demo with GraphQL subscriptions and GraphQL Apollo client.
I already have my GraphQL API, but when I try to use Apollo client, it looks like it doesn't complete the websocket subscribe step:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery } from '#apollo/client';
import { split, HttpLink } from '#apollo/client';
import { getMainDefinition } from '#apollo/client/utilities';
import { GraphQLWsLink } from '#apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { useSubscription } from '#apollo/react-hooks'
import reportWebVitals from './reportWebVitals';
const httpLink = new HttpLink({
uri: 'https://mygraphql.api'
});
const wsLink = new GraphQLWsLink(createClient({
url: 'wss://mygraphql.api',
options: {
reconnect: true
}
}));
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
fetchOptions: {
mode: 'no-cors',
}
});
const FAMILIES_SUBSCRIPTION = gql`
subscription{
onFamilyCreated {
id
name
}
}
`;
function LastFamily() {
const { loading, error, data } = useSubscription(FAMILIES_SUBSCRIPTION, {
variables: { },
onData: data => console.log('new data', data)
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
console.log(data);
const family = data.onFamilyCreated[0];
return (
<div>
<h1>{family.name}</h1>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
(<ApolloProvider client={client}>
<div>
<LastFamily />
</div>
</ApolloProvider>));
reportWebVitals();
According to graphql-transport-ws, to accomplish a success call, it should call connection_init and subscribe message. But when I open Dev Tools, it only sends "connection_init"
I'm expecting this output:
What step should I add to accomplish a successful call using graphql-transport-ws?
P.s. I'm not a React Developer, just be kind.
The solutions I'm putting up are based on #apollo/server v.4, with expressMiddleware and mongodb/mongoose on the backend and subscribeToMore with updateQuery on the client-side instead of useSubscription hook. In light of my observations, I believe there may be some issues with your backend code that require refactoring. The transport library graphql-transport-ws has been deprecated and advise to use graphql-ws. The following setup also applies as of 12.2022.
Subscription on the backend
Install the following dependencies.
$ npm i #apollo/server #graphql-tools/schema graphql-subscriptions graphql-ws ws cors body-parser mongoose graphql express
Set up the db models, I will refer to mongodb using mongoose and it might look like this one e.g.
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const model = mongoose.model
const FamilySchema = new Schema({
name: {
type: String,
unique: true, //optional
trim: true,
}
})
FamilySchema.virtual('id').get(function () {
return this._id.toHexString()
})
FamilySchema.set('toJSON', {
virtuals: true,
transform: (document, retObj) => {
delete retObj.__v
},
})
const FamilyModel = model('FamilyModel', FamilySchema)
export default FamilyModel
Setup schema types & resolvers; it might look like this one e.g.
// typeDefs.js
const typeDefs = `#graphql
type Family {
id: ID!
name: String!
}
type Query {
families: [Family]!
family(familyId: ID!): Family!
}
type Mutation {
createFamily(name: String): Family
}
type Subscription {
familyCreated: Family
}
`
// resolvers.js
import { PubSub } from 'graphql-subscriptions'
import mongoose from 'mongoose'
import { GraphQLError } from 'graphql'
import FamilyModel from '../models/Family.js'
const pubsub = new PubSub()
const Family = FamilyModel
const resolvers = {
Query: {
families: async () => {
try {
const families = await Family.find({})
return families
} catch (error) {
console.error(error.message)
}
},
family: async (parent, args) => {
const family = await Family.findById(args.familyId)
return family
},
Mutation: {
createFamily: async (_, args) => {
const family = new Family({ ...args })
try {
const savedFamily = await family.save()
const createdFamily = {
id: savedFamily.id,
name: savedFamily.name
}
// resolvers for backend family subscription with object iterator FAMILY_ADDED
pubsub.publish('FAMILY_CREATED', { familyCreated: createdFamily })
return family
} catch (error) {
console.error(error.message)
}
}
},
Subscription: {
familyCreated: {
subscribe: () => pubsub.asyncIterator('FAMILY_CREATED'),
}
},
Family: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
name: async (parent) => {
return parent.name
}
}
}
export default resolvers
At the main entry server file (e.g. index.js) the code might look like this one e.g.
import dotenv from 'dotenv'
import { ApolloServer } from '#apollo/server'
import { expressMiddleware } from '#apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '#apollo/server/plugin/drainHttpServer'
import { makeExecutableSchema } from '#graphql-tools/schema'
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
import express from 'express'
import http from 'http'
import cors from 'cors'
import bodyParser from 'body-parser'
import typeDefs from './schema/tpeDefs.js'
import resolvers from './schema/resolvers.js'
import mongoose from 'mongoose'
dotenv.config()
...
mongoose.set('strictQuery', false)
let db_uri
if (process.env.NODE_ENV === 'development') {
db_uri = process.env.MONGO_DEV
}
mongoose.connect(db_uri).then(
() => {
console.log('Database connected')
},
(err) => {
console.log(err)
}
)
const startGraphQLServer = async () => {
const app = express()
const httpServer = http.createServer(app)
const schema = makeExecutableSchema({ typeDefs, resolvers })
const wsServer = new WebSocketServer({
server: httpServer,
path: '/',
})
const serverCleanup = useServer({ schema }, wsServer)
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose()
},
}
},
},
],
})
await server.start()
app.use(
'/',
cors(),
bodyParser.json(),
expressMiddleware(server)
)
const PORT = 4000
httpServer.listen(PORT, () =>
console.log(`Server is now running on http://localhost:${PORT}`)
)
}
startGraphQLServer()
Subscription on the CRA frontend
Install the following dependencies.
$ npm i #apollo/client graphql graphql-ws
General connection setup e.g.
// src/client.js
import { ApolloClient, HttpLink, InMemoryCache, split } from '#apollo/client'
import { getMainDefinition } from '#apollo/client/utilities'
import { defaultOptions } from './graphql/defaultOptions'
import { GraphQLWsLink } from '#apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
...
const baseUri = process.env.REACT_APP_BASE_URI // for the client
const wsBaseUri = process.env.REACT_APP_WS_BASE_URI // for the backend as websocket
const httpLink = new HttpLink({
uri: baseUri,
})
const wsLink = new GraphQLWsLink(
createClient({
url: wsBaseUri
})
)
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
})
export default client
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import client from './client'
import { ApolloProvider } from '#apollo/client'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
)
Define the operations types for the client: queries, mutation & subscription e.g.
// src/graphql.js
import { gql } from '#apollo/client'
// Queries
export const FAMILIES = gql`
query Families {
families {
id
name
}
}
`
export const FAMILY = gql`
query Family($familyId: ID) {
family {
id
name
}
}
`
// Mutation
export const CREATE_FAMILY = gql`
mutation createFamily($name: String!) {
createFamily(name: $name) {
id
name
}
}
`
// Subscription
export const FAMILY_SUBSCRIPTION = gql`
subscription {
familyCreated {
id
name
}
}
Components, it might look like this one e.g.
Apollo's useQuery hook provides us with access to a function called subscribeToMore. This function can be destructured and used to act on new data that comes in via subscription. This has the result of rendering our app real-time.
The subscribeToMore function utilizes a single object as an argument. This object requires configuration to listen for and respond to subscriptions.
At the very least, we must pass a subscription document to the document key in this object. This is a GraphQL document in which we define our subscription.
We can a updateQuery field that can be used to update the cache, similar to how we would do in a mutation.
// src/components/CreateFamilyForm.js
import { useMutation } from '#apollo/client'
import { CREATE_FAMILY, FAMILIES } from '../graphql'
...
const [createFamily, { error, loading, data }] = useMutation(CREATE_FAMILY, {
refetchQueries: [{ query: FAMILIES }], // be sure to refetchQueries after mutation
})
...
// src/components/FamilyList.js
import React, { useEffect, useState } from 'react'
import { useQuery } from '#apollo/client'
import { Families, FAMILY_SUBSCRIPTION } from '../graphql'
const { cloneDeep, orderBy } = pkg
...
export const FamilyList = () => {
const [families, setFamilies] = useState([])
const { loading, error, data, subscribeToMore } = useQuery(Families)
...
useEffect(() => {
if (data?.families) {
setFamilies(cloneDeep(data?.families)) // if you're using lodash but it can be also setFamilies(data?.families)
}
}, [data?.families])
useEffect(() => {
subscribeToMore({
document: FAMILY_SUBSCRIPTION,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev
const newFamily = subscriptionData.data.familyCreated
if (!prev.families.find((family) => family.id === newFamily.id)) {
return Object.assign({}, prev.families, {
families: [...prev.families, newFamily],
})
} else {
return prev
}
},
})
}, [subscribeToMore])
const sorted = orderBy(families, ['names'], ['desc']) // optional; order/sort the list
...
console.log(sorted)
// map the sorted on the return statement
return(...)
END. Hard-coding some of the default resolvers are useful for ensuring that the value that you expect will returned while avoiding the return of null values. Perhaps not in every case, but for fields that refer to other models or schema.
Happy coding!

#client Apollo GQL tag breaks query

I have a vue-apollo (using nuxt) query that is supposed to have a local client field show. However, when I have the show #client line included in the query the component does not render. For some reason it also seems to fail silently.
query myAccounts {
accounts: myAccounts {
email
calendars {
id
name
hex_color
is_enabled
show #client
}
}
}
I am extending the Calendar type in an extensions.js file (pasted below) with two mutations.
import gql from 'graphql-tag'
export const typeDefs = gql`
extend type Calendar {
show: Boolean
}
type Mutation {
showCalendar(id: ID!): Boolean
hideCalendar(id: ID!): Boolean
}
`
Here is the resolver that sets the value, along with the Apollo config:
import { InMemoryCache } from 'apollo-cache-inmemory'
import { typeDefs } from './extensions'
import MY_ACCOUNTS_QUERY from '~/apollo/queries/MyAccounts'
const cache = new InMemoryCache()
const resolvers = {
Mutation: {
showCalendar: (_, { id }, { cache }) => {
const data = cache.readQuery({ query: MY_ACCOUNTS_QUERY })
const found = data.accounts
.flatMap(({ calendars }) => calendars)
.find(({ id }) => id === '1842')
if (found) {
found.show = true
}
cache.writeQuery({ query: todoItemsQuery, data })
return true
}
}
}
export default context => {
return {
cache,
typeDefs,
resolvers,
httpLinkOptions: {
credentials: 'same-origin'
},
}
}
along with the nuxt config:
apollo: {
defaultOptions: {
$query: {
loadingKey: 'loading',
fetchPolicy: 'cache-and-network',
},
},
errorHandler: '~/plugins/apollo-error-handler.js',
clientConfigs: {
default: '~/apollo/apollo-config.js'
}
}
Querying local state requires the state to exist (i.e. it should be initialized) or for a local resolver to be defined for the field. Apollo will run the resolver first, or check the cache directly for the value if a resolver is not defined. There's not really a good way to initialize that value since it's nested inside a remote query, so you can add a resolver:
const resolvers = {
Calendar: {
show: (parent) => !!parent.show,
},
// the rest of your resolvers
}
See the docs for additional examples and more details.

Update local config based on query result in react-apollo

Using apollo and react, I want to update my local apollo state based on the result of a (remote) apollo query.
The query is of type [String!]!, my local state contains { name } of type String. I want to assign the local { name } to the first item returned by the query.
const App = compose(
graphql(gql`{ name #client }`, { name: 'localData' }),
graphql(gql`{ currencies { name } }`)
)(({ localData }) => <h1>{ localData.name }</h1>)
I don't want to use componentWillReceiveProps (mutating there the local state as soon as props come from the remote query), as it's bad practice.
I need to use local apollo state, not just the state of the component, as this value is going to be used by other components.
I tried using the props config of the graphql hoc, but it feels as bad as componentWillReceiveProps, and lead to an infinite loop.
Any idea?
Relevant versioning:
"apollo-boost": "^0.1.19",
"apollo-link-state": "^0.4.2",
"react-apollo": "^2.2.4",
Code
import React from 'react'
import ReactDOM from 'react-dom'
import ApolloClient from 'apollo-boost'
import gql from 'graphql-tag'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloProvider, graphql, compose } from 'react-apollo'
import { ApolloLink } from 'apollo-link'
import { withClientState } from 'apollo-link-state'
import { HttpLink } from 'apollo-link-http'
const resolvers = {
Mutation: {
updateName: (ops, { name }, { cache }) => cache.writeData({ data: { name } })
}
}
const typedefs = gql`
type Query {
name: String
}
type Mutation {
updateName(name: String): String
}
`
const defaults = { name: 'defaultName' }
const cache = new InMemoryCache()
const stateLink = withClientState({
cache,
defaults,
resolvers
});
const apolloClient = new ApolloClient({
uri: '/store/api/graphql',
link: ApolloLink.from([stateLink, new HttpLink()]),
cache,
clientState: {
defaults,
resolvers,
typedefs
}
})
const App = compose(
graphql(gql`{ name #client }`, { name: 'localData' }),
graphql(gql`{ currencies { name id } }`)
)(({ localData }) => <h1>{ localData.name }</h1>)
ReactDOM.render(
<ApolloProvider client={ apolloClient }>
<App />
</ApolloProvider>,
document.getElementById('app')
)
I found a solution using the Query component instead of the graphql hoc. The Query component has the onCompleted property, where I can pass a function that updates the local state:
const UPDATE_NAME = gql`mutation ($name: String) {
updateName (name: $name) #client { name }
}`
const App = compose(
withApollo,
graphql(gql`{ name #client }`, { name: 'localData' })
)(({ client, localData }) => <Query
query={ gql`{ currencies { name id } }` }
onCompleted={ data =>
client.mutate({
mutation: UPDATE_NAME,
variables: { name: data.currencies[0].name }
})
}>
{
() => <h1>Current: { localData.name }</h1>
}
</Query>)
I find the Query component less elegant than the graphql hoc, but at least is a solution.

Apollo react: combining rest and graphql with link-state

I am trying to use REST endpoints to post data and GraphQL for query and fetch along with apollo-link-state. My rest endpoint is getting hit and application is getting created. But when I try to run the query to write to cache it's not hitting the graphql endpoint. and I keep getting the following error:
Unhandled Rejection (Error): Can't find field findApplicationByUuid({"uuid":"912dc46d-2ef8-4a77-91bc-fec421f5e4bc"}) on object (ROOT_QUERY) {
"application": {
"type": "id",
"id": "$ROOT_QUERY.application",
"generated": true
}
}.
Here are my GQL query
import gql from 'graphql-tag';
const START_APP = gql`
mutation startApp($type: String!) {
application: startApp( input: { applicationType: $type})
#rest(type: "Application", path: "v1/member/application/create", method: "POST") {
uuid: applicationUuid
}
}
`;
const GET_APP = gql`
query getAppByUuid($uuid: String!) {
application: findApplicationByUuid(uuid: $uuid) {
uuid,
type,
version,
}
}
`;
export {
START_APP,
GET_APP,
};
Here is my resolver:
import { START_APP, GET_APP } from './application'
import client from '../apolloClient';
const startApp = async (_, { type }, { cache }) => {
client.mutate({
variables: { type },
mutation: START_APP,
}).then(({ data: { application } }) => {
const { uuid } = application;
const { data } = cache.readQuery({
query: GET_APP,
variables: { uuid },
});
cache.writeQuery({
query: GET_APP,
data,
});
});
};
const resolvers = {
Mutation: {
startApp,
},
};
Here are my links:
import { resolvers, defaults } from './resolvers';
const cache = new InMemoryCache();
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(`[GQL Error]: Msg: ${message}, Loc: ${locations}, Path: ${path}`));
if (networkError) console.log(`[Network error]: ${networkError}`);
});
const stateLink = withClientState({
cache,
defaults,
resolvers,
});
const restLink = new RestLink({
uri: 'http://localhost:7010/api/',
credentials: 'include',
});
const batchHttpLink = new BatchHttpLink({
uri: 'http://localhost:7010/api/graphql',
credentials: 'include',
});
const httpLink = new HttpLink({
uri: 'http://loaclhost:7010/api/graphql',
credentials: 'include',
});
const link = ApolloLink.from([
errorLink,
stateLink,
restLink,
httpLink,
]);
my client
const client = new ApolloClient({
link,
cache,
});
My react component:
// Remote Mutation
const START_APP = gql`
mutation startApp($type: String!) {
startApp(type: $type) #client {
uuid
}
}
`;
const StartApp = ({ match }) => {
const { type } = match.params;
return (
<Mutation mutation={START_APP} variables={{ type }}>
{startApp => (<button onClick={startApp}>click me</button>)}
</Mutation>
)
};
When I hit the button it calls create endpoint and creates the app and returns the uuid. But the following I want to happen is hit the graphql endpoint and query for the application using the uuid returned from the rest request, and write that data to the cache/state.

orderBy: the results from update cache query

Working on a react apollo graphcool project
I've got my mutation update working, however I would like to filter the results, the results only filter on page refresh?
Looking at cache.writeQuery() the docs say get the query and concat to that so i guess thats why its not filtering. Is there anyway to query after?
Here the code from my CreatePost component
import React from 'react';
import gql from "graphql-tag";
import { Mutation } from "react-apollo";
const GET_POSTS = gql`
{
allPosts(orderBy: createdAt_DESC) {
id
title
content
}
}
`;
const CREATE_POST = gql`
mutation createPost($title: String!, $content: String!){
createPost(title: $title, content: $content){
id
title
content
}
}
`;
const udpateCache = (cache, { data: { createPost } }) => {
const { allPosts } = cache.readQuery({ query: GET_POSTS });
cache.writeQuery({
query: GET_POSTS,
data: { allPosts: allPosts.concat([createPost]) }
})
}
const CreatePost = () => {
//vars for the input refs
let titleInput
let contentInput
return (
<Mutation
mutation={CREATE_POST}
update={udpateCache}
>
{createPost => ( //##form and onSubmit ##// ) }
</Mutation>
)
}
export default CreatePost
When you do your writeQuery you also need to pass in any variables used, to make sure you receive the same information from the cache.
const udpateCache = (cache, { data: { createPost } }) => {
const { allPosts } = cache.readQuery({ query: GET_POSTS });
cache.writeQuery({
query: GET_POSTS,
data: { allPosts: allPosts.concat([createPost]) },
variables: { orderBy: /* input */ }
})
}

Resources