Setting Apollo cache / state after mutation - reactjs

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

Related

How to remove response_mode = web_message from loginwithPopup url?

I am working on authentication using Auth0 and react. I am using loginWithPopup() for the login popup screen. But every time I end up getting misconfiguration error(like you can see in the attachment). But if I remove the response_mode = web_message from the URL it works, is there any way to remove response_mode from code. I am using the react-auth0-spa.js given my auth0 quick start
import React, { Component, createContext } from 'react';
import createAuth0Client from '#auth0/auth0-spa-js';
// create the context
export const Auth0Context = createContext();
// create a provider
export class Auth0Provider extends Component {
state = {
auth0Client: null,
isLoading: true,
isAuthenticated: false,
user: false,
};
config = {
domain: "dev-ufnn-q8r.auth0.com",
client_id: "zZh4I0PgRLQqLKSPP1BUKlnmfJfLqdoK",
redirect_uri: window.location.origin,
//audience: "https://reachpst.auth0.com/api/v2/"
};
componentDidMount() {
this.initializeAuth0();
}
// initialize the auth0 library
initializeAuth0 = async () => {
const auth0Client = await createAuth0Client(this.config);
const isAuthenticated = await auth0Client.isAuthenticated();
const user = isAuthenticated ? await auth0Client.getUser() : null;
this.setState({ auth0Client, isLoading: false, isAuthenticated, user });
};
loginWithPopup = async () => {
try {
await this.state.auth0Client.loginWithPopup();
}
catch (error) {
console.error(error);
}
this.setState({
user: await this.state.auth0Client.getUser(),
isAuthenticated: true,
});
};
render() {
const { auth0Client, isLoading, isAuthenticated, user } = this.state;
const { children } = this.props;
const configObject = {
isLoading,
isAuthenticated,
user,
loginWithPopup: this.loginWithPopup,
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
logout: (...p) => auth0Client.logout(...p)
};
return (
<Auth0Context.Provider value={configObject}>
{children}
</Auth0Context.Provider>
);
}
}
After a bit of research, I found an answer to my own question. So if we use response_mode = web_message then we need to configure our callback URL in allowed web origin field as well. In my case, I am using loginWithPopup() so which typically adds response_mode = web_message in the login URL because loginWithPopup() from auth0 SDK is a combination of PKCE + web_message
https://auth0.com/docs/protocols/oauth2 (under how response mode works?)
https://auth0.com/blog/introducing-auth0-single-page-apps-spa-js-sdk (under behind the curtain)

Unable to get updated cache in component in a server rendered nextjs apollo app

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

Best Practice for handling consecutive identical useFetch calls with React Hooks?

Here's the useFetch code I've constructed, which is very much based upon several well known articles on the subject:
const dataFetchReducer = (state: any, action: any) => {
let data, status, url;
if (action.payload && action.payload.config) {
({ data, status } = action.payload);
({ url } = action.payload.config);
}
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: data,
status: status,
url: url
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
data: null,
status: status,
url: url
};
default:
throw new Error();
}
}
/**
* GET data from endpoints using AWS Access Token
* #param {string} initialUrl The full path of the endpoint to query
* #param {JSON} initialData Used to initially populate 'data'
*/
export const useFetch = (initialUrl: ?string, initialData: any) => {
const [url, setUrl] = useState<?string>(initialUrl);
const { appStore } = useContext(AppContext);
console.log('useFetch: url = ', url);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
status: null,
url: url
});
useEffect(() => {
console.log('Starting useEffect in requests.useFetch', Date.now());
let didCancel = false;
const options = appStore.awsConfig;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
let response = {};
if (url && options) {
response = await axios.get(url, options);
}
if (!didCancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: response });
}
} catch (error) {
// We won't force an error if there's no URL
if (!didCancel && url !== null) {
dispatch({ type: 'FETCH_FAILURE', payload: error.response });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url, appStore.awsConfig]);
return [state, setUrl];
}
This seems to work fine except for one use case:
Imagine a new Customer Name or UserName or Email Address is typed in - some piece of data that has to be checked to see if it already exists to ensure such things remain unique.
So, as an example, let's say the user enters "My Existing Company" as the Company Name and this company already exists. They enter the data and press Submit. The Click event of this button will be wired up such that an async request to an API Endpoint will be called - something like this: companyFetch('acct_mgmt/companies/name/My%20Existing%20Company')
There'll then be a useEffect construct in the component that will wait for the response to come back from the Endpoint. Such code might look like this:
useEffect(() => {
if (!companyName.isLoading && acctMgmtContext.companyName.length > 0) {
if (fleetName.status === 200) {
const errorMessage = 'This company name already exists in the system.';
updateValidationErrors(name, {type: 'fetch', message: errorMessage});
} else {
clearValidationError(name);
changeWizardIndex('+1');
}
}
}, [companyName.isLoading, companyName.isError, companyName.data]);
In this code just above, an error is shown if the Company Name exists. If it doesn't yet exist then the wizard this component resides in will advance forward. The key takeaway here is that all of the logic to handle the response is contained in the useEffect.
This all works fine unless the user enters the same Company Name twice in a row. In this particular case, the url dependency in the companyFetch instance of useFetch does not change and thus there is no new request sent to the API Endpoint.
I can think of several ways to try to solve this but they all seem like hacks. I'm thinking that others must have encountered this problem before and am curious how they solved it.
Not a specific answer to your question, more of another approach: You could always provide a function to trigger a refetch by the custom hook instead of relying of the useEffect to catch all different cases.
If you want to do that, use useCallback in your useFetch so you don't create an endless loop:
const triggerFetch = useCallback(async () => {
console.log('Starting useCallback in requests.useFetch', Date.now());
const options = appStore.awsConfig;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
let response = {};
if (url && options) {
response = await axios.get(url, options);
}
dispatch({ type: 'FETCH_SUCCESS', payload: response });
} catch (error) {
// We won't force an error if there's no URL
if (url !== null) {
dispatch({ type: 'FETCH_FAILURE', payload: error.response });
}
}
};
fetchData();
}, [url, appStore.awsConfig]);
..and at the end of the hook:
return [state, setUrl, triggerFetch];
You can now use triggerRefetch() anywhere in your consuming component to programmatically refetch data instead of checking every case in the useEffect.
Here is a complete example:
CodeSandbox: useFetch with trigger
To me this slightly related to thing "how to force my browser to skip cache for particular resource" - I know, XHR is not cached, just similar case. There we may avoid cache by providing some random meaningless parameter in URL. So you can do the same.
const [requestIndex, incRequest] = useState(0);
...
const [data, updateURl] = useFetch(`${url}&random=${requestIndex}`);
const onSearchClick = useCallback(() => {
incRequest();
}, []);

GraphQL error: Cannot query field 'mutation_name' on type 'Mutation'

I am trying to mutate a mutation. The mutation does exist and works fine on my graphql playground. but as I implement it in my react component, I get error. Queries work fine though. By the way, I need client in my code so I definitely have to use ApolloConsumer.
I tried using client.mutate like https://github.com/apollographql/apollo-client/issues/2762
export const LOGIN = gql`
mutation LOGIN($email: String!, $password: String!) {
login(email: $email, password: $password) {
email
}
}
`;
class LoginComponent extends Component{
render(){
return(
<ApolloConsumer>
{client=>{
return(
<Button onClick={()=>{
client
.mutate({
mutation: LOGIN,
variables: {
email: "test#test.com",
password: "test"
}
})
.then(result => {
console.log('result', result)
})
.catch(err => {
console.log("err", err);
alert(err.toString());
});
}}>
OK
</Button>
)
}}
</ApolloConsumer>
)
}
}
I expect to get success but I get Error: GraphQL error: Cannot query field 'login' on type 'Mutation'. (line 2, column 3):
login(email: $email, password: $password) {
^
Same issue here, I want to add my 2 cents as an answer:
I have several "context" for different endpoint for different GraphQL servers.
I was using the mutation hook like:
const THE_QUERY_OR_MUTATION_CODE = gql`
mutation {
myMutation(etc) {
_id
etc
}
}`
const [handlerOrCaller, { loading, error, data } ] =
useMutation(THE_QUERY_OR_MUTATION_CODE);
In that way I wasn't giving it the correct "context" so Apollo Client was taking the first context found, which was of course another endpoint, and there the schema of course doesn't have that mutation called myMutation.
The config of my Apollo Client is:
const client = new ApolloClient({
cache: new InMemoryCache({
dataIdFromObject: (object) => object.key || null
}),
link: authLink.concat(splitOne)
});
That splitOne is a concatenated var made with different endpoints in this way:
const endpointALink = createHttpLink({
uri: process.env.REACT_APP_ENDPOINT_A
});
const ednpointBLink = createUploadLink({
uri: process.env.REACT_APP_ENDPOINT_B
});
const endpointCLink = createUploadLink({
uri: process.env.REACT_APP_ENDPOINT_C
});
const endpointDLink = createHttpLink({
uri: process.env.REACT_APP_ENDPOINT_D
});
const splitThree = split(
(operation) => operation.getContext().clientName === 'context_a',
endpointA,
endpointB
);
const splitTwo = split(
(operation) => operation.getContext().clientName === 'context_b',
endpointC,
splitThree
);
const splitOne = split(
(operation) => operation.getContext().clientName === 'context_c',
endpointD,
splitTwo
);
SO the correct way, in my case, was using the correct context:
const [handlerOrCaller, { loading, error, data } ] =
useMutation(THE_QUERY_OR_MUTATION_CODE, {
context: { clientName: 'context_c' }
});
My mistake was trivial: I was using type-graphql and appolo-server in the backend but added the datasource instead of the correct resolver in the resolvers field:
const schema = await buildSchema({
resolvers: [
FirstResolver,
SecondResolver,
// added datasource instead of resolver
ThirdDataSource,
FourthResolver,
]})
Replacing datasource with the resolver solved the issue.
Package versions:
"apollo-datasource": "^3.0.3",
"type-graphql": "^1.1.1",
"apollo-server-core": "^3.6.2",

How to make a query and a mutation in the same time with GraphQL?

Hello guys I am new in this Apollo - GraphQL and I am using I try to implement my server with my React Native app.
While I am trying to build the Password Change functions I get the following error this.props.resetPassword is not a function. (In 'this.props.resetPassword(_id, password)', 'this.props.resetPassword' is undefined)
My code looks like this
toSend() {
const { _id } = this.props.data.me;
const { password } = this.state;
console.log(_id, password)
this.props
.resetPassword(_id, password)
.then(({ data }) => {
return console.log(data);
})
}
And here is my query and my mutation
export default graphql(
gql`
query me {
me {
_id
}
}
`,
gql`
mutation resetPassword($_id: String!, $password: String!) {
resetPassword(_id: $_id, password: $password) {
_id
}
}
`,
{
props: ({ mutate }) => ({
resetPassword: (_id, password) => mutate({ variables: { _id, password } }),
}),
},
)(PasswordChange);
If you're using the graphql HOC, you'll need to use compose to combine multiple queries/mutations. For example:
const mutationConfig = {
props: ({mutate, ownProps}) => ({
resetPassword: (_id, password) => mutate({ variables: { _id, password } }),
...ownProps,
})
}
export default compose(
graphql(YOUR_QUERY, { name: 'createTodo' }),
graphql(YOUR_MUTATION, mutationConfig),
)(MyComponent);
Keep in mind that the HOC will pass down whatever props it receives by default, but if you provide a function for props in the configuration, it's on you to do so by utilizing ownProps. Also, when using compose, the order of the HOCs matters if any of them are dependent on the others for props. That means as long as your query comes first inside compose, you can use the query HOC's data inside your mutation's configuration.

Resources