useEffect with recyclerlistview (React Native) and exhaustive-deps - reactjs

I am using a useEffect to fetch data on component render and set it to React Native RecyclerView as a data provider. I then set the state locally by appending to the existing dataProviderState. The problem is when I add the state as a dependency to the array the performance is terrible, as now the useEffect is called every time the list changes, so whenever the list is scrolled. How can I rewrite this hook to make this more performant while also following the exhaustive deps rule?
const dataProvider = new DataProvider((r1, r2) => r1 !== r2);
const rows = dataProvider.cloneWithRows([]);
const [dataProviderState, setDataProviderState] = useState(rows);
useEffect(() => {
const handleFetchContacts = async () => {
try {
const fetchedContacts = await fetchContacts();
setContacts(fetchedContacts);
setDataProviderState(dataProviderState.cloneWithRows(fetchedContacts));
} catch (error) {
logger.error(error);
}
};
const requestPermissionAndFetchContacts = async () => {
try {
const permission = await requestPermissionToContacts();
setPermissions({contacts: permission});
if (permission) {
handleFetchContacts();
}
} catch (err) {
logger.error(err);
setTimeout(() => requestPermissionAndFetchContacts(), 2000);
}
};
requestPermissionAndFetchContacts();
}, [handleFetchContacts]);
^adding dataProviderState to the dependency array here makes the list slow

I was able to fix this by referencing the previous state in the callback, so exhaustive-deps stopped complaining about me referencing the state value directly inside the component, but not having it in the array:
setDataProviderState(state => state.cloneWithRows(fetchedContacts));

Related

React Native I can not store an array with AsyncStorage

I am newbie in React Native and I am trying to store and get an array with AsyncStorage in ReactNative.
I have two problems.
First, I do not know why but when I storage data, it only works the second time but I am calling first the set of useState.
const handleAddTask = () => {
Keyboard.dismiss();
setTaskItems([...taskItems, task]);
storeData(taskItems);
};
Second, how can I call the getData function to get all the data and show it? Are there something like .onInit, .onInitialize... in ReactNative? Here is my full code
const [task, setTask] = useState();
const [taskItems, setTaskItems] = useState([]);
const handleAddTask = () => {
Keyboard.dismiss();
setTaskItems([...taskItems, task]);
storeData(taskItems);
};
const completeTask = (index) => {
var itemsCopy = [...taskItems];
itemsCopy.splice(index, 1);
setTaskItems(itemsCopy);
storeData(taskItems);
}
const storeData = async (value) => {
try {
await AsyncStorage.setItem('#tasks', JSON.stringify(value))
console.log('store', JSON.stringify(taskItems));
} catch (e) {
console.log('error');
}
}
const getData = async () => {
try {
const value = await AsyncStorage.getItem('#tasks')
if(value !== null) {
console.log('get', JSON.parse(value));
}
} catch(e) {
console.log('error get');
}
}
Updating state in React is not super intuitive. It's not asynchronous, and can't be awaited. However, it's not done immediately, either - it gets put into a queue which React optimizes according to its own spec.
That's why BYIRINGIRO Emmanuel's answer is correct, and is the easiest way to work with state inside functions. If you have a state update you need to pass to more than one place, set it to a variable inside your function, and use that.
If you need to react to state updates inside your component, use the useEffect hook, and add the state variable to its dependency array. The function in your useEffect will then run whenever the state variable changes.
Even if you're update state setTaskItems([...taskItems, task]) before save new data in local storage, storeData(taskItems) executed before state updated and save old state data.
Refactor handleAddTask as below.
const handleAddTask = () => {
Keyboard.dismiss();
const newTaskItems = [...taskItems, task]
setTaskItems(newTaskItems);
storeData(newTaskItems);
};

Use Effect and Firebase causing infinite loop

I am using firebase and trying to load all my data at the start of the app using this code:
const [books, setBooks] = useState<BookType[]>([]);
const bookCollectionRef = collection(db, "books");
useEffect(() => {
const getBooks = async () => {
const data = await getDocs(bookCollectionRef);
const temp: BookType[] = data.docs.map((doc) => {
const book: BookType = {
//set properties
};
return book;
});
setBooks(temp);
};
getBooks();
}, [bookCollectionRef]);
This useEffect is getting run constantly leading me to believe that I have made an infinite loop. I don't see why this would be happening because I don't think I am updating bookCollectionRef inside the useEffect hook. Is there possibly a problem where firebase collection references constantly get updated? Any ideas help!
From what I can tell it may be that collection(db, "books") returns a new collection reference each time the component rerenders. Any time the component renders (triggered by parent rerendering, props updating, or updating the local books state) the new bookCollectionRef reference triggers the useEffect hook callback and updates the books state, thus triggering a rerender. Rinse and repeat.
If you don't need to reference the collection outside of the useEffect hook then simply omit bookCollectionRef and reference the collection directly. Trigger the useEffect only when the db value updates.
const [books, setBooks] = useState<BookType[]>([]);
useEffect(() => {
const getBooks = async () => {
const data = await getDocs(collection(db, "books"));
const temp: BookType[] = data.docs.map((doc) => {
const book: BookType = {
//set properties
};
return book;
});
setBooks(temp);
};
getBooks();
}, [db]);
If you only need to run the effect once when the component mounts then remove all dependencies, i.e. use an empty dependency array.

How to use context values in useEffect, that only runs once

i've got an interesting problem here. I am building a react application using web socket communication with the server. I create this websocket in a useEffect hook, which therefore cannot run multiple times, otherwise i'd end up with multiple connections. In this useEffect, however i intend to use some variables,which are actually in a context (useContext) hook. And when the context values change, the values in useEffect , understandably, don't update. I've tried useRef, but didn't work. Do you have any ideas?
const ws = useRef<WebSocket>();
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
ws.current.addEventListener("message", (message) => {
const messageData: ResponseData = JSON.parse(message.data);
const { response, reload } = messageData;
if (typeof response === "string") {
const event = new CustomEvent<ResponseData>(response, {
detail: messageData,
});
ws.current?.dispatchEvent(event);
} else {
if (reload !== undefined) {
console.log("general info should reload now");
GeneralInfoContext.reload(reload);
}
console.log(messageData);
}
});
});
The web socket is stored as a ref for better use in different functions outside of this useEffect block
Note: the context value to be used is actually a function, GeneralInfoContext.reload()
Solution with split useEffect
You can split the logic that opens the websocket connection vs. the one that adds the message handler into separate useEffects - the first can run once, while the second can re-attach the event every time a dependency changes:
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
}, []);
useEffect(() => {
const socket = ws.current;
if(!socket) throw new Error("Expected to have a websocket instance");
const handler = (message) => {
/*...*/
}
socket.addEventListener("message", handler);
// cleanup
return () => socket.removeEventListener("message", handler);
}, [/* deps here*/])
The effects will run in order so the second effect will run after the first effect has already set ws.current.
Solution with callback ref
Alternatively you could put the handler into a ref and update it as necessary, and reference the ref when calling the event:
const handlerRef = useRef(() => {})
useEffect(() => {
handlerRef.current = (message) => {
/*...*/
}
// No deps here, can update the function on every render
});
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
const handlerFunc = (message) => handlerRef.current(message);
ws.current.addEventListener("message", handlerFunc);
return () => ws.current.removeEventListener("message", handlerFunc);
}, []);
It's important that you don't do addEventListener("message", handlerRef.current) as that will only attach the original version of the function - the extra (message) => handlerRef.current(message) wrapper is necessary so that every message gets passed to the latest version of the handler func.
This approach still requires two useEffect as it's best to not put handlerRef.current = /* func */ directly in the render logic, as rendering shouldn't have side-effects.
Which to use?
I like the first one personally, detaching and reattaching event handlers should be harmless (and basically 'free') and feels less complicated than adding an additional ref.
But the second one avoids the need for an explicit dependency list, which is nice too, especially if you aren't using the eslint rule to ensure exhaustive deps. (Though you definitely should be)
You can provide useEffect with a list of variables and useEffect will re-run when these variables change.
This is a little example:
const [exampleState, setExampleState] = useState<boolean>(false);
useEffect(() => {
console.log("exampleState was updated.");
}, [exampleState]);
An example from reactjs website:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
You should pass an empty array as the second parameter to the useEffect, so it this case it becomes akin to the componentDidMount() logic of react
useEffect(() => {
...your websocket code here
}, [])

React Component gets unmounted and i don't know why

I'm a completely new to the whole react world but I'm trying to develop a SPA with a integrated calendar. I'm using react-router for routing, react-big-calendar for the calendar, axios for my API calls and webpack.
Whenever I'm loading my Calender Component it gets mounted and unmounted several times and I think that causes my API call to never actually get any data. I just can't figure out what is causing this.
The Code:
useEffect(() => {
console.log("mounting Calendar")
let source = Axios.CancelToken.source()
if(!initialized) {
console.log("getting Data")
getCalendarEvents(source)
}
return () => {
console.log("unmounting Calendar")
source.cancel();
}
})
const getCalendarEvents = async source => {
setInitialized(true)
setLoading(true)
try {
const response = await getCalendar({cancelToken: source.token})
const evts = response.data.map(item => {
return {
...item,
}
})
calendarStore.setCalendarEvents(evts)
} catch (error) {
if(Axios.isCancel(error)){
console.log("caught cancel")
}else{
console.log(Object.keys(error), error.message)
}
}
setLoading(false)
}
This is the result when i render the component:
Console log
If you need any more code to assess the problem, I will post it.
I appreciate any kind of input to solve my problem.
Thank you
Its because of the useEffect. If you want it to run just once on mount you need to pass an empty array as a dependency like so :
useEffect(() => {
console.log("mounting Calendar")
let source = Axios.CancelToken.source()
if(!initialized) {
console.log("getting Data")
getCalendarEvents(source)
}
return () => {
console.log("unmounting Calendar")
source.cancel();
}
},[])
This means it will only run once. If there is some state or prop you would like to keep a watch on you could pass that in the array. What this means is that useEffect will watch for changes for whatever is passed in its dependency array and rerun if it detects a change. If its empty it will just run on mount.

How to stop memory leak in useEffect hook react

I am using Effect hook to fetch the datas from server and these data are passed to the react table there i have used the same api call to load the next set of datas from server.
When the application gets loaded i am getting an warning like below
Warning: 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 function.
Effect Hook:
useEffect(() => {
setPageLoading(true);
props
.dispatch(fetchCourses())
.then(() => {
setPageLoading(false);
})
.catch((error: string) => {
toast.error(error);
setPageLoading(false);
});
}, []);
React Table Page:
<ReactTable
className="-striped -highlight"
columns={columns}
data={coursesData}
defaultPage={currentPage}
defaultPageSize={courses.perPage}
loading={isLoading}
manual={true}
onFetchData={setFilter}
/>
Set Filter function:
const setFilter = (pagination: any) => {
props.dispatch(updateCoursePageSize(pagination.pageSize));
props.dispatch(updateCourseCurrentPage(pagination.page + 1));
setCurrentPage(pagination.page);
setPerPage(pagination.pageSize);
setLoading(true);
props.dispatch(fetchCourses()).then(() => {
setLoading(false);
});
};
Does anyone know how to clean up the hook in react
Update (June 2022):
React 18 has removed this warning message, and the workarounds to get rid of it may no longer be necessary. Part of the reason they removed it is that it has always been a bit misleading. It says you have a memory leak, but often times you don't.
The code in the question -- and indeed most code that causes this warning -- runs for a finite amount of time past the unmounting of the component, then sets state, then is done running. Since it's done running, javascript can free up variables in its closure, and thus there is usually no leak.
The case where you will have a memory leak is if you are setting up a persistent subscription which continues indefinitely. For example, maybe you set up a websocket and listen to messages, but you never tear down that websocket. These cases do need to be fixed (by supplying a cleanup function to the useEffect) but they are uncommon.
The other reason react 18 has removed the warning is that they are working on the ability for components to preserve their state after being unmounted. Once that feature is in react, setting state after unmount will be a perfectly valid thing to do.
Original answer (September 2019):
With useEffect you can return a function that will be run on cleanup. So in your case, you'll want something like this:
useEffect(() => {
let unmounted = false;
setPageLoading(true);
props
.dispatch(fetchCourses())
.then(() => {
if (!unmounted) {
setPageLoading(false);
}
})
.catch((error: string) => {
if (!unmounted) {
toast.error(error);
setPageLoading(false);
}
});
return () => { unmounted = true };
}, []);
EDIT: if you need to have a call that's kicked off outside of useEffect, then it will still need to check an unmounted variable to tell whether it should skip the call to setState. That unmounted variable will be set by a useEffect, but now you need to go through some hurdles to make the variable accessible outside of the effect.
const Example = (props) => {
const unmounted = useRef(false);
useEffect(() => {
return () => { unmounted.current = true }
}, []);
const setFilter = () => {
// ...
props.dispatch(fetchCourses()).then(() => {
if (!unmounted.current) {
setLoading(false);
}
})
}
// ...
return (
<ReactTable onFetchData={setFilter} /* other props omitted */ />
);
}
you can create a custom hook for that like that :
import * as React from 'react';
export default function useStateWhenMounted<T>(initialValue: T) {
const [state, setState] = React.useState(initialValue);
const isMounted = React.useRef(true);
React.useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const setNewState = React.useCallback((value) => {
if (isMounted.current) {
setState(value);
}
}, []);
return [state, setNewState];
}
Memory leak happens, when a thing that is unnecessary and is supposed to be cleared from memory is kept because some other thing is still holding it. In React Component case, the async call made in component may hold the references of setState or other references and will hold them until the call completes.
The warning you see is from React saying that something is still holding and setting state of a component instance that was removed from tree long back when component unmounted. Now using a flag to not set the state only removes the warning but not the memory leak, even using Abort controller does the same. To escape this situation you can use state management tools that helps dispatching an action which will do processing out side of component without holding any memory references of the component, for example redux. If you are not using such tools then you should find a way to clear the callbacks you pass to the async call (then, catch, finally blocks) when component unmounts. In the below snippet I am doing the same detaching the references to the methods passed to async call to avoid memory leaks.
Event Emitter here is an Observer, you can create one or use some package.
const PromiseObserver = new EventEmitter();
class AsyncAbort {
constructor() {
this.id = `async_${getRandomString(10)}`;
this.asyncFun = null;
this.asyncFunParams = [];
this.thenBlock = null;
this.catchBlock = null;
this.finallyBlock = null;
}
addCall(asyncFun, params) {
this.asyncFun = asyncFun;
this.asyncFunParams = params;
return this;
}
addThen(callback) {
this.thenBlock = callback;
return this;
}
addCatch(callback) {
this.catchBlock = callback;
return this;
}
addFinally(callback) {
this.finallyBlock = callback;
return this;
}
call() {
const callback = ({ type, value }) => {
switch (type) {
case "then":
if (this.thenBlock) this.thenBlock(value);
break;
case "catch":
if (this.catchBlock) this.catchBlock(value);
break;
case "finally":
if (this.finallyBlock) this.finallyBlock(value);
break;
default:
}
};
PromiseObserver.addListener(this.id, callback);
const cancel = () => {
PromiseObserver.removeAllListeners(this.id);
};
this.asyncFun(...this.asyncFunParams)
.then((resp) => {
PromiseObserver.emit(this.id, { type: "then", value: resp });
})
.catch((error) => {
PromiseObserver.emit(this.id, { type: "catch", value: error });
})
.finally(() => {
PromiseObserver.emit(this.id, { type: "finally" });
PromiseObserver.removeAllListeners(this.id);
});
return cancel;
}
}
in the useEffect hook you can do
React.useEffect(() => {
const abort = new AsyncAbort()
.addCall(simulateSlowNetworkRequest, [])
.addThen((resp) => {
setText("done!");
})
.addCatch((error) => {
console.log(error);
})
.call();
return () => {
abort();
};
}, [setText]);
I forked someones code from here to use above logic, you can check it in action in the below link
link
The other answers work of course, I just wanted to share a solution I came up with.
I built this hook that works just like React's useState, but will only setState if the component is mounted. I find it more elegant because you don't have to mess arround with an isMounted variable in your component !
Installation :
npm install use-state-if-mounted
Usage :
const [count, setCount] = useStateIfMounted(0);
You can find more advanced documentation on the npm page of the hook.

Resources