I am currently building a simple CRUD workflow using React and GraphQL. After I create an object (an article in this case which just has an id, title and description.), I navigate back to an Index page which displays all of the currently created articles. My issue is that after an article is created, the index page does not display the created article until I refresh the page. I am using apollo to query the graphql api and have disabled cacheing on it so I'm not sure why the data isn't displaying. I've set breakpoints in my ArticlesIndex's componentDidMount function and ensured that it is executing and at the time of executing, the database does include the newly added article.
My server side is actually never even hit when the client side query to retrieve all articles executes. I'm not sure what is cacheing this data and why it is not being retrieved from the server as expected.
My ArticlesCreate component inserts the new record and redirects back to the ArticlesIndex component as follows:
handleSubmit(event) {
event.preventDefault();
const { client } = this.props;
var article = {
"article": {
"title": this.state.title,
"description": this.state.description
}
};
client
.mutate({ mutation: CREATE_EDIT_ARTICLE,
variables: article })
.then(({ data: { articles } }) => {
this.props.history.push("/articles");
})
.catch(err => {
console.log("err", err);
});
}
}
then my ArticlesIndex component retrieves all articles from the db as follows:
componentDidMount = () => {
const { client } = this.props; //client is an ApolloClient
client
.query({ query: GET_ARTICLES })
.then(({ data: { articles } }) => {
if (articles) {
this.setState({ loading: false, articles: articles });
}
})
.catch(err => {
console.log("err", err);
});
};
and I've set ApolloClient to not cache data as in my App.js as follows:
const defaultApolloOptions = {
watchQuery: {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
}
export default class App extends Component {
displayName = App.name;
client = new ApolloClient({
uri: "https://localhost:44360/graphql",
cache: new InMemoryCache(),
defaultOptions: defaultApolloOptions
});
//...render method, route definitions, etc
}
Why is this happening and how can I solve it?
It looks like this is an issue with ApolloBoost not supporting defaultOptions as noted in this github issue. To resolve the issue I changed:
const defaultApolloOptions = {
watchQuery: {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
}
export default class App extends Component {
displayName = App.name;
client = new ApolloClient({
uri: "https://localhost:44360/graphql",
cache: new InMemoryCache(),
defaultOptions: defaultApolloOptions
});
//...render method, route definitions, etc
}
To:
const client = new ApolloClient({
uri: "https://localhost:44360/graphql"
});
client.defaultOptions = {
watchQuery: {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
};
export default class App extends Component {
//....
}
I can see that you are getting the data and setting your components state on initial mount. Most probably when you redirect it doesn't fire the componentDidMount lifecylcle hook as it is already mounted, if that is the issue try using componentDidUpdate lifecycle hook so that your component knows there was an update and re-set the data.
Related
I am working on image upload with React TinyMCE editor. I have two graphql endpoints to process my image upload. This is my file_picker_callback
file_picker_callback: function() {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.onchange = function() {
const file = this.files[0];
const isFileValid = validateUpload(file);
if (isFileValid) {
setLastUploadedFile(file);
getImageSignedUrl({
variables: {
id,
locale: 'en-US',
},
});
setIsImageValid(false);
}
};
}
This is the first graphql endpoint that I make
const [getImageSignedUrl] = useLazyQuery(GET_SIGNED_IMAGE_URL, {
fetchPolicy: 'no-cache',
onCompleted: async ({ uploadUrlForCustomContentImage }) => {
const res = await uploadImage(lastUploadedFile, uploadUrlForCustomContentImage?.signedUrl);
if (res.status === 200) {
confirmImageUpload({
variables: {
input: {
id,
locale: 'en-US',
fileName: uploadUrlForCustomContentImage?.fileName,
},
},
});
}
},
});
After 'getImageSignedUrl' is finished in onCompleted block I make a second graphql 'confirmImageUpload' call which should return my imageUrl that I was planning to use within file_picker_callback to insert into input field.
Second endpoint
const [confirmImageUpload] = useMutation(CONFIRM_IMAGE_UPLOAD, {
fetchPolicy: 'no-cache',
});
However I am having trouble accessing data within file_picker_callback after confirmImageUpload is finished executing. I tried to update my local state in onCompleted block but its not able to pick up the change within file_picker_callback.
This is the first time I am working with React TinyMCE editor so if anyone has any suggestions please let me know
I'm new to Apollo and Apollo caching. I've been writing a demo project and have run into two issues:
Can't get cacheRedirects to work properly:
In my demo, I "Load All" COVID19 test data, which queries an API and returns an array of results for 3 countries, including Canada. Then I try to load an individual result ("Load Canada") to get it to use my cacheRedirects resolver. I think I have this set up correctly, but it always goes back to the API to do the query rather than reading from the cache.
Here is my Apollo client with cache:
const client = new ApolloClient({
uri: "https://covid19-graphql.now.sh",
cache: new InMemoryCache({
dataIdFromObject: obj => {
console.log("in cache: obj:", obj);
let dataId = null;
switch (obj.__typename) {
case "Country":
dataId = obj.__typename + "_" + obj.name;
break;
default:
dataId = defaultDataIdFromObject(obj);
}
console.log("in cache: dataId:", dataId);
return dataId;
},
cacheRedirects: {
Query: {
country: (_, args, { getCacheKey }) => {
console.log("in cacheRedirects:", _, args);
const cacheKey = getCacheKey({ __typename: "Country", ...args });
console.log("cacheKey:", cacheKey);
return cacheKey;
}
}
}
})
// connectToDevTools: true
});
I can't figure out how to perform a readFragment:
I've tried so many different configurations within this code, but I never get any results.
Here is my function:
const ReadFragment = () => {
console.log("in ReadFragment");
try {
const data = client.readFragment({
id: GET_DATA.countryData,
fragment: gql`
fragment mostRecent on country {
id
text
complete
}
`
});
if (data) {
console.log(data);
return (
<div>
From Fragment: {JSON.stringify(data)}
</div>
);
} else {
return <div>From Fragment: not found</div>;
}
} catch (error) {
// console.error(error);
return <div>From Fragment: not found</div>;
}
};
Bonus Question: I don't seem to be able to get the Apollo Client Developer Tools extension to work in Chrome browser. Does this still work? My code never seems to connect to it. (uncomment out the connectToDevTools: true.) It seems that being able to examine the contents of the cache would be very useful for development and learning. Is there an alternate way to view the cache contents?
The Apollo GraphQL maintain the cache itself and certainly you don't have to -
export declare type FetchPolicy = 'cache-first' | 'network-only' | 'cache-only' | 'no-cache' | 'standby';
If you look into the fetchPolicy declaration then there are several options to do that -
Network Only
const { data } = useQuery(GET_LIST, {
fetchPolicy: 'network-only'
});
Cache First
const { data } = useQuery(GET_LIST, {
fetchPolicy: 'cache-first'
});
Cache Only
const { data } = useQuery(GET_LIST, {
fetchPolicy: 'cache-only'
});
Similarly the rest options also can be looked upon based on requirement.
If you want to maintain state and do that kinda work then write resolvers for those queries -
const client = new ApolloClient({
uri: apollo.networkInterface,
cache,
resolvers: {
Mutation: {
searchQuery: (launch, _args, { cache }) => {
console.log(cache); // cache can be queried here
// read from cache if planning to modify the data
//const { searchQuery } = cache.readQuery({ query: GET_LIST });
cache.writeQuery({
query: GET_LIST,
data: {searchQuery:_args.searchQuery}
});
},
},
},
})
The Chrome client does work but for that the connection with the graphql server has to be done. Else it will not show up in chrome.
I have created subscription whenever a new post is added. The subscription works fine on the graphiql interface.
Here is my react code to use useSubscription hook
export const SUSCRIBE_POSTS = gql`
{
subscription
posts {
newPost {
body
id
createdAt
}
}
}
`;
const {
loading: suscriptionLoading,
error: subscriptionError,
data: subscriptionData,
} = useSubscription(SUSCRIBE_POSTS);
when I try to console log subscriptionData I get nothing. when I add a post it is saved in database correctly, the useQuery hook for getting posts also work fine, but when I add a new post I don't see the subscription data. I can't see anything wrong in the console as well. When I log suscriptionLoading, Ido get true at the start. I am not sure how to debug this.
The client setup is done correctly according to the docs https://www.apollographql.com/docs/react/data/subscriptions/
and here is the code
const httpLink = createHttpLink({
uri: "http://localhost:4000/",
});
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/graphql`,
options: {
reconnect: true,
},
});
const authLink = setContext(() => {
const token = localStorage.getItem("jwtToken");
return {
headers: {
Authorization: token ? `Bearer ${token}` : "",
},
};
});
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
authLink.concat(httpLink)
);
const client = new ApolloClient({
link: link,
cache: new InMemoryCache(),
});
export default (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
export const SUSCRIBE_POSTS = gql`
subscription
posts {
newPost {
body
id
createdAt
}
}
`;
Can you try to remove the most-outside brackets of subscription gql?
I have a nextjs apollo server rendered application using apollo client state.
The issue that I'm facing is that on app load, even when the local state is updated correctly, the local state graphql query called in the header component doesn't return the latest state with the correct data, instead it returns the initial state. I am unable to figure out why that is happening and is it because of the apollo client setup or because of the cache initialState.
The app is a nextjs server rendered app using apollo client. I've extensively tried tweaking the apollo client setup to figure out where exactly is the state getting reset [I say reset because on logging the state after updating the state gives me the correct result].
The apollo config is a modified version of the official nextjs apollo example
// lib/apollo.js
...
const appCache = new InMemoryCache().restore(initialState);
const getState = (query) => appCache.readQuery({ query });
const writeState = (state) => appCache.writeData({ data: state });
initCache(appCache);
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: ApolloLink.from([consoleLink, errorLink, authLink, fileUploadLink]),
cache: appCache,
resolvers: StateResolvers(getState, writeState),
typeDefs,
defaults: {},
connectToDevTools: true,
});
...
// apollo/StateResolvers.js
export const GET_LOGIN_STATUS_QUERY = gql`
query {
loginStatus #client {
isLoggedIn
}
}
`;
export default (getState, writeState) => {
return {
Mutation: {
updateLoginStatus(_, data) {
const { loginStatus } = getState(GET_LOGIN_STATUS_QUERY);
const newState = {
...loginStatus,
loginStatus: {
...loginStatus,
...data,
},
};
writeState(newState);
const updatedData = getState(terminal); // getting correct updated state here
console.log('log updateLoginStatus:', updatedData);
return { ...data, __typename: 'loginStatus' };
},
},
};
};
// initCache.js
export default (appCache) =>
appCache.writeData({
data: {
loginStatus: {
isLoggedIn: false,
__typename: 'loginStatus',
},
},
});
Header is imported in the Layout component which is used in the _app.js nextjs file
// header/index.js
...
const q = gql`
query {
loginStatus #client {
isLoggedIn
}
}
`;
const Header = (props) => {
const { loading, data, error } = useQuery(q); // not getting the updated state here
console.log('header query data:', loading, data, error);
return (
<div>header</div>
);
};
...
The second log with the result is the output from the checkLoggedIn file similar to the one here
// server side terminal output
log updateLoginStatus: { loginStatus: { isLoggedIn: true, __typename: 'loginStatus' } }
result: { data: updateLoginStatus: { isLoggedIn: true, __typename: 'loginStatus' } } }
header query data: true undefined undefined {}
header query data: false { loginStatus: { isLoggedIn: false, __typename: 'loginStatus' } } undefined
header query data: false { loginStatus: { isLoggedIn: false, __typename: 'loginStatus' } } undefined
My ultimate goal is to get the correctly set isLoggedIn flag in the header to correctly toggle between Logged in and logout state.
Please let me know if any more details are required. Any help would be highly appreciated.
It looks like you are restoring initial state
You want to restore the server rendered state.
For example:
lets say you did
const state = apolloClient.extract();
return `
<script>
window.__APOLLO_STATE__ = ${JSON.stringify(state)};
</script>
`;
you want to restore this state
not the inital state
I am using Apollo 2.0 to manage my graphQL API calls and to handle the global state of my react application.
I am trying to create a login screen where a user enters their username and password, this gets sent to my API to authenticate and upon success, I want to then set the global state of isLoggedIn to true.
So far, I am able to set the global state with one mutation which utilises the #client declaration so it is only concerned with local state. I have another mutation which makes the graphQL API call and validates username / password and then returns success / error responses.
I want to be able to set isLoggedIn once the API call mutation has completed or failed.
My client has the following default state and resolvers set like so:
const httpLink = new HttpLink({
uri: '/graphql',
credentials: 'same-origin'
});
const cache = new InMemoryCache();
const stateLink = withClientState({
cache,
resolvers: {
Mutation: {
updateLoggedInStatus: (_, { isLoggedIn }, { cache }) => {
const data = {
loggedInStatus: {
__typename: 'LoggedInStatus',
isLoggedIn
},
};
cache.writeData({ data });
return null;
},
},
},
defaults: {
loggedInStatus: {
__typename: 'LoggedInStatus',
isLoggedIn: false,
},
},
});
const link = ApolloLink.from([stateLink, httpLink])
const client = new ApolloClient({
link,
cache
});
export default client
Then in my Login component I have the following mutations and queries which I pass as a HOC with the help of compose:
const UPDATE_LOGGED_IN_STATUS = gql`
mutation updateLoggedInStatus($isLoggedIn: Boolean) {
updateLoggedInStatus(isLoggedIn: $isLoggedIn) #client
}`
const AUTHENTICATE = gql`
mutation authenticate($username: String!, $password: String!) {
auth(username: $username, password: $password) {
username
sales_channel
full_name
roles
}
}`
const GET_AUTH_STATUS = gql`
query {
loggedInStatus #client {
isLoggedIn
}
}`
export default compose(
graphql(GET_AUTH_STATUS, {
props: ({ data: { loading, error, loggedInStatus } }) => {
if (loading) {
return { loading };
}
if (error) {
return { error };
}
return {
loading: false,
loggedInStatus
};
},
}),
graphql(UPDATE_LOGGED_IN_STATUS, {
props: ({ mutate }) => ({
updateLoggedInStatus: isLoggedIn => mutate({ variables: { isLoggedIn } }),
}),
}),
graphql(AUTHENTICATE, {
props: ({ mutate }) => ({
authenticate: (username, password) => mutate({ variables: { username, password } }),
}),
})
)(withRouter(Login));
So as you can see I have this.props.authenticate(username, password) which is used when the login form is submitted.
Then I have the this.props.updateLoggedInStatus(Boolean) which I am able to update the client cache / state.
How do I combine these so that I can call authenticate() and if it's successful, set the loggedInStatus and if it fails, set a hasErrored or errorMessage flag of sorts?
Thanks in advance.
EDIT:
I have attempted to handle updating the state within the callback of my mutation.
// Form submission handler
onSubmit = async ({ username, password }) => {
this.setState({loading: true})
this.props.authenticate(username, password)
.then(res => {
this.setState({loading: false})
this.props.updateLoggedInStatus(true)
})
.catch(err => {
this.setState({loading: false, errorMessage: err.message})
console.log('err', err)
})
}
Is there a better way of doing it than this? It feels very convoluted having to wait for the call back. I would have thought I could map the response to my cache object via my resolver?
I think the way you're currently handling it (calling authenticate and then updateLoggedInStatus) is about as clean and simple as you're going to get with apollo-link-state. However, using apollo-link-state for this is probably overkill in the first place. It would probably be simpler to derive logged-in status from Apollo's cache instead. For example, you could have a HOC like this:
import client from '../wherever/client'
const withLoggedInUser = (Component) => {
const user = client.readFragment({
id: 'loggedInUser',
fragment: gql`
fragment loggedInUser on User { # or whatever your type is called
username
sales_channel
full_name
roles
# be careful about what fields you list here -- even if the User
# is in the cache, missing fields will result in an error being thrown
}
`
})
const isLoggedIn = !!user
return (props) => <Component {...props} user={user} isLoggedIn={isLoggedIn}/>
}
Notice that I use loggedInUser as the key. That means we also have to utilize dataIdFromObject when configuring the InMemoryCache:
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory'
const cache = new InMemoryCache({
dataIdFromObject: object => {
switch (object.__typename) {
case 'User': return 'loggedInUser'
// other types you don't want the default behavior for
default: return defaultDataIdFromObject(object);
}
}
})