Update apollo client with response of mutation - javascript

I have a login component that when submitted, calls a login mutation and retrieves the user data.
When the user data is returned, I want to set the apollo client state but I am confused by the docs on how to do so:
My component looks as so:
const LOGIN = gql`
mutation login($username: String!, $password: String!) {
login(username: $username, password: $password) {
username
fullName
}
}
`
const Login = () => {
const onSubmit = (data, login) => {
login({ variables: data })
.then(response => console.log("response", response))
.catch(err => console.log("err", err))
}
return (
<Mutation
mutation={LOGIN}
update={(cache, { data: { login } }) => {
}}
>
{(login, data) => {
return (
<Fragment>
{data.loading ? (
<Spinner />
) : (
<Form buttonLabel="Submit" fields={loginForm} onChange={() => {}} onSubmit={e => onSubmit(e, login)} />
)}
{data.error ? <div>Incorrect username or password</div> : null}
</Fragment>
)
}}
</Mutation>
)
}
As you can see, I have the update prop in my mutation which receives the login param and has the user data which I want to set in the apollo client's state.
The example here writes the response to the cache cache.writeQuery but I am unsure if this is what I want to do. Would I not want to write to client (as opposed to cache) like they do in this example where they update the local data?
The update property of mutation only seems to receive the cache param so I'm not sure if there is any way to access client as opposed to cache.
How do I go about updating my apollo client state with the response of my mutation in the update property of mutate?
EDIT: my client:
const cache = new InMemoryCache()
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
clientState: {
defaults: {
locale: "en-GB",
agent: null /* <--- update agent */
},
typeDefs: `
enum Locale {
en-GB
fr-FR
nl-NL
}
type Query {
locale: Locale
}
`
},
cache
})

Using Context API
If you have wrapped your component somewhere higher up in the hierarchy with ApolloProvider
Using ApolloConsumer
const Login = () => {
const onSubmit = async (data, login, client) => {
const response = await login({ variables: data });
if (response) {
client.whatever = response;
}
};
return (
<ApolloConsumer>
{client => (
<Mutation mutation={LOGIN}>
{login => <Form onSubmit={e => onSubmit(e, login, client)} />}
</Mutation>
)}
</ApolloConsumer>
);
};
Using withApollo
const Login = client => {
const onSubmit = async (data, login) => {
const response = await login({ variables: data });
if (response) {
client.whatever = response;
}
};
return (
<Mutation mutation={LOGIN}>
{login => <Form onSubmit={e => onSubmit(e, login)} />}
</Mutation>
);
};
withApollo(Login);
Without Context API
import { client } from "wherever-you-made-your-client";
and just reference it there

Related

No data returned from useQuery

i am new to react query
i did not get any data return by useQuery hook but api request is working fine
const listofgroupsapi = async (lastId, limit, id) => {
const res = await axios.get(
`${apiURL}/groups/getAllGroups?lastId=-1&limit=10&id=${id}`
);
console.log(res.data);
return res.data;
};
const Superadminpanel = () => {
const [lastId, setLastId] = useState(0);
const [limit, setLimit] = useState(10);
const [id, setId] = useState(cookies.id);
const { isLoading, data } = useQuery("groups", () => {
listofgroupsapi(lastId, limit, id);
});
return (
<div style={{ minHeight: "90vh" }}>
<div>
<h1>here we are</h1>
{isLoading ? <h1>loading</h1> : <h1>not loading</h1>}
{data ? <h1>data</h1> : <h1>no data:{data}</h1>}
</div>
</div>
);
};
export default Superadminpanel;
console.log(res.data) gives me correct result from my api
response of my api
I don't know why useQuery doesn't give me any data
React query dev tool image
Your main issue is that you aren't returning the promise result from listofgroupsapi() but there are also other improvements you can make.
As per the react-query documentation...
If your query function depends on a variable, include it in your query key
Since query keys uniquely describe the data they are fetching, they should include any variables you use in your query function that change
With that in mind, you should use the following
const listofgroupsapi = async (lastId, limit, id) =>
(
await axios.get(`/groups/getAllGroups`, { // no need for template literals
baseURL: apiURL,
params: { // query params are easier and safer this way
lastId: -1, // not lastId?
limit: 10, // not limit?
id,
},
})
).data;
and in your component
const { isLoading, data } = useQuery(["groups", lastId, limit, id], () =>
listofgroupsapi(lastId, limit, id) // no {...} means implicit return
);

Routing with Spotify API

I am developing a spotify clone with the ability to play a preview of the songs and display user's different top tracks and artists. I have already made standalone pages for the website after authorizing with the help spotify-web-api-node package, but i am kinda facing a problem connecting the routers, after i login with spotify i reach my profile page where i have links to other pages, but when i try to go to another page i get an error on the server that it is an invalid authorization code and on the web console, the package throws an error that no access token was provided. I have tried every possible way to correct this but i am not able to do anything. Please help me out. The relevant code as well the whole GitHub repository is linked below:
The Github repository for this project is https://github.com/amoghkapoor/Spotify-Clone
App.js
const code = new URLSearchParams(window.location.search).get("code")
const App = () => {
return (
<>
{code ?
<Router>
<Link to="/tracks">
<div style={{ marginBottom: "3rem" }}>
<p>Tracks</p>
</div>
</Link>
<Link to="/">
<div style={{ marginBottom: "3rem" }}>
<p>Home</p>
</div>
</Link>
<Switch>
<Route exact path="/">
<Profile code={code} />
</Route>
<Route path="/tracks">
<TopTracks code={code} />
</Route>
</Switch>
</Router> : <Login />}
</>
)
}
TopTracks.js
const spotifyApi = new SpotifyWebApi({
client_id: "some client id"
})
const TopTracks = ({ code }) => {
const accessToken = useAuth(code)
console.log(accessToken) // undefined in console
console.log(code) // the correct code as provided by spotify
useEffect(() => {
if (accessToken) {
spotifyApi.setAccessToken(accessToken)
return
}
}, [accessToken])
'useAuth' custom Hook
export default function useAuth(code) {
const [accessToken, setAccessToken] = useState()
const [refreshToken, setRefreshToken] = useState()
const [expiresIn, setExpiresIn] = useState()
useEffect(() => {
axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
setAccessToken(res.data.accessToken)
setRefreshToken(res.data.refreshToken)
setExpiresIn(res.data.expiresIn)
window.history.pushState({}, null, "/")
})
.catch((err) => {
// window.location = "/"
console.log("login error", err)
})
}, [code])
You don't appear to be persisting your access/refresh tokens anywhere. As soon as the component is unloaded, the data would be discarded. In addition, a sign in code is only usable once. If you use it more than once, any OAuth-compliant service will invalidate all tokens related to that code.
You can persist these tokens using localStorage, IndexedDB or another database mechanism.
For the purposes of an example (i.e. use something more secure & permanent than this), I'll use localStorage.
To help manage state across multiple views and components, you should make use of a React Context. This allows you to lift common logic higher in your component tree so that it can be reused.
Furthermore, instead of using setInterval to refresh the token periodically, you should only perform refresh operations on-demand - that is, refresh it when it expires.
// SpotifyAuthContext.js
import SpotifyWebApi from 'spotify-web-api-node';
const spotifyApi = new SpotifyWebApi({
clientId: 'fcecfc72172e4cd267473117a17cbd4d',
});
export const SpotifyAuthContext = React.createContext({
exchangeCode: () => throw new Error("context not loaded"),
refreshAccessToken: () => throw new Error("context not loaded"),
get hasToken: spotifyApi.getAccessToken() !== undefined,
api: spotifyApi
});
export const useSpotify = () => useContext(SpotifyAuthContext);
function setStoredJSON(id, obj) {
localStorage.setItem(id, JSON.stringify(obj));
}
function getStoredJSON(id, fallbackValue = null) {
const storedValue = localStorage.getItem(id);
return storedValue === null
? fallbackValue
: JSON.parse(storedValue);
}
export function SpotifyAuthContextProvider({children}) {
const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))
const hasToken = tokenInfo !== null
useEffect(() => {
if (tokenInfo === null) return; // do nothing, no tokens available
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
})
// persist tokens
setStoredJSON('myApp:spotify', tokenInfo)
}, [tokenInfo])
function exchangeCode(code) {
return axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
// TODO: Confirm whether response contains `accessToken` or `access_token`
const { accessToken, refreshToken, expiresIn } = res.data;
// store expiry time instead of expires in
setTokenInfo({
accessToken,
refreshToken,
expiresAt: Date.now() + (expiresIn * 1000)
});
})
}
function refreshAccessToken() {
return axios
.post("http://localhost:3001/refresh", {
refreshToken
})
.then(res => {
const refreshedTokenInfo = {
accessToken: res.data.accessToken,
// some refreshes may include a new refresh token!
refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
// store expiry time instead of expires in
expiresAt: Date.now() + (res.data.expiresIn * 1000)
}
setTokenInfo(refreshedTokenInfo)
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: refreshedTokenInfo.accessToken,
refreshToken: refreshedTokenInfo.refreshToken,
})
return refreshedTokenInfo
})
}
async function refreshableCall(callApiFunc) {
if (Date.now() > tokenInfo.expiresAt)
await refreshAccessToken();
try {
return await callApiFunc()
} catch (err) {
if (err.name !== "WebapiAuthenticationError")
throw err; // rethrow irrelevant errors
}
// if here, has an authentication error, try refreshing now
return refreshAccessToken()
.then(callApiFunc)
}
return (
<SpotifyAuthContext.Provider value={{
api: spotifyApi,
exchangeCode,
hasToken,
refreshableCall,
refreshAccessToken
}}>
{children}
</SpotifyAuthContext.Provider>
)
}
Usage:
// TopTracks.js
import useSpotify from '...'
const TopTracks = () => {
const { api, refreshableCall } = useSpotify()
const [ tracks, setTracks ] = useState([])
const [ error, setError ] = useState(null)
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopTracks()) // <- calls getMyTopTracks, but retry if the token has expired
.then((res) => {
if (disposed) return
setTracks(res.body.items)
setError(null)
})
.catch((err) => {
if (disposed) return
setTracks([])
setError(err)
});
return () => disposed = true
});
if (error != null) {
return <span class="error">{error.message}</span>
}
if (tracks.length === 0) {
return <span class="warning">No tracks found.</span>
}
return (<ul>
{tracks.map((track) => {
const artists = track.artists
.map(artist => artist.name)
.join(', ')
return (
<li key={track.id}>
<a href={track.preview_url}>
{track.name} - {artists}
</a>
</li>
)
}
</ul>)
}
// Login.js
import useSpotify from '...'
const Login = () => {
const { exchangeCode } = useSpotify()
const [ error, setError ] = useState(null)
const code = new URLSearchParams(window.location.search).get("code")
useEffect(() => {
if (!code) return // no code. do nothing.
// if here, code available for login
let disposed = false
exchangeCode(code)
.then(() => {
if (disposed) return
setError(null)
window.history.pushState({}, null, "/")
})
.catch(error => {
if (disposed) return
console.error(error)
setError(error)
})
return () => disposed = true
}, [code])
if (error !== null) {
return <span class="error">{error.message}</span>
}
if (code) {
// TODO: Render progress bar/spinner/throbber for "Signing in..."
return /* ... */
}
// if here, no code & no error. Show login button
// TODO: Render login button
return /* ... */
}
// MyRouter.js (rename it however you like)
import useSpotify from '...'
import Login from '...'
const MyRouter = () => {
const { hasToken } = useSpotify()
if (!hasToken) {
// No access token available, show login screen
return <Login />
}
// Access token available, show main content
return (
<Router>
// ...
</Router>
)
}
// App.js
import SpotifyAuthContextProvider from '...'
import MyRouter from '...'
const App = () => {
return (
<SpotifyAuthContextProvider>
<MyRouter />
</SpotifyAuthContextProvider>
);
}

Why cant react native find my navigation route when logging user in?

Im using react native and trying to handle authentication in my app. I'm having a problem where when the user successfully logs in, my app is unable to navigate them to the home screen. I am conditionally rendering two different types of navigators based on a value being present in AsyncStorage.
The problem seems like it's coming from the useEffect hook in app.js. When the app loads and the user is not logged in, the useEffect hook runs once. Then when I successfully login and try to navigate the user to the home screen, the app doesnt know about the <Drawer.Navigator> that I render in my else condition and because of that is not able to navigate there.
What am I doing wrong?
App.js
export default function App() {
const [token, setToken] = useState("");
useEffect(() => {
console.log("use effect");
readData();
});
const readData = async () => {
try {
const value = await AsyncStorage.getItem("token");
if (value !== null) {
setToken(value);
}
} catch (e) {
console.log(e);
}
};
return (
<AuthProvider>
<PaperProvider theme={theme}>
<NavigationContainer ref={navigationRef}>
{token == null ? (
<Stack.Navigator
initialRouteName="Login"
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="Verify" component={VerifyScreen} />
</Stack.Navigator>
) : (
<Drawer.Navigator initialRouteName="Home">
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Episodes" component={EpisodeListScreen} />
<Drawer.Screen name="Account" component={AccountScreen} />
</Drawer.Navigator>
)}
</NavigationContainer>
</PaperProvider>
</AuthProvider>
);
}
AuthContext.js
import createDataContext from "./createDataContext";
import authApi from "../api/auth";
import AsyncStorage from "#react-native-async-storage/async-storage";
import { v4 as uuid } from "uuid";
import * as RootNavigation from "../common/RootNavigation";
const authReducer = (state, action) => {
switch (action.type) {
case "add_error":
return { ...state, errorMessage: action.payload };
case "login":
return { errorMessage: "", token: action.payload };
case "logout":
return { token: null, errorMessage: "" };
case "clear_error_message":
return { ...state, errorMessage: "" };
default:
state;
}
};
const register = (dispatch) => ({ fullName, email, password }) => {
const id = uuid();
const user = { id, fullName, email, password };
authApi
.put("register", user)
.then((response) => {
if (response.status === 200) {
RootNavigation.navigate("Verify");
}
})
.catch(() => {
dispatch({
type: "add_error",
payload: "Something went wrong during registation.",
});
});
};
const login = (dispatch) => async ({ email, password }) => {
// make api request to api
try {
const user = { email, password };
const response = await authApi.post("/login", user);
await AsyncStorage.setItem("token", response.data.access_token);
dispatch({ type: "login", payload: response.data.access_token });
RootNavigation.navigate("Episodes");
} catch (e) {
dispatch({ type: "add_error", payload: e.response.data.message });
}
};
const tryLocalLogin = (dispatch) => async () => {
const token = await AsyncStorage.getItem("token");
if (token) {
dispatch({ type: "login", payload: token });
RootNavigation.navigate("Episodes");
} else {
RootNavigation.navigate("Login");
}
};
const logout = (dispatch) => async () => {
await AsyncStorage.removeItem("token");
dispatch({ type: "logout" });
RootNavigation.navigate("Login");
try {
} catch (e) {
console.log(e)
}
};
const clearErrorMessage = (dispatch) => () => {
dispatch({ type: "clear_error_message" });
};
export const { Provider, Context } = createDataContext(
authReducer,
{ register, login, logout, tryLocalLogin, clearErrorMessage },
{ token: null, errorMessage: "" }
);
Just taking a quick look at this, are you saying your issue is the Drawer.Navigator never seems to load in / work?
If so I think your issue is that every time the component loads it will assess the token as null, as it will be waiting on the Async function and you are assessing token as null on each load as its rendering before the Async returns anything.
One fix may be to use useState and set the token (or at least a boolean to indicate its present) in state. This may help, because setting state will cause a re-render and in turn assess the latest state value (which will for example be tokenState !== null) and in turn may render your drawer navigator.
If this doesn't help, could you also not generally look to use state across your app to handle if the user is signed in and get their token etc? Sure, it may occasionally mean firing an API call on app load sometimes, but I remember a while ago generally looking into Redux (there may be better cross-application app state management tools now) to replace a lot of what I was using Async Storage for.

QueryRenderer doesn't update after a mutation is committed

I am very new to relay and GraphQL and looking to emulate the behaviour of after adding a mutation, my component updates with the new mutation. For example, when I add an animal, it then shows up on my animal list.
I have read a bit about manually updating the store via optimistic updates, fetch container etc, however wondering if this behaviour can occur by using just a QueryRenderer HOC and a simple mutation.
The mutation works successfully, however I can only see the update after I refresh that page.
This is the code for the root query render:
export const Customer = ({ userId }) => {
return (
<QueryRenderer
environment={environment}
query={query}
variables={{ userId }}
render={({ error, props }) => {
//handle errors and loading
const user = props.customers_connection.edges[0].node
return (
<div className="flex justify-between items-center">
<h1 className="text-xl">Pets</h1>
<AddPetForm />
<PetList user={user} />
</div>
)
}}
/>
)
}
const query = graphql`
query CustomerQuery($userId: bigint) {
customers_connection(where: { userId: { _eq: $userId } }) {
edges {
node {
id
userId
pets {
id
animal {
...Animal_animal
}
}
}
}
}
}
`
Here is the Animal component that lives in the PetList component. The PetList component is the one I am expect to re-render, to include a new Animal component with the animal created with the mutation.
const Animal = ({ animal }) => {
return (
<li>
<Summary animal={animal} />
</li>
)
}
export default createFragmentContainer(Animal, {
animal: graphql`
fragment Animal_animal on animals {
name
category
imageUrl
weight
type
}
`
})
Here is the AddPetForm component:
const AddPetForm = () => {
const { hide } = useModal()
return (
<Modal>
<QueryRenderer
environment={environment}
query={query}
variables={{}}
render={({ error, props }) => {
//handle errors and loading
return (
<Form
hide={hide}
//pass down props from query
/>
)
}}
/>
</Modal>
)
}
const query = graphql`
query AddPetFormQuery {
enum_animal_category_connection {
edges {
node {
id
value
}
}
}
//other enums to use in the form
}
`
And the code for the mutation (this lives in the AddPetForm component):
const Form = ({ hide, ...other props }) => {
const { handleSubmit, register, errors, } = useForm({ defaultValues, resolver })
const [mutationIsInFlight, setMutationIsInFlight] = useState(false)
const [mutationHasError, setMutationHasError] = useState(false)
const onCompleted = (response, error) => {
setMutationIsInFlight(false)
if (error) onError(error)
else showSuccessNotification()
}
const onError = error => {
setMutationIsInFlight(false)
setMutationHasError(true)
}
const onSubmit = animal => {
setMutationIsInFlight(true)
setMutationHasError(false)
//assume animal has already been added
addAnimalAsPet({ animalId: animal.animalId, userId, onCompleted, onError })
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-wrap">
// all of the inputs
<div className="mt-3 w-full">
<PrimaryButton type="submit" fullWidth={true} loading={mutationIsInFlight}>
Save
</PrimaryButton>
</div>
{mutationHasError && <p className="text-red-700 my-3">An error has occurred. Please try again.</p>}
</form>
)
}
And the mutation code:
const mutation = graphql`
mutation addAnimalAsPet Mutation($animalId: Int, $userId: Int) {
insert_pets_one(object: { animalId: $animalId, userId: $userId }) {
id
}
}
`
export const addAnimalAsPet = ({ userId, animalId, onCompleted, onError }) => {
const variables = { userId, animalId }
commitMutation(environment, {
mutation,
variables,
onCompleted,
onError
})
}
Is there something obvious I am doing wrong? As you can see, there is a nested QueryRenderer HOC, is this breaking the automatic update somehow?
I have not used a subscription as I do not need real time event listening. This data would only be updated when the user adds a new pet, and this doesn't occur very frequently. And I haven't used the optimistic updates/response as there isn't really a need to show the user the update is successful before waiting for the response.
From the docs it looks like there is a way to do this using a Refetch Container, however will have to refactor my code to use a fragment.
The suggestion by xadm worked like a charm. There is a retry function that is a property of the object in the QueryRenderer render prop.
Here is a working example:
<QueryRenderer
environment={environment}
query={query}
variables={{ userId }}
render={({ error, props, retry }) => {
//handle errors and loading
const user = props.customers_connection.edges[0].node
return (
<div className="flex justify-between items-center">
<h1 className="text-xl">Pets</h1>
<AddPetForm refreshCustomerQuery={retry} />
<PetList user={user} />
</div>
)
}}
/>
I then simply call the refreshCustomerQuery function just after a successful mutation.
const onCompleted = (response, error) => {
setMutationIsInFlight(false)
if (error) onError(error)
else {
showSuccessNotification()
refreshCustomerQuery()
}
}

react-admin refresh dataGrid by custom form

First, i want to say that i'm beginner in react (and i hate front development but, you know, sometimes we don't choose in the job's life)....
So, i create a custom form with react-admin without use the REST connexion from react-admin (it's a specific form).
After the form's validation, a value named processingStatut of several data change and need to show this new value in the
<List><Datagrid> mapped by react-admin.
So i follow the documentation for create a reducer action for change a boolean value named processingStatut in my dataGrid like this:
epassesReceived.js
export const EPASSES_RECEIVED = 'EPASSES_RECEIVED';
export const epassesReceived = (data) => ({
type: EPASSES_RECEIVED,
payload: { data },
});
my customForm.js
import { epassesReceived as epassesReceivedAction } from './epassesReceived';
handleSubmit(event) {
this.setState({
post: this.post
});
const { fetchJson } = fetchUtils;
const {
showNotification,
history,
push,
epassesReceived,
fetchStart, fetchEnd
} = this.props;
const url = `${API_URL}/ePasses/update`;
const datas = JSON.stringify(this.state);
const options = {
method: 'POST',
body: datas
};
fetchStart();
fetchJson(url, options)
.then( response => epassesReceived(response.json) )
.then(() => {
showNotification('ra.notification.epasseRecorded');
history.goBack();
})
.catch( error => {
console.error(error);
var message = error.message.replace(/ /g, '');
showNotification(`ra.notification.${message}`, 'warning');
})
.finally(fetchEnd);
event.preventDefault();
}
...
const mapStateToProps = state => ({
customReducer: state.customReducer
});
export const EpassesUpdate = connect(mapStateToProps, {
epassesReceived: epassesReceivedAction,
showNotification,
push,fetchStart, fetchEnd
})(translate(withStyles(formStyle)(EpassesUpdateView)));
and in my app.js
import { EPASSES_RECEIVED } from './epassesReceived';
const customReducer = (previousState = 0, { type, payload }) => {
console.log(payload, type);
if (type == EPASSES_RECEIVED) {
// console.log('modif');
// payload.data[0].processingStatut=1; this is the purpose of the script. To show de modification changed after form's validation
return payload;
}
return previousState;
}
and the viewDataGrid.js
<List
classes={props.classes}
{...props}
exporter={exporter}
title='ePass.pageTitle'
perPage={15}
pagination={<PostPagination />}
filters={<EPassFilter businessunit={businessUnit} />}
bulkActions={<EPassBulkActions businessunit={businessUnit} />}
actions={<PostActions businessUnit={businessUnit} />}
>
<Datagrid classes={props.classes}>
{ businessUnit === undefined || !businessUnit.companyName &&
<TextField source="businessUnitName" label="ePass.businessUnitName" />
}
<StateField source="processingStatut" label="" translate={props.translate} />
.....
But in my console log my value doesn't change and i don't now why... Of course it's works if i refresh my web page by F5 because the value is changed in my database. But not in react's dataGrid... I'm lost...
maybe the log output can be helpfull:
We can see the type "EPASSES_RECEIVED" and the data changed
i think your problem comes from your fetch. Try this :
fetch(url, options)
.then( response => response.json() )
.then(data => {
epassesReceived(data);
showNotification('ra.notification.epasseRecorded');
history.goBack();
})
.catch( error => {
console.error(error);
var message = error.message.replace(/ /g, '');
showNotification(`ra.notification.${message}`, 'warning');
})
.finally(fetchEnd);

Categories