Preventing Unnecessary Requests when update the input - javascript

How to preventing unnecessary requests when update the input?
I tried below solution.But in the App file, that search is declared but never used. I tried something like: https://alligator.io/react/live-search-with-axios/.
What is the variable let token in the fileutils.js. Should I assign let token = localStorage.getItem ('token') to this variable;?
App
import search from /.utils
class App extends Component {
constructor (props) {
super(props);
this.state = {
todos: [],
}
}
search = (query) => {
axios({
url: `/api/v1/todos/{query}`,
method: "GET"
})
.then(res => {
this.setState({
todos: res.data
});
})
.catch(error => {
console.log(error);
})
render () {
return (
<input onChange={this.search} />
)
}
}
utils.js
import axios from 'axios';
const makeRequestCreator = () => {
let token;
return (query) => {
// Check if we made a request
if(token){
// Cancel the previous request before making a new request
token.cancel()
}
// Create a new CancelToken
token = axios.CancelToken.source()
try{
const res = axios(query, {cancelToken: cancel.token})
const result = data.data
return result;
} catch(error) {
if(axios.isCancel(error)) {
// Handle if request was cancelled
console.log('Request canceled', error.message);
} else {
// Handle usual errors
console.log('Something went wrong: ', error.message)
}
}
}
}
const search = makeRequestCreator()
export default search;

You can do that with a function that delays executing of your onChange.you can use debounce function from lodash.js
// _.debounce(yourSearch function, delay time);
search(e){
let str = e.target.value;
_.debounce(() => yourFunction, 500);
}

Related

How to cancel a dispatched axios request from reducer

I have used useEffects in my components to load data, moment the component mounts. But i am trying to optimize my code by avoiding any memmory leaks. To achieve this i am trying to use AbortController to cancel any request in any case if the component unmounts. Something like this
useEffect(() => {
let abortController;
(async () {
abortController = new AbortController();
let signal = abortController.signal;
// the signal is passed into the request(s) we want to abort using this controller
const { data } = await axios.get(
'https://random-data-api.com/api/company/random_company',
{ signal: signal }
);
setCompany(data);
})();
return () => abortController.abort();
}, []);
But i am finding it difficult to implement this because my axios request is in a service file which is called by a reducer in slice file.
Below is my useEffect of my Component.
// Component.js
import { bookDetails } from '../../features/user/userSlice'
//import reducer from my slice file
.
.
// code
useEffect(() => {
let mounted = true
if (mounted) {
dispatch(bookDetails(bookId))
}
return () => mounted = false
}, [])
Below is my reducer from my slice file which imports function from my service file.
// userSlice.js
import userService from "./userService";
export const bookDetails = createAsyncThunk(
"user/book",
async (id, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await userService.bookDetails({ id, token });
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
Below is my function from my service file
// userService.js
const bookDetails = async ({ id, token }) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await axios.get(API_URL + `/book/${id}`, config);
return response.data;
};
I want to cancel this request in case my component unmounts from useEffect. Please Help. Thanks in advance.
Since you're using Redux Toolkit's createAsyncThunk, it sounds like you're looking for the "canceling while running" feature of createAsyncThunk. Perhaps something like the following:
export const bookDetails = createAsyncThunk(
"user/book",
async (id, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await userService.bookDetails({ id, token, signal: thunkAPI.signal });
} catch (error) {
return thunkAPI.rejectWithValue(getErrorMessage(error));
}
}
);
useEffect(() => {
const promise = dispatch(bookDetails(bookId));
return () => promise.abort();
}, [])
However, if all you're worried about is fetching data, I strongly suggest taking a look at RTK Query, React Query, or SWR. These take care of the common pattern of asynchronously fetching data, without making you write the various slices and reducers yourself, and add useful features such as caching, retrying, etc.
Make your bookDetails method accept an extra property named signal. Then pass it to the axios method.
const bookDetails = async ({ id, token, signal }) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
signal,
};
const response = await axios.get(API_URL + `/book/${id}`, config);
return response.data;
};
For this purpose, change your reducer method like this:
async ({ id, signal}, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await userService.bookDetails({ id, token, signal });
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
Finally dispatch actions like this:
abortController = new AbortController();
let signal = abortController.signal;
dispatch(bookDetails({ id: bookId, signal: signal ))
it's a good idea to have an AbortController to avoid memory leaks when using useEffect to load data this will be useful when its possible to get errors during dispatching requests to avoid requests still running when an error occurs, here's an example that you can use to cancel requests if the state doesn't load for any reason.
import { useEffect, useState } from 'react';
const MyComponent = () => {
const [data, setData] = useState(null); // set state to null
useEffect(() => {
const controller = new AbortController(); // AbortController instance
// Dispatch the request
fetch('/data', { signal: controller.signal })
.then(response => response.json()) // get response
.then(data => setData(data)) // Updating useState
.catch(e => { // Catch errors
if (e.name === 'AbortError') {
console.log("Failed to fetching data");
}
throw e;
});
// cleanup controller
return () => {
controller.abort();
};
}, []);
// Render the component
return <div>{data && data.message}</div>;
};

error boundary when using hooks to display data

I try to get user data when load page using hooks(in case useEffect), but i got error said "Consider adding an error boundary to your tree" and my app getting blank.
my code looks similar like below
still configure out how to fix it, but i dont know how. coz i new using react
const [ProfileData, setProfileData] = useState({});
const [ownerId, setOwnerId] = useState('')
let ProfileID = 12
const getId = async () => {
const responseData = await getUserData();
setOwnerId(responseData.ID);
};
this is my function to get data from API
const getProfileData = () => {
setLoading(true);
getId();
const jsonData = {
ID: ProfileID,
OwnerId: ownerId
};
const headers = {
'Access-Control-Allow-Headers': '*'
};
try {
axios
.post(
config.API_SERVER + 'SearchProfile',
{
Data: jsonData ,
},
headers
)
.then(function (response) {
if (response.data.status == 'Success') {
setProfileData(response.data.Data);
setLoading(false);
} else {
setProfileData({});
alert(response.data.status);
}
})
} catch (error) {
console.log(error);
}
};
this is my hooks
useEffect(() => {
const load = async () => {
try {
await getProfileData();
} catch (err) {
throw err
}
}
load()
}, []);
and this is my return to display data
return (
<p>{ProfileData.Name}</p>
)
Since you throw an error (throw err) but you don't catch it, you can omit that part, pass the error message to state, or use react-error-boundary.
useEffect(() => {
getProfileData();
}, []);

Prevent submit button from firing multiple fetch requests

I have a section on a webpage for a task :
In the form i write an email which is 'validated' later with a function.First when i submit an email which passed the validation it sends a sendSubscribe function to the server,after that i click the button unsubscribe and it sends a unsubscribeUser function.But,after that,when i click on the email input,it starts to send unsubscribe fetch requests everytime and when i click on the subscribe button it also does the same.
The network tab looks like this:
I think i know which is the problem,but i dont know how to fix it.My idea is that everytime i click on the subscribe button it ataches an event listener from the function,thats why it fires multiple unsubscribe requests.
Subscribe functions : the subscribeEmail is the most important
import { validateEmail } from './email-validator.js'
import { unsubscribeUser } from './unsubscribeFetch.js'
export const subscribe = () => {
const subscribeBtn = document.getElementById('subscribeButton')
subscribeBtn.setAttribute('value', 'Unsubscribe')
document.getElementById('emailForm').style.display = 'none'
localStorage.setItem('isSubscribed', 'true')
document.getElementById('submit-info').value = ''
}
export const unsubscribe = () => {
const subscribeBtn = document.getElementById('subscribeButton')
subscribeBtn.setAttribute('value', 'Subscribe')
document.getElementById('emailForm').style.display = 'block'
localStorage.setItem('isSubscribed', 'false')
}
export const subscribeEmail = (email) => {
const isValidEmail = validateEmail(email)
if (isValidEmail === true) {
subscribe()
document.querySelector('form').addEventListener('click', function (e) {
unsubscribe()
unsubscribeUser()
localStorage.removeItem('Email')
e.stopPropagation()
})
} else if (isValidEmail === false) {
unsubscribe()
}
}
Subscribe fetch functions:
import { validateEmail } from './email-validator.js'
export const sendSubscribe = (emailInput) => {
const isValidEmail = validateEmail(emailInput)
if (isValidEmail === true) {
sendData(emailInput)
}
}
export const sendHttpRequest = (method, url, data) => {
return fetch(url, {
method: method,
body: JSON.stringify(data),
headers: data
? {
'Content-Type': 'application/json'
}
: {}
}).then(response => {
if (response.status >= 400) {
return response.json().then(errResData => {
const error = new Error('Something went wrong!')
error.data = errResData
throw error
})
}
return response.json()
})
}
const sendData = (emailInput) => {
sendHttpRequest('POST', 'http://localhost:8080/subscribe', {
email: emailInput
}).then(responseData => {
return responseData
}).catch(err => {
console.log(err, err.data)
window.alert(err.data.error)
})
}
Unsubscribe fetch function:
export const unsubscribeUser = () => {
fetch('http://localhost:8080/unsubscribe', { method: 'POST' }).then(response => { console.log(response.status) })
}
Subscribe button event listener:
document.querySelector('form').addEventListener('submit', async function (e) {
// create a variable to store localStorage email value
const introducedEmail = inputForm.value
e.preventDefault()
console.log(introducedEmail)
localStorage.setItem('Email', introducedEmail)
subscribeEmail(introducedEmail) //change the button style and set in local storage isSubscribed to true
sendSubscribe(introducedEmail) //send subscribe fetch to the server
// prevent additional requests upon clicking on "Subscribe" and "Unsubscribe".
if (isFetching) return // do nothing if request already made
isFetching = true
disableBtn()
const response = await fetchMock() //eslint-disable-line
isFetching = false
enableBtn()
})
// functions for disabling the submit button when a fetch request is in progress
const fetchMock = () => {
return new Promise(resolve => setTimeout(() => resolve('hello'), 2000))
}
const disableBtn = () => {
submitForm.setAttribute('disabled', 'disabled')
submitForm.style.opacity = '0.5'
}
const enableBtn = () => {
submitForm.removeAttribute('disabled')
submitForm.style.opacity = '1'
}
}
Could you guys please help me? I have no idea how to fix this.Thanks in advance!
I modified all your functions and fixed your problem, implemented async and await.
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
Subscribe fetch functions
import { validateEmail } from './email-validator.js'
export const sendSubscribe = async (emailInput) => {
const isValidEmail = validateEmail(emailInput) // idk if this is async func
if (isValidEmail === true) {
await sendData(emailInput);
}
}
export const sendHttpRequest = async (method, url, data) => {
return await fetch(url, {
method: method,
body: JSON.stringify(data),
headers: data
? {
'Content-Type': 'application/json'
}
: {}
}).then(response => {
if (response.status >= 400) {
return response.json().then(errResData => {
const error = new Error('Something went wrong!')
error.data = errResData
throw error
})
}
return response.json()
})
}
const sendData = async (emailInput) => {
await sendHttpRequest('POST', 'http://localhost:8080/subscribe', {
email: emailInput
}).then(responseData => {
return responseData
}).catch(err => {
console.log(err, err.data)
window.alert(err.data.error)
})
}
Unsubscribe fetch function
export const unsubscribeUser = async () => {
await fetch('http://localhost:8080/unsubscribe', { method: 'POST' }).then(response => { console.log(response.status) })
}
Subscribe button event listener
let isFetching = false;
document.querySelector('form').addEventListener('submit', async function (e) {
e.preventDefault();
if (isFetching) return // do nothing if request already made
// create a variable to store localStorage email value
const introducedEmail = inputForm.value;
console.log(introducedEmail);
localStorage.setItem('Email', introducedEmail);
// prevent additional requests upon clicking on "Subscribe" and "Unsubscribe".
disableBtn();
await fetchMock();
isFetching = true;
await subscribeEmail(introducedEmail); //change the button style and set in local storage isSubscribed to true
await sendSubscribe(introducedEmail); //send subscribe fetch to the server
// data sent, reenabling button
isFetching = true
enableBtn();
});
// functions for disabling the submit button when a fetch request is in progress
...
const fetchMock = () => {
return new Promise(resolve => setTimeout(() => resolve('hello'), 2000))
}
const disableBtn = () => {
submitForm.setAttribute('disabled', 'disabled')
submitForm.style.opacity = '0.5'
}
const enableBtn = () => {
submitForm.removeAttribute('disabled')
submitForm.style.opacity = '1'
}
}
So,like i said,the problem was in the function that attached a new event listener to the button,thats why unsubscribe request was sent everytime with +1 more request. So,i did this way :
Function for the submit button:
let isUsed = false
const submitClickButton = async () => {
// create a variable to store localStorage email value
const introducedEmail = inputForm.value
//e.preventDefault()
console.log(introducedEmail)
localStorage.setItem('Email', introducedEmail)
subscribeEmail(introducedEmail) //change the button style and set in local storage isSubscribed to true
sendSubscribe(introducedEmail) //send subscribe fetch to the server
// prevent additional requests upon clicking on "Subscribe" and "Unsubscribe".
if (isFetching) return // do nothing if request already made
isFetching = true
disableBtn()
const response = await fetchMock() //eslint-disable-line
isFetching = false
enableBtn()
isUsed = true
}
const undoClickButton = () => {
//e.preventDefault()
//unsubscribeEmail()
unsubscribeEmail()
isUsed = false
}
const toggleButton = () => {
isUsed ? undoClickButton() : submitClickButton()
}
submitForm.addEventListener('click', toggleButton, false)
And subscribeEmail function :
export const subscribeEmail = (email) => {
const isValidEmail = validateEmail(email)
if (isValidEmail === true) {
subscribe()
// document.querySelector('form').addEventListener('click', function (e) {
// unsubscribe()
// unsubscribeUser()
// localStorage.removeItem('Email')
// e.stopPropagation()
// })
} else if (isValidEmail === false) {
unsubscribe()
}
}
export const unsubscribeEmail = () => {
// const isValidEmail = validateEmail(email)
// if (isValidEmail===true){
unsubscribe()
unsubscribeUser()
localStorage.removeItem('Email')
//}
}

Return Data from Axios

I am trying to return the response from an axios API call. I don't quite get what a promise is and all the tutorials/information I find they only log the response, I want to return it.
Here is what I have, but when I call getPokemon it's undefined.
const axios = require('axios');
const getPokemon = () => {
axios.get('https://pokeapi.co/api/v2/pokemon/')
.then(function (response) {
console.log("Response:", response.data.results);
return response.data.results;
})
.catch(function (error) {
return null;
});
}
export {getPokemon};
If this is a React app then you want to do your Axios call in componentDidMount. Axios automatically returns a promise.
class Example extends Component {
constructor(props) {
super(props);
this.state = {
data: ""
};
}
componentDidMount() {
axios
.get("https://pokeapi.co/api/v2/pokemon/")
.then(res => {
console.log(res);
this.setState({
data: res.data.results
});
})
.catch(err => {
console.log(err);
});
}
render() {
let pokemon = this.state.data;
let display = Object.values(pokemon).map((item, key) => {
return (
<div>
<p>{item.name}</p>
<p>{item.url}</p>
</div>
);
});
return (
<div>{display}</div>
);
}
}
export default Example;
Doing it like this will send the Axios request after the React app has loaded and set the JSON data in the component state. You should be able to access the JSON data via this.state.data.
Check out this Codepen example with working API call.
Well, first of all, I suggest you read about promises.
a good method for achieving what you need is by using async/await syntax check out the following code:
const axios = require('axios');
const getPokemon = async () => {
try{
let res = await axios.get('https://pokeapi.co/api/v2/pokemon/');
return res.data.results;
}
catch(error){
return null //that's what you did in your code.
}
}
export {getPokemon};
Remove ".result"
const axios = require("axios");
const getPokemon = async () => {
try {
let res = await axios.get("https://jsonplaceholder.typicode.com/users");
return res.data; **here remove his .result**
} catch (error) {
return null; //that's what you did in your code.
}
};
export default getPokemon;
In index.js or any page call it:
import getPokemon from "./GetPokimon";
const xyz = async () => {
const data = await getPokemon();
alert(JSON.stringify(data));//u will see the data here
}
xyz(); //calling getPokemon()

Axios: how to cancel request inside request interceptor properly?

I want to cancel the request if there's no token, so I do like this:
instance.interceptors.request.use(config => {
if (!getToken()) {
console.log("interceptors: no access token");
} else {
config.headers.Authorization = "Bearer " + getToken().accessToken;
return config;
}
});
But in negative scenario there's an error TypeError: Cannot read property 'cancelToken' of undefined.
You cannot use the token inside the interceptors but instead throw Cancel
axios.interceptors.response.use(function (response) {
throw new axios.Cancel('Operation canceled by the user.');
}, function (error) {
return Promise.reject(error);
});
Refer to this post:
https://github.com/axios/axios/issues/583
Axios v0.22.0 and higher
As per the documentation, cancellation is now pretty straightforward with the AbortController class
instance.interceptors.request.use(config => {
/* some logic */
const controller = new AbortController();
if (needToCancelRequest) {
controller.abort();
}
return {
...config,
signal: controller.signal
};
});
Browser Compatibility
You might be tempted to do a pretty concise signal: AbortSignal.abort() instead. Please, note that it is much less supported than the solution above. See AbortSignal.abort() vs new AbortController().abort() compatibility.
Axios before v0.22.0
This is a solution taken from the axios issue on github
instance.interceptors.request.use(config => {
/* some logic */
return {
...config,
cancelToken: new CancelToken((cancel) => {
if (needToCancelRequest) {
cancel('Cancel repeated request')
}
})
};
});
So for whatever reason none of these answers worked for me. Here is what did.
axiosInstance.interceptors.request.use(
function (config) {
const controller = new AbortController();
const cfg = {
...config,
signal: controller.signal,
};
controller.abort('We gotta cancel this');
return cfg;
},
function (error) {
return Promise.reject(error);
},
);
Thing I learned from this: AbortController is native to javascript/node.
I have implemented this in this way. I am not sure if this is the best solution, but for my use case is useful.
My idea is not to cancel the last request. I would like to cancel previous requests to the same endpoint, and let the last one to do his job. For that reason I keep track of the request that are being executed.
// I keep track of the current requests that are being executed
const currentExecutingRequests = {};
axios.interceptors.request.use(
(req) => {
let originalRequest = req;
if (currentExecutingRequests[req.url]) {
const source = currentExecutingRequests[req.url];
delete currentExecutingRequests[req.url];
source.cancel();
}
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
originalRequest.cancelToken = source.token;
currentExecutingRequests[req.url] = source;
// here you could add the authorization header to the request
return originalRequest;
},
(err) => {
return Promise.reject(err);
}
);
axios.interceptors.response.use(
(response) => {
if (currentExecutingRequests[response.request.responseURL]) {
// here you clean the request
delete currentExecutingRequests[response.request.responseURL];
}
return response;
},
(error) => {
const { config, response } = error;
const originalRequest = config;
if (axios.isCancel(error)) {
// here you check if this is a cancelled request to drop it silently (without error)
return new Promise(() => {});
}
if (currentExecutingRequests[originalRequest.url]) {
// here you clean the request
delete currentExecutingRequests[originalRequest.url];
}
// here you could check expired token and refresh it if necessary
return Promise.reject(error);
}
);
As of Axios v0.22.0 an AbortSignal is the recommended way to cancel from a request interceptor.
axios.interceptors.request.use(
(requestConfig) => {
/* some logic */
return {
...requestConfig,
signal: AbortSignal.abort()
};
}
},
(error) => {
return Promise.reject(error);
}
);
#Kirill Taletski's answer solve this perfectly, but add one line:
const CancelToken = Axios.CancelToken;
then ,it gonna be like this :
instance.interceptors.request.use(config => {
/* some logic */
const CancelToken = Axios.CancelToken;
return {
...config,
cancelToken: new CancelToken((cancel) => cancel('Cancel repeated request'))
};
});
here is the solution
import axios from 'axios';
const CancelToken = axios.CancelToken;
let cancel;
axios.interceptors.request.use((config) => {
if (cancel) {
cancel(); // cancel request
}
config.cancelToken = new CancelToken(function executor(c)
{
cancel = c;
})
return config
}, function (error) {
return Promise.reject(error)
});
My solution based on https://stackoverflow.com/a/64228288/2051938
axios.ts
const axiosInstance = axios.create({ baseURL: apiBaseUrl });
axiosInstance.interceptors.request.use(
req => {
const originalRequest = req;
const cancelUniqId = (originalRequest.cancelToken as unknown) as string;
if (Object.hasOwnProperty.call(currentExecutingRequests, cancelUniqId)) {
const source = currentExecutingRequests[cancelUniqId];
delete currentExecutingRequests[cancelUniqId];
source.cancel();
}
if (cancelUniqId) {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
originalRequest.cancelToken = source.token;
currentExecutingRequests[cancelUniqId] = source;
}
return originalRequest;
},
err => {
return Promise.reject(err);
}
);
axiosInstance.interceptors.response.use(
response => {
for (const key of Object.keys(currentExecutingRequests)) {
if (currentExecutingRequests[key].token === response.config.cancelToken) {
delete currentExecutingRequests[key];
break;
}
}
return response;
},
error => {
const { response } = error;
if (axios.isCancel(error)) {
return new Promise(() => {
//
});
}
for (const key of Object.keys(currentExecutingRequests)) {
if (currentExecutingRequests[key].token === response.config.cancelToken) {
delete currentExecutingRequests[key];
break;
}
}
return Promise.reject(error);
}
);
export { axiosInstance };
Usage:
axiosInstance.request({
url: "some/req/path",
method: "POST",
params: {...},
data: {...},
cancelToken: "someUniqRequestID" // <-- IMPORTANT!
})
as a result, all requests with someUniqRequestID token will be cancelled when previous request with SAME cancelToken was not finished before.
This works for me for axios 0.20.0:
const interceptorRequestConfig = (config) => {
if (sourceRequest[config.url]) {
sourceRequest[config.url].cancel('Automatic cancellation')
}
const axiosSource = axios.CancelToken.source()
sourceRequest[config.url] = { cancel: axiosSource.cancel }
config.cancelToken = axiosSource.token
return config
}
credit for the idea: https://stackoverflow.com/a/66701130/8840359

Categories

Resources