Memory Leak using react useState and useEffect - javascript

I'm creating a component that will continously fetch data from an api
(on real use the response would take some time, so it wont get called all the time)
So I created this custom hook to do it
export const callUpdateApi = (url) => {
const [state, setState] = useState({ data: null, loading: true });
useEffect(() => {
setState(state => ({ data: state.data, loading: true }));
axios.get(url)
.then(x => {
return x.data
} , err => {
console.log('CALL UPDATE API ERROR ')
// timer to try again later if request fail
setTimeout(function(){
console.log('err status', err);
setState({...state , data: null})
} , 3000)
} )
.then(y => {
setState({ data: y, loading: false });
});
}, [state.data]);
return state;
};
then, if I call it the memory usage will go up as time go on.
If I set so my api responds quickly,in minutes my memory is almost full

Related

Map.prototype.size shows 0 in first render Javascript

I have a Promise that can run multiple fetch requests, after I make request, I am getting response with Map(). Then sending this data with useContext (setResponse(newResponses);). But I am having a problem because response.size equals to 0 -zero. But there is data inside. After I change something inside useEffect, the page rerenders and shows me response.size 1. I couldn't understand where I make mistake.
Promise.allSettled(promises).then((results) => {
const newResponses = new Map(response); //?
results.forEach(async (result) => {
// get url as it is used as key in the Maps
const url = new URL(result.value.url).pathname;
if (result.status === 'fulfilled' && result.value.ok) {
// get response data
const data = await result.value.json();
setRequests((prev) => {
prev.delete(url);
return prev;
});
newResponses.set(url, data); //?
} else {
setHeaderStates((prev) => ({
...prev,
reload: true,
isSaving: false,
}));
setFetchRequestData((prev) => ({
...prev,
isErrorMessageHidden: false,
errorMessage: 'Server request failed.',
}));
}
});
setResponse(newResponses);
});
code above from custom hook
code below from the page
useEffect(() => {
console.log(response.size);
}, [response, router, setResponse]);

Cleaning up a useEffect warning with useReducer,

I keep getting these warnings:
Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup
For some of my useEffects that pull data from an API with the help of my useReducer:
export default function HomeBucketsExample(props) {
const {mobileView} = props
const [allDemoBuckets, dispatchAllBuckets] = useReducer(reducer, initialStateAllBuckets)
const ListLoading = LoadingComponent(HomeBucketLists);
useEffect(() =>
{
getAllDemoBuckets(dispatchAllBuckets);
}, [])
return (
<ListLoading mobileView={ mobileView} isLoading={allDemoBuckets.loading} buckets={allDemoBuckets.data} />
);
}
However, Im not sure how to clean up this effect above, I've tried mounting it using True and False, however the error still showed up. How can I fix my function above so the useEffect doesnt throw any warnings
EDIT:
code for my reduer:
export const getAllDemoBuckets = (dispatch) => axiosInstance
.get('demo/all/')
.then(response => {
dispatch({ type: 'FETCH_SUCCESS', payload: response.data })
console.log('fired bucket-data')
})
.catch(error => {
dispatch({ type: 'FETCH_ERROR' })
})
const initialStateAllBuckets = {
loading: true,
error: '',
data: []
}
const reducer = (state, action) =>
{
switch (action.type)
{
case 'FETCH_SUCCESS':
return {
loading: false,
data: action.payload,
error: ''
}
case 'FETCH_ERROR':
return {
loading: false,
data: {},
error: "Something went wrong!"
}
default:
return state
}
}
const [allDemoBuckets, dispatchAllBuckets] = useReducer(reducer, initialStateAllBuckets)
The goal of the warning is to tell you that some action is taking place after the component is unmounted and that the result of that work is going to be thrown away.
The solution isn't to try and work around it with a reducer; the solution is to cancel whatever is happening by returning a callback from useEffect. For example:
useEffect(() => {
const ctrl = new AbortController();
fetchExternalResource(ctrl.signal);
return () => {
ctrl.abort();
}
}, []);
Using flags to determine if a component is mounted (ie using a reducer) to determine whether or not to update state is missing the point of the warning.
It's also okay to leave the warning up if this isn't actually an issue. It's just there to nit pick and tell you that, hey, you may want to clean this up. But it's not an error.
In your case, if you are using fetch, I would modify your code such that the function that dispatches actions can take an AbortSignal to cancel its operations. If you're not using fetch, there's not much you can do, and you should just ignore this warning. It's not a big deal.
It looks like you're using Axios for your requests. Axios supports a mechanism similar to abort signals - This should do the trick.
import { CancelToken } from 'axios';
const getAllDemoBuckets = async (dispatch, cancelToken) => {
try {
const response = await axiosInstance.get('/demo/all', { cancelToken });
dispatch({ type: 'FETCH_SUCCESS', payload: response.data });
} catch (err) {
if ('isCancel' in err && err.isCancel()) {
return;
}
dispatch({ type: 'FETCH_ERROR' });
}
}
const MyComponent = () => {
useEffect(() => {
const source = CancelToken.source();
getAllDemoBuckets(dispatch, source.token);
return () => {
source.cancel();
};
}, []);
}

React setState for API doesn't update even thought it gets new parameter

I'm having issues with this web app im working on.
https://food-search-octavian.netlify.com/
Now, on the recipe page https://food-search-octavian.netlify.com/recipes my search doesn't actually work. It's binded to Enter key which works, the request itself works, but when I try to set the state to the new search, it doesn't update.
This is the code in the component:
this.state = {
recipes: [],
search: "chicken",
loading: false,
height: 0
};
this.getRecipes = this.getRecipes.bind(this);
this.changeActive = this.changeActive.bind(this);
}
componentDidMount() {
this.getRecipes(this.state.search);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.search !== this.state.search) {
this.getRecipes(this.state.search);
}
}
changeActive(newSearch) {
this.setState({
search: newSearch
});
}
getRecipes = async e => {
this.setState({
loading: true
});
await recipesRequest(e).then(response => {
this.setState({
recipes: response,
loading: false
});
});
};
This is the code for the request:
const axios = require("axios");
const recipesRequest = param => {
let api_key = "*";
let api_id = "*";
return axios
.get(
`https://api.edamam.com/search?q=chicken&app_id=${api_id}&app_key=${api_key}`,
{
headers: {
"Content-Type": "application/json"
}
}
)
.then(function(response) {
return response.data.hits;
});
};
export default recipesRequest;
This is the component with the search that updates the active state in the first compomnent:
this.state = {
input: ""
};
this.checkInput = this.checkInput.bind(this);
this.newSearch = this.newSearch.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
checkInput(e) {
var value = e.target.value;
this.setState({
input: value
});
}
handleKeyDown = e => {
if (e.key === "Enter") {
console.log(e.key);
let choice = this.state.input;
this.newSearch(choice);
}
};
newSearch(choice) {
this.props.changeActive(choice);
this.setState({
input: ""
});
}
From what I read, setState is async but I kinda have the exact logic in the other page of my web app and it works.
I'm guessing it is your getRecipes function. React's setState is "asynchronous, but not in the javascript async/await sense. It is more like state updates during the current render cycle are queued up to be processed for the next render cycle.
getRecipes = async e => {
this.setState({
loading: true
});
await recipesRequest(e).then(response => {
this.setState({
recipes: response,
loading: false
});
});
};
Here you are awaiting the execution of the state updates you've queued up, which normally are processed after the function returns. Try not awaiting it.
getRecipes = e => {
this.setState({
loading: true
});
recipesRequest(e).then(response => {
this.setState({
recipes: response,
loading: false
});
});
};
This allows the fetch to occur, which is asynchronous, but it correctly allows the state updates to be queued. The state will update loading to be true, and returns the (likely) unresolved Promise, and when the request resolves (any number of render cycles later) it updates state again loading false and with the recipes.
codesandbox
EDIT
In the codesandbox both ways work, so perhaps you are not receiving/processing response data correctly, or you have a malformed request.

How can I pass a param to a promise inside usePromise React hook

I created an usePromise React Hook that should be able to resolve every kind of javascript promise and returns every result and state: data, resolving state and error.
Im able to make it work passing the function without any param, but when I try to change it to allow a param, I get an infinite loop.
const usePromise = (promise: any): [any, boolean, any] => {
const [data, setData] = useState<object | null>(null);
const [error, setError] = useState<object | null>(null);
const [fetching, setFetchingState] = useState<boolean>(true);
useEffect(() => {
setFetchingState(true);
promise
.then((data: object) => {
setData(data);
})
.catch((error: object) => {
setError(error);
})
.finally(() => {
setFetchingState(false);
});
}, [promise]);
return [data, fetching, error];
};
const apiCall = (param?: string): Promise<any> => {
return new Promise(resolve => {
setTimeout(() => {
resolve({ response: `Response generated with your param ${param}.` });
}, 500);
});
};
const App = (): React.Element => {
// How can I pass an argument to apiCall?
const [response, fetching, error] = usePromise(apiCall(5));
console.log("render"); // This logs infinitely
return <div>{JSON.stringify({ response, fetching, error })}</div>;
};
You can check the working code (without params) at: https://codesandbox.io/s/react-typescript-fl13w
And the bug at (The tab gets stuck, be adviced): https://codesandbox.io/s/react-typescript-9ow82
Note: I would like to find the solution without using a usePromise single function library from NPM or similar
Custom hooks might be executed multiple times. You should design it that way, that everything you want to do just once (e.g. the API call) is inside a useEffect hook. That can be achieved by taking a callback that gets then called in a hook.
Also, slightly more typesafe:
const usePromise = <T>(task: () => Promise<T>) => {
const [state, setState] = useState<[T?, boolean, Error?]>([null, true, null]);
useEffect(() => {
task()
.then(result => setState([result, false, null])
.catch(error => setState([null, false, error]);
}, []); // << omit the condition here, functions don't equal each other²
return state;
};
// Then used as
usePromise(() => apiCall(5));
² yes, thats generally a bad practice, but as task is not supposed to change here, I think that's fine
Upon request, here's a version that I use in some of my projects:
export function useAPI<Q, R>(api: (query: Q) => Promise<R | never>) {
const [state, setState] = useState<{ loading?: true, pending?: true, error?: string, errorCode?: number, result?: R }>({ pending: true });
async function run(query: Q) {
if(state.loading) return;
setState({ loading: true });
try {
const result = await api(query);
setState({ result });
} catch(error) {
if(error instanceof HTTPError) {
console.error(`API Error: ${error.path}`, error);
setState({ error: error.message, errorCode: error.code });
} else {
setState({ error: error.message, errorCode: NaN });
}
}
}
function reset() {
setState({ pending: true });
}
return [state, run, reset] as const;
}

Using a callback while dispatching actions

I am trying to make a GET request for some data.
Here is my action call.
componentDidMount() {
this.props.fetchData(() => {
this.setState({ isLoading: false });
});
}
Prior to completion I'd like to display "Loading..." momentarily as the fetch request is making it's trip. I'm using a callback for this and setting my local state.
Here is my action creator with a 'callback'.
export function fetchData(callback) {
return (dispatch) => {
axios.get(`/api/fetchsomething`)
.then(() => callback())
.catch((err) => {
console.log(err.message);
});
}
}
And here is that same function above but dispatching the action so that I can receive as props and render to my ui.
export function fetchData(callback) {
return (dispatch) => {
axios.get(`/api/fetchsomething`)
.then((response) => dispatch({ type: FETCH_DATA, payload: response }))
.catch((err) => {
console.log(err.message);
});
}
}
My question is how do you make the callback and dispatch the action in the same action creator function? Is that even good practice?
You could do something like this
componentDidMount() {
this.setState({ isLoading: true }, () => {
// ensuring that you make the API request only
// after the local state `isLoading` is set to `true`
this.props.fetchData().then(() => this.setState({ isLoading: false });
});
}
and, fetchData would be defined as follows
export function fetchData(callback) {
return (dispatch) => {
return axios.get(`/api/fetchsomething`)
.then((response) => dispatch({ type: FETCH_DATA, payload: response }))
.catch((err) => console.log(err.message));
}
}
If you're using the redux-thunk middleware to use asynchronous actions, then these actions will return Promises; so you can set your component's local state after that Promise resolves.
In the component:
.....
componentDidMount() {
this.props.fetchData();
}
....
export default connect((state) => ({loading: state.loading, data: state.data}))(Component);
In the actions, you should do :
....
export function fetchData() {
return (dispatch) => {
dispatch({ type: FETCHING_DATA}); //dispatch an action for loading state to set it to true
return axios.get(`/api/fetchsomething`)
.then((response) => dispatch({ type: DATA_FETCHED, payload: response }))
.catch((err) => console.log(err.message));
}
}
....
In the reducer, you should do :
....
case 'FETCHING_DATA':
return {
...state,
loading: true,
}
case 'DATA_FETCHED':
return {
...state,
data: action.payload,
loading: false,
}
....
I personally feel that you shouldn't put any business logic in your component because it can cause some problems later when you want to refactor your app. This means that there shouldn't be any .then in your component and everything should be guided through redux (if there is some side effects in your app). So, you should control your loading state from redux itself and not inside the component.

Categories