In one of my components within a redux-form onSubmit, I have the following:
const result = await helloSignup(values);
console.log(result);
helloSignup is mutating the database as expected but the const result is currently be logged as undefined
Why?
My HOC/mutation helloSignup:
export const HELLO_SIGNUP_MUTATION = gql`
mutation (
$email: String!
$code: String!
) {
signup(authProvider: {
emailAndCode: {
email: $email
code: $code
}
}) {
token
user {
id
}
}
}
`;
export default graphql(
HELLO_SIGNUP_MUTATION,
{
props: ({ mutate }) => ({
emailAndCodeSignup: async (variables) => {
const { data } = await mutate({ variables });
const { token } = data.signup;
},
}),
}
);
Using GraphiQL, I can see that my graphql mutation, returns the desired results:
{
"data": {
"signup": {
"token": "xxx",
"user": {
"id": "16"
}
}
}
}
If GraphiQL is getting the desired results after mutating, why isn't the result being console logged above?
React-Apollo provides a HOC for client side queries and mutations called withApollo.
This signature is something like this:
withApollo(MyForm)
https://www.apollographql.com/docs/react/basics/setup.html#withApollo
which adds a prop of 'client' to the MyForm component. On form submission, you'd want to access this prop, and call the mutation from there. So in your form submit handler youd end up with something like this:
https://www.apollographql.com/docs/react/basics/mutations.html#basics
onSubmit() {
const { client } = this.props
const options = {} // your mutation options
// mutations ands queries return promises,
// so you must wait for their completion before accessing data
client.mutate(
HELLO_SIGNUP_MUTATION,
options
).then(({ data }) => (
console.log('got data', data);
)
}
}
Where data should be whats coming back from the API
Related
Is is possible to get the response of endpoint in React RTK Query Mutation .
I have a one mutation which insert into DB and I need to get the ID which inserted. in my api :
addRecord: build.mutation({
query(data) {
return {
url: base + 'myControler/SaveDataAsync',
method: 'post',
data: data,
}
},
}),
and in my component after import my hook I call it like
const [addRecord] = useAddRecordMutation();
and then in my submit function is use it like
const handleSubmitCustom = async (values) => {
await addRecord(values);
}
which I need the return value of await addRecord(values);
You can just do
const handleSubmitCustom = async (values) => {
try {
const returned = await addRecord(values).unwrap();
} catch (error) {
// you can handle errors here if you want to
}
}
I try to use social login.
if I success to login in kakao. they give me access_token and I use it to mutation to my server
below is my code
import { useMutation } from "react-apollo-hooks";
import { KAKAO_LOGIN } from "./AuthQuery";
export default () => {
const kakaoLoginMutation = useMutation(KAKAO_LOGIN, {
variables: { provder: "kakao", accessToken: authObj.access_token },
});
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function (authObj) {
console.log(authObj.access_token);
},
});
};
if (authObj.access_token !== "") {
kakaoLoginMutation();
}
return (
<a href="#" onClick={kakaoLogin}>
<h1>카카오로그인</h1>
</a>
);
};
if I success to login using by function kakaoLogin, it give authObj.
console.log(authObj.access_token) show me access_token
and I want to use it to useMutation. but it show to me authObj is not defined.
Looks like you're looking for a local state to hold authObj
const [authObj, setAuthObj] = useState({});
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function(authObj) {
setAuthObj(authObj);
},
});
};
const kakaoLoginMutation = useMutation(KAKAO_LOGIN, {
variables: {
provder: "kakao",
accessToken: authObj.access_token
},
});
if (authObj.access_token !== "") {
kakaoLoginMutation();
}
After reading Apollo auth docs (you read it, right?) you should know tokens should be sent using headers.
... if auth link is used and reads the token from localStorage ... then any login function (mutation, request) result should end storing token in localStorage (to be passed by header in next queries/mutations) ... it should be obvious.
In this case
... we have a bit different situation - kakao token is passed into login mutation as variable ...
We can simply, directly (not using state, effect, rerenderings) pass the kakao token to 'mutation caller' (kakaoLoginMutation):
// mutation definition
const [kakaoLoginMutation] = useMutation(KAKAO_LOGIN);
// event handler
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function(authObj) {
// run mutation
kakaoLoginMutation({
variables: {
provder: "kakao",
accessToken: authObj.access_token
}
})
},
});
};
When required, login mutation (KAKAO_LOGIN) result can be processed within onCompleted handler:
const [kakaoLoginMutation] = useMutation(KAKAO_LOGIN,
onCompleted = (data) => {
// save mutation result in localStorage
// set some state to force rerendering
// or redirection
}
);
I'm building a discord/slack clone. I have Channels, Messages and users.
As soon as my Chat component loads, channels get fetched with useQuery hook from Apollo.
By default when a users comes at the Chat component, he needs to click on a specific channels to see the info about the channel and also the messages.
In the smaller Channel.js component I write the channelid of the clicked Channel to the apollo-cache. This works perfect, I use the useQuery hooks #client in the Messages.js component to fetch the channelid from the cache and it's working perfect.
The problem shows up when I use the useLazyQuery hook for fetching the messages for a specific channel (the channel the user clicks on).
It causes a infinite re-render loop in React causing the app to crash.
I've tried working with the normal useQuery hook with the skip option. I then call the refetch() function when I need it. This 'works' in the sense of it not giving me infinite loop.
But then the console.log() give me this error: [GraphQL error]: Message: Variable "$channelid" of required type "String!" was not provided. Path: undefined. This is very weird because my schema and variables are correct ??
The useLazyQuery does give me infinite loop as said before.
I'm really struggling with the conditionality of apollo/react hooks...
/// Channel.js component ///
const Channel = ({ id, channelName, channelDescription, authorName }) => {
const chatContext = useContext(ChatContext);
const client = useApolloClient();
const { fetchChannelInfo, setCurrentChannel } = chatContext;
const selectChannel = (e, id) => {
fetchChannelInfo(true);
const currentChannel = {
channelid: id,
channelName,
channelDescription,
authorName
};
setCurrentChannel(currentChannel);
client.writeData({
data: {
channelid: id
}
});
// console.log(currentChannel);
};
return (
<ChannelNameAndLogo onClick={e => selectChannel(e, id)}>
<ChannelLogo className='fab fa-slack-hash' />
<ChannelName>{channelName}</ChannelName>
</ChannelNameAndLogo>
);
};
export default Channel;
/// Messages.js component ///
const FETCH_CHANNELID = gql`
{
channelid #client
}
`;
const Messages = () => {
const [messageContent, setMessageContent] = useState('');
const chatContext = useContext(ChatContext);
const { currentChannel } = chatContext;
// const { data, loading, refetch } = useQuery(FETCH_MESSAGES, {
// skip: true
// });
const { data: channelidData, loading: channelidLoading } = useQuery(
FETCH_CHANNELID
);
const [fetchMessages, { data, called, loading, error }] = useLazyQuery(
FETCH_MESSAGES
);
//// useMutation is working
const [
createMessage,
{ data: MessageData, loading: MessageLoading }
] = useMutation(CREATE_MESSAGE);
if (channelidLoading && !channelidData) {
console.log('loading');
setInterval(() => {
console.log('loading ...');
}, 1000);
} else if (!channelidLoading && channelidData) {
console.log('not loading anymore...');
console.log(channelidData.channelid);
fetchMessages({ variables: { channelid: channelidData.channelid } });
console.log(data);
}
I expect to have messages in data from the useLazyQuery ...But instead get this in the console.log():
react-dom.development.js:16408 Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.
You could use the called variable return by useLazyQuery.
!called && fetchMessages({ variables: { channelid: channelidData.channelid } });
You call fetchMessages on every render.
Try to put fetchMessages in a useEffect :
useEffect(() => {
if (!channelidLoading && channelidData) {
fetchMessages();
}
}, [channelidLoading, channelidData]);
Like that the fetchMessages function only calls when
channelidLoading or channelidData is changing.
You could also look at doing the following:
import debounce from 'lodash.debounce';
...
const [fetchMessages, { data, called, loading, error }] = useLazyQuery(
FETCH_MESSAGES
);
const findMessageButChill = debounce(fetchMessages, 350);
...
} else if (!channelidLoading && channelidData) {
findMessageButChill({
variables: { channelid: channelidData.channelid },
});
}
My Problem
I am using React and Apollo in my frontend application. When I login as a regular user I want to be navigated to a certain path, but before that I need to set my providers state with my selectedCompany value. However, sometimes I get my value from my provider in my components CDM and sometimes I don't, so I have to refresh in order to get the value.
I've tried to solve this, but with no luck. So I am turning to the peeps at SO.
My setup (code)
In my login component I have my login mutation, which looks like this:
login = async (username, password) => {
const { client, qoplaStore, history } = this.props;
try {
const result = await client.mutate({
mutation: LOGIN,
variables: {
credentials: {
username,
password,
}}
});
const authenticated = result.data.login;
const { token, roles } = authenticated;
sessionStorage.setItem('jwtToken', token);
sessionStorage.setItem('lastContactWithBackend', moment().unix());
qoplaStore.setSelectedUser(authenticated);
qoplaStore.setUserSessionTTL(result.data.ttlTimeoutMs);
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany();
history.push("/admin/shops");
}
} catch (error) {
this.setState({ errorMessage: loginErrorMessage(error) });
}
};
So if my login is successful I check the users roles, and in this case, I will get into the else statement. The function getAndSetSelectedCompany look like this:
getAndSetSelectedCompany = () => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0]);
});
};
So I am fetching my companies try to set one of them in my providers state with the function setSelectedCompany. selectedValues is what im passing down from my consumer to all my routes in my router file:
<QoplaConsumer>
{(selectedValues) => {
return (
...
)
}}
</QoplaConsumer
And in my provider I have my setSelectedCompany function which looks like this:
setSelectedCompany = company => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
})
};
And my selectedValues are coming from my providers state.
And in the component that has the route I'm sending the user to I have this in it's CDM:
async componentDidMount() {
const { client, selectedValues: { selectedCompany, authenticatedUser } } = this.props;
console.log('authenticatedUser', authenticatedUser)
if (selectedCompany.id === null) {
console.log('NULL')
}
Sometimes I get into the if statement, and sometimes I don't. But I rather always come into that if statement. And that is my current problem
All the help I can get is greatly appreciated and if more info is needed. Just let me know.
Thanks for reading.
Your getAndSetSelectedCompany is asynchronous and it also calls another method that does setState which is also async.
One way to do this would be to pass a callback to the getAndSetSelectedCompany that would be passed down and executed when the state is actually set.
changes to your login component
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany(()=>history.push("/admin/shops"));
}
changes to the two methods that are called
getAndSetSelectedCompany = (callback) => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0], callback);
});
};
setSelectedCompany = (company, callback) => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
}, callback)
};
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);
}
}
})