tanstack react query mutate onSettled to access PUT body data - reactjs

Dears,
Due to some reason, I need access to the PUT request body after the PUT request settled.
Please check the sandbox example I tried to prepare.
My question is - is it ok to return the PUT params in onMutate and then do some logic in onSettled based on these params, for example selectively setting a loading state to false.
And then, why the PUT params are the 3rd argument of the onSettled function?
p.s. please don't argue about state management, the question is about onSettled usage :)
Best regards,
MJ
import React from "react";
import { useMutation } from "react-query";
const someProps = { prop1: "key1" };
export default function App() {
const [isLoading, setIsLoading] = React.useState(false);
const { mutate } = useMutation({
mutationFn: async (someProps) =>
await fetch("https://httpbin.org/put", {
method: "PUT",
body: JSON.stringify(someProps)
}).then((response) => response.json()),
onSuccess: (responseData) => {
console.log("RESPONSE ON SUCCESS: " + JSON.stringify(responseData));
},
onMutate: (data) => {
setIsLoading(true);
console.log(
"Yes, I have access to props before I send the request: " +
JSON.stringify(data)
);
// I return the data so I can use it in on settled
return data;
},
onSettled: (arg1NotUsed, arg2NotUsed, data) => {
console.log(
"Yes, I have access to props after I receive the response: " +
JSON.stringify(data)
);
if (data) {
setIsLoading(false);
}
}
});
return (
<div>
<p>is loading: {isLoading ? "LOADING" : "IDLE"}</p>
<button onClick={() => mutate(someProps)}>trigger mutation</button>
</div>
);
}

variables are available in onSettled even if you don't return them from onMutate. What onSettled receives is:
onSettled(data, error, variables, context)
where context is what you return from onSettled. In your example, you're using the 3rd parameter, which is not the value returned from onMutate, so you can safely leave that out.
There is also no need to separately track an isLoading boolean, because useMutation does this for you and also returns a loading state.
export default function App() {
const { mutate, isLoading } = useMutation({
mutationFn: async (someProps) =>
await fetch("https://httpbin.org/put", {
method: "PUT",
body: JSON.stringify(someProps)
}).then((response) => response.json()),
onSuccess: (responseData) => {
console.log("RESPONSE ON SUCCESS: " + JSON.stringify(responseData));
},
onSettled: (arg1NotUsed, arg2NotUsed, data) => {
console.log(
"Yes, I have access to props after I receive the response: " +
JSON.stringify(data)
);
}
});
return (
<div>
<p>is loading: {isLoading ? "LOADING" : "IDLE"}</p>
<button onClick={() => mutate(someProps)}>trigger mutation</button>
</div>
);
}
Here's a fork of your sandbox with these changes: https://codesandbox.io/s/usequery-forked-vq8kcr?file=/src/App.js

Related

Why is my state not being set in my functional component?

I have this small React component, and it doesn't seem to be setting the state of my authToken in the useEffect() call.
Here is my code:
const App = ({ personId, buildingId }) => {
const [authToken, setAuthToken] = useState();
useEffect(() => {
axios.post('/api/auth/' + personId + '/location/' + buildingId, {
headers: {
'Content-Type': 'application/json',
}
}).then((res) => {
setAuthToken(res.data);
})
}, []);
return (
<Editor
init={{
tinydrive_token_provider: function (success, failure) {
success({ token: authToken.token });
}}
/>
)
}
export default App;
Do I need to set it some other way?
Thanks!
Try this:
const App = ({ personId, buildingId }) => {
const handleTokenProvider = useCallback((success, failure) => {
axios.post('/api/auth/' + personId + '/location/' + buildingId, {
headers: {
'Content-Type': 'application/json',
}
}).then((res) => {
if (res && req.data && req.data.token) {
success(res.data.token);
} else {
failure("Authentication failed");
}
}).catch(err => {
failure(err);
});
}, [personId, buildingId]);
return (
<Editor
init={{
tinydrive_token_provider: handleTokenProvider
}}
/>
);
}
export default App;
The code above assumes a few things :
That the property tinydrive_token_provider detects when it's value change and calls the function again when it does. If it does not, then there must be a way to update the token. And if it still does not, then unmounting the component and re-mounting it again will force recreating it.
That you do not need the authentication token somewhere else in the component. If you do, then this solution will not be optimal. There would be ways to set a state (useState) or a reference (useRef) but there should be a way to call success/failure without requiring a component update.
That every time you render the App component, a request will be made, even if you just got the token for the same props. Having a way to store this token in a cache (e.g. localStorage) could greatly improve performance on refresh.
Alternative
If you need to have access to the token elsewhere than the Editor, this would also work :
// this function get be in a separate module!
const fetchAuthToken = (personId, buildingId) => new Promise((resolve, reject) => {
// TODO : check some cache first, and resolve if a
// previous token already exists and is not expired, etc.
axios.post('/api/auth/' + personId + '/location/' + buildingId, {
headers: {
'Content-Type': 'application/json',
}
}).then((res) => {
if (res?.data?.token) {
resolve(res.data.token);
} else {
reject("Authentication failed");
}
}).catch(err => {
reject(err);
});
});
const App = ({ personId, buildingId }) => {
const tokenPromise = useMemo(() =>
fetchAuthToken(personId, buildingId),
[personId, buildingId]
);
// DEBUG ONLY
useEffect(() => {
tokenPromise.then(token => {
console.log('Token for', personId, buildingId, 'is', token);
}, err => {
console.error('Failed to get token for', personId, buildingId);
console.error(err);
});
}, [tokenPromise, personId, buildingId]);
return (
<Editor
init={{
tinydrive_token_provider: (success, failure) => {
tokenPromise.then(success, failure);
}
}}
/>
);
}
export default App;

json response from mock server not printing but is in the console

I am trying to learn react, and I am making a successful API call, but it only prints in the console. I found examples but many of them recommended to use setData(json) but I am not able to use it because the file is a list of export async function which was also recommended.
export async function GetHellWorld() {
return fetch(`http://localhost:8080/api`, {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(response => response.json())
.then(json => {
console.log(json)
})
.catch(error => (console.log(error)))
}
and the component
function Test(thisArg, argArray) {
const result = GetHellWorld.apply()
return (
<div className="App">
{JSON.stringify(result)}
</div>
);
}
export default Test;
In the console I see "Hello World" but in the browser is get just {}.
Two questions:
How can I bind the JSON response to an object so I can do something like result.name.
Is this the correct was to call the await function? const result = GetHellWorld.apply()
---- update ----
I decided to try axios because I want to make multiple calls in one file.
const axios = require('axios');
export class AppService {
public async GetHelloWorld(): Promise<any> {
const response = await axios.get(`http://localhost:8080/api`, {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).catch(() => console.log("Issue in GetHelloWorld"))
return response.data
}
}
component
import React from 'react';
import {AppService} from "../services/app.service";
function Movies() {
const api = new AppService()
const hello = async () => {
const response = await api.GetHelloWorld();
console.log("The response: " + response)
}
return (
<div className="App">
{JSON.stringify(hello)}
</div>
);
}
note I had to add typescript support.
For whatever reason I get
Module not found: Error: Can't resolve '../services/app.service' in '/Users/miketye/programming/test-react/src/components'
While the other answer about using a custom hook can work, I would not recommend it while you're still leaning React.
Look up how to use the "useEffect" hook, that's generally how you want to do any sort of loading logic in React.
First off, you need to fix your async function so it actually returns a value:
// style/convention note, but non-component functions should not start with a capital letter
export async function getHelloWorld() {
return fetch(`http://localhost:8080/api`, {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(response => response.json())
.then(json => {
return json // will cause this function to return a Promise of type "string", since we're in an async function
})
// better to just let the error get thrown here, for testing
}
Then use it like this:
function Test(thisArg, argArray) {
[fetchResult, setFetchResult] = useState(undefined) // look up useState. State is how you have values that change over time in a resct component
useEffect(() => {
async function fetchData() {
const data = await getHelloWorld()
setFetchResult(data)
}
fetchData()
}, [])
// look up useEffect. Since the second argument (the "dependency array") is empty, useEffect will fire only once, after the component loads
return (
<div className="App">
{result ? JSON.stringify(result) : "no result yet"}
</div>
);
}
export default Test;
You can use a custom hook for this purpose:
import { useState } from "react";
const useFetchData = () => {
const [data, setData] = useState(null);
const fetchData = () => {
fetch("http://localhost:8080/api", {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(response => response.json())
.then(json => { setData(json); })
.catch(error => { console.log(error); })
}
useEffect(() => {
fetchData();
}, []);
return { data, fetchData };
}
export default useFetchData;
And then call it in your component:
import useFetchData from "#/hooks/useFetchData";
const Test = () => {
const { data, fetchData } = useFetchData();
// CALL fetchData IF YOU WANT TO UPDATE THE CURRENT STATE
return (
<div className="App">
{data && JSON.stringify(data)}
</div>
);
}
export default Test;

How to fetch api via looped callbacks with React functional components

So I have a 40+ loop that's calling another component to display images. Each image has an ID and with that ID I can get more information about the image like Name and description via another API call.
When DisplayImage gets called I want it to call another callback function that will send out API calls for that image's metadata, store it in a variable and display it as an H1 tag.
return (
<div>
{array.map(index) => {
// Some Other Code That return a TokenID //
<>
{displayImage(tokenId)}
</>
</div>
})
const displayImage = (tokenId) => {
const imageName = GetURI(tokenId)
return (
<div className="token-container">
<h1>{imageName}</h1>
<img className="artwork" width="250px" src={`https://ipfs-asdf/${tokenId}`} />
</div>
)
}
const GetURI = async (tokenId) => {
const res = await fetch("https://api"+tokenId , {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
}).then(data => {
console.log(data)
return data.json();
})
.then(data => {
return (data.name || [])
})
.catch(err => {
console.log(err);
});
}
The data is being displayed on the console but now I'm running into an infinite loop issue that I know UseEffect can solve but I can't quite figure it out. I managed to display the data on the console with UseEffect using the [] attribute but don't know how to display the data. Any help would be amazing. Thank you!
Two things useful to your situation
functions declared outside the component aren't recreated each render
useState and useEffect pairing limits calls to API to only when tokenId changes
// Put this function outside the component
// so it does not need a useCallback
// i.e not reconstructed each render of DisplayImage
const GetURI = async (tokenId) => {
...
});
const DisplayImage = (tokenId) => {
const [imageName, setImageName] = useState()
// limit calls to API to when tokenId changes
// and if eslint complains add GetURI to dependency list
// - but GetURI never changes, so no un-needed calls from it
useEffect(() => {
setImageName(GetURI(tokenId))
}, [tokenId, GetURI])
return (
<div className="token-container">
<h2>{imageName}</h2>
<img className="artwork" width="250px" src={`https://ipfs-asdf/${tokenId}`} />
</div>
)
};
You can also abstract to custom hook useImageName()
const GetURI = async (tokenId) => {
...
});
const useImageName = (tokenId) => {
const [imageName, setImageName] = useState()
useEffect(() => {
setImageName(GetURI(tokenId))
}, [tokenId, GetURI])
return imageName
})
const DisplayImage = (tokenId) => {
const imageName = useImageName(tokenId)
return (
<div className="token-container">
<h2>{imageName}</h2>
<img className="artwork" width="250px" src={`https://ipfs-asdf/${tokenId}`} />
</div>
)
};
BTW in GetURI this
return (data.name || [])
looks like should be
return data.name || ''
Is a different approach ok? I'd put display image into its own component.
const DisplayImage = ({tokenId: {_tokenId}}) => {
const imageName = GetURI(_tokenId)
const GetURI = useCallback(async () => {
await fetch("https://api"+tokenId , {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
}).then(data => {
console.log(data)
return data.json();
})
.then(data => {
return (data.name || [])
})
.catch(err => {
console.log(err);
});
})
});
useEffect(() => {
if (_tokenId) GetURI();
}, [GetURI]);
return (
<div className="token-container">
<h2>{imageName}</h2>
<img className="artwork" width="250px" src={`https://ipfs-asdf/${_tokenId}`} />
</div>
)
};
and then
return (
<div>
{array.map(index) => {
//Some Other Code//
<DisplayImage tokenId={tokenId} />
</div>
})
You should probably cache the response from GetURI(tokenId). No need to ask twice for the same URI when using the same tokenId.
An easy way is using react-query:
Setup in App.js:
// App.js
import { QueryClient, QueryClientProvider } from 'react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
Then use in a DisplayImage component (instead of inline function):
// DisplayImage.js
import { useQuery } from 'react-query'
export function DisplayImage(tokenId) {
const { isLoading, error, data: imageName } = useQuery(['images', tokenId], GetURI(tokenId))
return (
<div className="token-container">
<h1>{isLoading ? 'loading...' : imageName}</h1>
<img className="artwork" width="250px" src={`https://ipfs-asdf/${tokenId}`} />
</div>
)
}
I found the best way to go about it with everyones help on here so thanks!
I put the GetURI function inside the show image component, and had a useEffect method call GetURI every time there was a new token ID, then I set a state variable to whatever was returned.
No loops, no errors 👌
const DisplayImage = (data) => {
const [nftMetadata, setNftMetadata] = useState();
const GetURI = async (data) => {
const nftURI = await data.drizzle.contracts.Contract.methods.tokenURI(data.tokenId).call()
await fetch(nftURI , {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
"Access-Control-Allow-Origin": "*"
},
})
.then(data => {
return data.json();
})
.then(data => {
return setNftMetadata(data || []);
})
.catch(err => {
return console.log(err);
});
});
useEffect(() => {
GetURI(data);
}, [data.tokenId])
return (
<div className="token-container">
<h2>{nftMetadata.name}</h2>
<img className="artwork" width="450px" src={`https://ipfs:/whatever/${nftMetadata.image}`} />
</div>
);
};
return (
<div>
{array.map(index) => {
// Some Other Code That returns a TokenID //
<>
<DisplayImage address={drizzle.contractList[0].address} tokenId={tokenId} drizzle={drizzle} drizzleState={drizzleState}/>
</>
</div>
})

Await for completion redux-saga in react class component method

I'm sorry if this is a duplicate question, but I've searched for this question, and haven't found an answer that completely gives the solution for my issue. I want to fetch data in sagas and update localStorage after data is fetched. Then I want to this.forceUpdate() in my component method. But obviously this.forceUpdate() function is launched before data
is loaded. Can I wrap my handleSubmit class method into promise or async/await to make my this.forceUpdate() await for data to be fetched? Thank you.
FETCHING FUNCTION
export const fetchAuth = data => {
return axios({
method: "post",
url: "http://url/",
headers: {},
data: {
email: data.email,
password: data.password.toString()
}
}).then(response => {
return response.data;
});
};
REDUX-SAGA
export function* authRequestFlow(action) {
try {
const tokens = yield call(fetchAuth, action.payload);
if (tokens) {
yield call(save, "access", tokens.access);
yield call(save, "refresh", tokens.refresh);
yield call(save, "isAuthorized", true);
yield put(authSuccess());
}
} catch (error) {
yield put(authFailure(error.message));
}
}
COMPONENT
class Login extends PureComponent {
handleSubmit = () => {
authRequest();
this.forceUpdate();
};
render() {
if (localStorage.getItem("isAuthorized") === "true")
return <Redirect to="/products" />;
return (
<div className={styles.root}>
<Button
onClick={this.handleSubmit}
>
Submit
</Button>
</div>
);
}
}

Why am I unable to set an array in useEffect?

I am learning React and trying to write an asynchronous hook. Using setResult inside of useEffect doesn't seem to work. When I tried to render the result, there was nothing, so I added some console logging to see what is going on. The setter function in the useState hook doesn't seem to be doing anything. I've been following this video for some guidance, and my code does not differ too much.
I have the following component:
import React, { useState, useEffect } from 'react'
const Search = () => {
const [search, setSearch] = useState('')
const [query, setQuery] = useState('')
const [result, setResult] = useState([])
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(
`https://api.spotify.com/v1/search?q=${encodeURIComponent(
query
)}&type=track`,
{
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + auth.access_token
}
}
)
const json = await response.json()
console.log({ json })
console.log(
json.tracks.items.map(item => {
return item.id
})
)
setResult(
json.tracks.items.map(item => {
return item.id
})
)
console.log({result})
} catch (error) {
console.log(error)
}
}
if (query !== '') {
fetchData()
}
}, [query])
return (
<div>
<input
value={search}
placeholder='Search...'
onChange={event => setSearch(event.target.value)}
onKeyPress={event => {
if (event.key === 'Enter') {
setQuery(search)
}
}}
></input>
<br />
{result.map(item => (
<h3 key={item}></h3>
))}
</div>
)
}
export default Search
From console.log ({ json }), I see the response from the server looks OK.
console.log(
json.tracks.items.map(item => {
return item.id
})
)
The above console output looks OK as well.
setResult(
json.tracks.items.map(item => {
return item.id
})
)
console.log({result})
Why is result empty?
EDIT: Thanks Patrick and Talgat. I understand now. So, when I console.log outside of useEffect, I could see result is set correctly. I then realized I was missing a reference to {item} in my render:
{result.map(item => (
<h3 key={item}>{item}</h3>
))}
Now I see the IDs rendered on the page. Thanks for your help.
setTimeout(()=>{
console.log(result);
},0);
Please try this instead using console.log(result).
Setting state is not updated as soon as you insert value via setter( on your side setResult ).
So to do it, delay is needed.

Resources