Limit Firebase rerenders with React Hooks - reactjs

I have a Firebase Realtime Database and I want to display the records by mapping a list.
So far I have:
useEffect(() => {
dbRefObject.on('value', snap => getRecords(snap.val()))
}, [dbRefObject, records])
And elsewhere I have:
export const dbRefObject = firebase.database().ref().child('record');
My getRecords() function is:
const getRecords = (snap) => {
let _recordsMap= []
for (let record in snap) {
_recordsMap.push({[record] : snap[record]})
}
I want some kind of behaviour like an unsubscribe() function returned by the useEffect, but I can't get this to work?
useEffect(() => {
const unsubscribe = () => {dbRefObject.on('value', snap => getRecords(snap.val()))}
return () => {
unsubscribe()
}
}, [dbRefObject, records])

you need to call dbRefObject.off("value", originalCallback);
check https://firebase.google.com/docs/database/admin/retrieve-data#section-detaching-callbacks

Related

How to make function with a few setStates in it synchronous?

I need to make a button click handler which have a few other function calls in it. One of them is a onAccept function which has a few setStates in it and want to wait until them all is done. Is there a way to make onAccept synchronous?
button click handler
const onUpdateBoundaries = async (recommendation) => {
await getSnippetIndex(
//some props
).then(response => {
onAccept({...recommendation, index: response});
});
fetchRecommendations() //<- this function shouldn't be called until onAccept's setStates are done
};
onAccept
const onAccept = (recommendation) => {
setAccepted((accepted) => [
...new Set([...accepted, ...recommendation.cluster_indices.map(recommendation => recommendation.index)]),
]);
setRejected((rejected) => [
...new Set(removeFromArray(rejected, recommendation.cluster_indices.map(recommendation => recommendation.index)))
]);
};
fetchRecommendations
const fetchRecommendations = async () => {
try {
const {//some props
propagated_accepted,
propagated_rejected,
} = await getRecommendations(
//some props
);
setAccepted((accepted) => [...accepted, ...propagated_accepted]);
setRejected((rejected) => [...rejected, ...propagated_rejected]);
} catch (err) {
//handling
}
setIsWaitingForRecommendations(false);
};
You can try with useEffect and useRef to achieve it
//track all previous values before state updates
const previousValues = useRef({ rejected, accepted });
useEffect(() => {
//only call `fetchRecommendations` once both `rejected` and `accepted` get updated
if(previousValues.current.rejected !== rejected && previousValues.current.accepted !== accepted) {
fetchRecommendations()
}
}, [rejected, accepted])
Another easier way that you can try setState, which is the old-school function with callback (the problem with this solution is you need to use class component - NOT function component)
const onAccept = (recommendation) => {
setState((prevState) => ({
accepted: [
...new Set([...prevState.accepted, ...recommendation.cluster_indices.map(recommendation => recommendation.index)]),
],
rejected: [
...new Set(removeFromArray(prevState.rejected, recommendation.cluster_indices.map(recommendation => recommendation.index)))
]
}), () => {
//callback here
fetchRecommendations()
})
}
React is declarative, which means it will control the setState function calls incl. batching them if necessary to optimise performance.
What you can do is make use of a useEffect to listen for changes in state and run code you need to run after state change there.
For eg: ( I'm assuming your two states are accepted and rejected)
useEffect(() => {
fetchRecommendations() //<- gets called everytime accepted or rejected changes
}, [accepted, rejected])
// onAccept remains the same
//button click handler
const onUpdateBoundaries = async (recommendation) => {
const response = await getSnippetIndex( //some props )
onAccept({...recommendation, index: response});
};
If you want to run it only if current values of accepted or rejected has changed, you can make use of use Ref to store the previous values of accepted and rejected.
You can create a custom hook like
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Then
// import usePrevious hook
const prevAccepted = usePrevious(accepted)
const prevRejected = usePrevious(rejected)
useEffect(() => {
if(prevAccepted!=accepted && prevRejected!=rejected)
fetchRecommendations() //<- gets called everytime accepted or rejected changes
}, [accepted, rejected])
const onUpdateBoundaries = async (recommendation) => {
const response = await getSnippetIndex( //some props )
onAccept({...recommendation, index: response});
};
Think something like this would do the trick. Let me know if this works :)
you can make a async method like this
const SampleOfPromise = () => {
onClick=async()=>{
await myPromise();
}
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('sample');
}, 300);
});
return(
<Button onClick={onClick}>
</Button>
)
}

React fetching data twice [duplicate]

This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 1 year ago.
I am trying to fetch some data from google's firestore in an useEffect, and saving it in useState variable
useEffect(() => {
const fetchFirst = async () => {
...
// setting the new found data in a useState
setData1(data1)
}
fetchFirst()
}, [])
Now, I want to fetch some other data from firestore, but this data requires some information from previous fetched (fetchFirst) data. I tried to do this but does not work
useEffect(() => {
const fetchFirst = async () => {
...
// setting the new found data in a useState
setData1(data1)
}
const fetchSecond = async (data1) => {
...
// setting the new found data in a useState
setData2(data2)
}
fetchFirst()
fetchSecond(data1)
}, [])
My first fetch works completely fine, but when my code reaches the second fetch, the input data (data1) is null. Can someone please help me figure it out. Thanks
If your using async you should wait, with the await keyword, for the first fetching to finish, then use its result in the fetchSecond:
useEffect(() => {
const fetchFirst = async (): SomeData => {
const data = await fetch(...);
return data;
};
const fetchSecond = async (data: SomeData) => {
await fetch(...);
};
const fetchAllData = async () => {
const data = await fetchFirst();
await fetchSecond();
};
fetchAllData();
}, []);
Both function are async. You need to call fetchSecond when data1 value changes:
useEffect(() => {
const fetchFirst = async () => {
...
// setting the new found data in a useState
setData1(data1)
}
fetchFirst()
}, []);
useEffect(() => {
const fetchSecond = async (data1) => {
...
// setting the new found data in a useState
setData2(data2)
}
fetchSecond(data1)
}, [data1]);
Or call fetchSecond inside then block
useEffect(() => {
const fetchFirst = async () => {
...
// setting the new found data in a useState
setData1(data1);
return data1 //--> return data value
}
const fetchSecond = async (data1) => {
...
// setting the new found data in a useState
setData2(data2)
}
fetchFirst().then(data => fetchSecond(data));
}, []);
You can simply call your second inside your first function call and pass data that you're setting in the state rather than passing state data.
useEffect(() => {
const fetchFirst = async () => {
...
// calling the function with new found data
fetchSecond(data1)
// setting the new found data in a useState
setData1(data1)
}
const fetchSecond = async (data1) => {
...
// setting the new found data in a useState
setData2(data2)
}
fetchFirst()
}, [])

Array of hooks in React

I'm starting to use hooks in React and I got stuck, when I realized I would need an array of hooks to solve my problem. But according to the Rules of Hooks
Only Call Hooks at the Top Level
I'm not allow to call hooks inside a loop (and I guess also not in map).
My custom hook subscribes to an API and adds data to the state when there is an update:
export const useTrace = (id) => {
[trace, setTrace] = useState([])
useEffect(() => {
Api.getCurrentTrace(id)
.then(currentTrace => {
setTrace(currentTrace)
})
}, [id])
useEffect(() => {
Api.subscribeTraceUpdate(onUpdateTrip)
return () => {
Api.unsubscribeTraceUpdate(onUpdateTrip)
}
}, [])
const onUpdateTrip = msg => {
if (msg.id === id) {
setTrace([msg.data].concat(trace))
}
}
}
In my component I have a state with an array of IDs. For each ID I would like to use the useTrace(id) hook somehow like this:
import DeckGL from '#deck.gl/react'
function TraceMap({ ids }) {
const data = ids.map((id) => ({
id,
path: useTrace(id)
}))
const pathLayer = new PathLayer({
id: 'path-layer',
data,
getPath: d => d.path
})
return <DeckGL
layers={[ pathLayer ]}
/>
}
For the sake of simplicity I got ids as a property instead of having a state.
Why not have a useTraces custom hook rather than useTrace. This new hook can take an array of ids instead of a single id.
export const useTraces = (ids) => {
[traces, setTraces] = useState([]);
useEffect(() => {
(async () => {
const traces = await Promise.all(
ids.map((id) => Api.getCurrentTrace(id))
);
setTraces(traces);
})();
}, [ids]);
// ...
};

Hooks in react are not updating my state correctly

I'm trying to load some data which I get from an API in a form, but I seem to be doing something wrong with my state hook.
In the code below I'm using hooks to define an employee and employeeId.
After that I'm trying to use useEffect to mimic the componentDidMount function from a class component.
Once in here I check if there are params in the url and I update the employeeId state with setEmployeeId(props.match.params.employeeId).
The issue is, my state value didn't update and my whole flow collapses.
Try to keep in mind that I rather use function components for this.
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
}
// This remains -1, while it should be the params.employeeId
if (employeeId) {
getEmployee();
}
}
const getEmployee = () => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId) // --> This will return an invalid employee
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The new value from setEmployeeId will be available probably in the next render.
The code you're running is part of the same render so the value won't be set yet.
Since you're in the same function, use the value you already have: props.match.params.employeeId.
Remember, when you call set* you're instructing React to queue an update. The update may happen when React decides.
If you'd prefer your getEmployee to only run once currentEmployeeId changes, consider putting that in its own effect:
useEffect(() => {
getEmployee(currentEmployeeId);
}, [currentEmployeeId])
The problem seems to be that you are trying to use the "updated" state before it is updated. I suggest you to use something like
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
let currentEmployeeId
if (props.match.params && props.match.params.employeeId) {
currentEmployeeId = props.match.params.employeeId
setEmployeeId(currentEmployeeId)
}
// This was remaining -1, because state wasn't updated
if (currentEmployeeId) {
getEmployee(currentEmployeeId);
//It's a good practice to only change the value gotten from a
//function by changing its parameter
}
}
const getEmployee = (id: number) => {
setIsLoading(true);
EmployeeService.getEmployee(id)
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The function returned from useEffect will be called on onmount. Since you're using implicit return, that's what happens in your case. If you need it to be called on mount, you need to call it instead of returning.
Edit: since you also set employee id, you need to track in the dependency array. This is due to the fact that setting state is async in React and the updated state value will be available only on the next render.
useEffect(() => {
componentDidMount()
}, [employeeId]);
An alternative would be to use the data from props directly in the getEmployee method:
useEffect(() => {
componentDidMount()
}, []);
const componentDidMount = () => {
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
getEmployee(props.match.params.employeeId);
}
}
const getEmployee = (employeeId) => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId);
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}

React-hooks. Can't perform a React state update on an unmounted component

I get this error:
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.
when fetching of data is started and component was unmounted, but function is trying to update state of unmounted component.
What is the best way to solve this?
CodePen example.
default function Test() {
const [notSeenAmount, setNotSeenAmount] = useState(false)
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [])
async function updateNotSeenAmount() {
let data // here i fetch data
setNotSeenAmount(data) // here is problem. If component was unmounted, i get error.
}
async function anotherFunction() {
updateNotSeenAmount() //it can trigger update too
}
return <button onClick={updateNotSeenAmount}>Push me</button> //update can be triggered manually
}
The easiest solution is to use a local variable that keeps track of whether the component is mounted or not. This is a common pattern with the class based approach. Here is an example that implement it with hooks:
function Example() {
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
let isCancelled = false;
simulateSlowNetworkRequest().then(() => {
if (!isCancelled) {
setText("done!");
}
});
return () => {
isCancelled = true;
};
}, []);
return <h2>{text}</h2>;
}
Here is an alternative with useRef (see below). Note that with a list of dependencies this solution won't work. The value of the ref will stay true after the first render. In that case the first solution is more appropriate.
function Example() {
const isCancelled = React.useRef(false);
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
fetch();
return () => {
isCancelled.current = true;
};
}, []);
function fetch() {
simulateSlowNetworkRequest().then(() => {
if (!isCancelled.current) {
setText("done!");
}
});
}
return <h2>{text}</h2>;
}
You can find more information about this pattern inside this article. Here is an issue inside the React project on GitHub that showcase this solution.
If you are fetching data from axios(using hooks) and the error still occurs, just wrap the setter inside the condition
let isRendered = useRef(false);
useEffect(() => {
isRendered = true;
axios
.get("/sample/api")
.then(res => {
if (isRendered) {
setState(res.data);
}
return null;
})
.catch(err => console.log(err));
return () => {
isRendered = false;
};
}, []);
TL;DR
Here is a CodeSandBox example
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.
Here is a simple solution for this. This warning is due to when we do some fetch request while that request is in the background (because some requests take some time.)and we navigate back from that screen then react cannot update the state. here is the example code for this. write this line before every state Update.
if(!isScreenMounted.current) return;
Here is Complete Example
import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
return () => isScreenMounted.current = false
},[])
const ConvertFileSubmit = () => {
if(!isScreenMounted.current) return;
setUpLoading(true)
var formdata = new FormData();
var file = {
uri: `file://${route.params.selectedfiles[0].uri}`,
type:`${route.params.selectedfiles[0].minetype}`,
name:`${route.params.selectedfiles[0].displayname}`,
};
formdata.append("file",file);
fetch(`${BASEURL}/UploadFile`, {
method: 'POST',
body: formdata,
redirect: 'manual'
}).then(response => response.json())
.then(result => {
if(!isScreenMounted.current) return;
setUpLoading(false)
}).catch(error => {
console.log('error', error)
});
}
return(
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Text>Search Screen</Text>
</ScrollView>
</SafeAreaView>
</>
)
}
export default SearchScreen;
const styles = StyleSheet.create({
scrollView: {
backgroundColor:"red",
},
container:{
flex:1,
justifyContent:"center",
alignItems:"center"
}
})
This answer is not related to the specific question but I got the same 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. and as a React newcomer could not find a solution to it.
My problem was related to useState in an unmounted component.
I noticed that I was calling a set state function (setIsLoading) after the function that unmounted my component:
const Login = () => {
const [isLoading, setIsLoading] = useState(false);
const handleLogin = () => {
setIsLoading(true);
firebase.auth().then(
functionToUnMountLoginSection();
// the problem is here
setIsLoading(false);
)
}
}
The correct way is to call setIsLoading when the component is still mounted, before calling the function to unmount/process user login in my specific case:
firebase.auth().then(
setIsLoading(false);
functionToUnMountLoginSection();
)
You add the state related datas into the useEffect body for not rerunning them every rerendering process. This method will solve the problem.
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [notSeenAmount])
REF: Tip: Optimizing Performance by Skipping Effects
Custom Hook Solution (ReactJs/NextJs)
Create a new folder named 'shared' and add two folders named 'hooks', 'utils' in it. Add a new file called 'commonFunctions.js' inside utils folder and add the code snippet below.
export const promisify = (fn) => {
return new Promise((resolve, reject) => {
fn
.then(response => resolve(response))
.catch(error => reject(error));
});
};
Add a new file called 'fetch-hook.js' inside hooks folder and add the code snippet below.
import { useCallback, useEffect, useRef } from "react";
import { promisify } from "../utils/commonFunctions";
export const useFetch = () => {
const isUnmounted = useRef(false);
useEffect(() => {
isUnmounted.current = false;
return () => {
isUnmounted.current = true;
};
}, []);
const call = useCallback((fn, onSuccess, onError = null) => {
promisify(fn).then(response => {
console.group('useFetch Hook response', response);
if (!isUnmounted.current) {
console.log('updating state..');
onSuccess(response.data);
}
else
console.log('aborted state update!');
console.groupEnd();
}).catch(error => {
console.log("useFetch Hook error", error);
if (!isUnmounted.current)
if (onError)
onError(error);
});
}, []);
return { call }
};
Folder Structure
Our custom hook is now ready. We use it in our component like below
const OurComponent = (props) => {
//..
const [subscriptions, setSubscriptions] = useState<any>([]);
//..
const { call } = useFetch();
// example method, change with your own
const getSubscriptions = useCallback(async () => {
call(
payment.companySubscriptions(userId), // example api call, change with your own
(data) => setSubscriptions(data),
);
}, [userId]);
//..
const updateSubscriptions = useCallback(async () => {
setTimeout(async () => {
await getSubscriptions();
}, 5000);// 5 seconds delay
}, [getSubscriptions]);
//..
}
In our component, we call 'updateSubscriptions' method. It will trigger 'getSubscriptions' method in which we used our custom hook. If we try to navigate to a different page after calling updateSubscriptions method before 5 seconds over, our custom hook will abort state update and prevent that warning on the title of this question
Wanna see opposite?
Change 'getSubscriptions' method with the one below
const getSubscriptions = useCallback(async () => {
const response = await payment.companySubscriptions(userId);
setSubscriptions(response);
}, [userId]);
Now try to call 'updateSubscriptions' method and navigate to a different page before 5 seconds over
Try this custom hook:
import { useEffect, useRef } from 'react';
export const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
function Example() {
const isMounted = useIsMounted();
const [text, setText] = useState();
const safeSetState = useCallback((callback, ...args) => {
if (isMounted.current) {
callback(...args);
}
}, []);
useEffect(() => {
safeSetState(setText, 'Hello')
});
}, []);
return <h2>{text}</h2>;
}

Resources