React useEffect called twice even with strict mode disabled - javascript

I'm new to React and am working on a simple receipt scanning web app based on AWS (Amplify, AppSync, GraphQL, DynamoDB, S3). I'm using the useEffect hook to fetch data for the user currently logged in, via a GraphQL call, and am noticing duplicate runs of it. At first, there were three calls, after which I read about and disabled Strict Mode. But now, even with Strict Mode disabled, I am seeing two calls.
Debugging reveals that useEffect is called only once if I comment out setWeekly(getTotalFromItems(response)), but even as little as setWeekly() ends up creating duplicate calls.
I've perused this post as well as numerous others, but they all point to Strict Mode being the primary culprit, which is not the case here.
Network log attached below for reference.
Could someone help me understand what might be causing this double-call, and how to fix it?
import WebFont from 'webfontloader';
import React, {
useEffect,
useState
} from 'react'
import {
withAuthenticator,
Text,
View
} from '#aws-amplify/ui-react';
import {
MainLayout
} from './ui-components';
import awsExports from "./aws-exports";
Amplify.configure(awsExports);
function App({signOut, user}) {
const [weekly, setWeekly] = useState([]);
useEffect(() => {
WebFont.load({
google: {
families: ['DM Sans', 'Inter']
}
});
async function fetchWeekly(queryTemplate, queryVars) {
try {
const weeklyData = await API.graphql(graphqlOperation(queryTemplate, queryVars))
console.log(weeklyData)
const response = weeklyData.data.getUser.receiptsByPurchaseDateU.items
const total = getTotalFromItems(response) // sums 'total' in [{'date': '...', 'total': 9}, ...]
setWeekly(total)
} catch (err) {
console.log('Error fetching data.');
console.log(err)
}
}
const queryVars = {
username: user.attributes.email,
}
let d = new Date();
d.setDate(d.getDate() - 7);
d = d.toLocaleDateString('en-CA');
let tmpl = generateSummaryTemplate(d) // returns a template string based on d
fetchWeekly(tmpl, queryVars);
console.log('Complete.')
});
return ( <View >
<MainLayout/>
</View >
)
}
export default withAuthenticator(App);

The issue here is that the useEffect hook is missing a dependency array. The useEffect callback enqueues a weekly state update which triggers a component rerender and the useEffect hook is called again. This second time it again computes a value and enqueues a weekly state update. It's this second time that the state is enqueued with the same value as the current state value and React decides to bail on further rerenders. See Bailing out of State Updates.
If you update a State Hook to the same value as the current state,
React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
The solution is to add a dependency array with appropriate dependencies. Use an empty array if you want the effect to run once after the initial render when the component mounts. In this case it seems the passed user prop is the only external dependency I see at the moment. Add user to the dependency array. This is to indicate when the effect should run, i.e. after the initial mount/render and anytime user value changes. See Conditionally firing an effect.
Example:
useEffect(() => {
...
async function fetchWeekly(queryTemplate, queryVars) {
try {
const weeklyData = await API.graphql(graphqlOperation(queryTemplate, queryVars));
const response = weeklyData.data.getUser.receiptsByPurchaseDateU.items
const total = getTotalFromItems(response) // sums 'total' in [{'date': '...', 'total': 9}, ...]
setWeekly(total);
} catch (err) {
console.log('Error fetching data.');
console.log(err);
}
}
const queryVars = {
username: user.attributes.email,
};
let d = new Date();
d.setDate(d.getDate() - 7);
d = d.toLocaleDateString('en-CA');
let tmpl = generateSummaryTemplate(d); // returns a template string based on d
fetchWeekly(tmpl, queryVars);
console.log('Complete.');
}, [user]); // <-- user is external dependency

First of add a dependency array as the second argument to the useEffect, after the callback function. Moreover, I may advice that you have service functions outside the useEffect body, don't overload it like that, is not appropirate.

It’s because setWeekly() is called within your useEffect() and triggers another render. You may remove this useState() entirely and instead return the data you need in fetchWeekly().

Related

how to update or assign 2d array using useState in react

const [updatedList, setUpdatedList] = useState([[1,2],[1,2]]);
try {
const resData = await axios.post("/getlist");
if (resData.status === 200) {
setUpdatedList(resData.data.list);
}
} catch (e) {
console.log("ndsjkncjksnc");
}
console.log(updatedList);
here resData.data.list is in the form [[122,4,3,2],[5,4,3,32],[54,5,6,7,77],[2,3,5,7,5,4]]
in console.log(updatedList), I am still getting [[1,2].[1.2]]
The array is not updating to [[122,4,3,2],[5,4,3,32],[54,5,6,7,77],[2,3,5,7,5,4]]
Any help would be highly appreciated.
I tried using setUpdatedList(updatedList=>[...updatedList,resData.data.list]), It isnt working too.
****the try block is executing i.e resData.status=200
Try to create an useEffect with your updatedList as a dependency. This will print your updatedList, as soon as it has changed.
If you put your console.log after the try catch, your component will just render, start the axios request and skip directly to the console.log (which will log the initial state [1,2],[1,2]).
import { useEffect } from "react";
useEffect(() => {
console.log(updatedList);
}, [updatedList]);

How to handle (worker) jobs in React?

I have a React component with a state variable jobs. When the state variable ready is true, it should start executing jobs by a Web Worker (one at a time).
import React, { useEffect, useRef } from 'react'
// create webworker
const job_worker = new Worker("worker.bundle.js", { type: "module" });
function App() {
const [jobs, set_jobs] = React.useState([
{ ... },
{ ... },
])
const [ready, set_ready] = React.useState(false)
// start worker loop
useEffect(() => {
const worker_loop = async () => {
setTimeout(async () => {
// check if ready to execute a job
if (ready) { // <== suffers from 'stale closure'
// grab a job
const job = jobsRef.current.find(j => !j.done)
// listen for webworker results
job_worker.onmessage = (e) => {
console.log("received response from webworker: '", e.data, "'")
// SET RESULT IN JOB
// job is handled by worker; now start the worker_loop again
return worker_loop()
}
// post job to worker
job_worker.postMessage({job: job})
return // do not continue; 'onmessage' will continue the loop
}
return worker_loop()
}, 1000)
}
// start worker_loop
worker_loop()
}, [])
return (
<div>
{/* code to add jobs and set ready state */}
</div>
);
}
I tried to do this by using an (infinite) worker_loop, which is started when the React component mounts (using useEffect). The loop kinda works, but the ready variable inside the worker_loop stays at the initial state value (known as the 'stale closure' problem). Probably the same for the jobs state variable.
I've already tried to use 'createRef' as suggested here. But the problem persists. Also I feel like there is a much simpler solution.
Is there a better way to handle 'jobs' in a React-state variable? Some sort of 'job-runner process/function' with access to the React component. By the way, I am not obliged to use WebWorker.
Thanks for the comments!
It indeed makes more sense to control the jobs outside React. I solved it by creating a global state using #hookstate/core. This makes it possible to access and control the state outside of React. Much cleaner code!

react component not running the cleanup function on unmount. Working with firestore

I have a react component useEffect hook that looks like the following. I am working with firestore. I am trying to remove a value from an array in firestore when the component unmounts. However, the value is not getting removed. I tried running the firestore query in the cleanup function independently to see if that query was the problem, but it's working fine independently. It's just not getting executed when it's inside the cleanup function. I THINK the problem is that my cleanup function at the end of the useEffect hook is not getting called when the component unmounts(for example when I close the window). does anyone know what I may be doing wrong? Thank you for your help in advance
useEffect(() => {
.......
return () => {
fire.firestore().collection("ActiveUsers").doc(utilvar.teacherID).get().then((snapshot) => {
var docref = snapshot.ref;
return docref.update({
active_users : fieldValue.arrayRemove({id: currentUser.uid, name: displayName})
})
})
};
}, []);
From my observation. utilvar.teacherID might not be ready as at the time your component got mounted.
So you may want to add it to the dependable array.
Your useEffect should therefore look something like this :
useEffect(() => {
.......
return () => {
fire.firestore().collection("ActiveUsers").doc(utilvar.teacherID).get().then((snapshot) => {
if(snapshot.exist){
return snapshot.ref.update({
active_users : fieldValue.arrayRemove({id: currentUser.uid, name: displayName})
}).catch(err=>err);
})
};
}, [utilvar.teacherID]);
I added the catch as it a promise being returned. It must be handled appropriately irrespective of where it's being used.

Re-rendering the page when an element in the 2D array changes in react native expo

I want to re-run the code below to reload the page whenever the value inside the invitation[1][1] changes, I update these array values using the AsyncStorage in another page, when I tried the following I got the error below.
Is there any other way to do so?
const [invitation, setInvitation] = React.useState(Array.from({length: 2},()=> Array.from({length: 2})));
React.useEffect(()=>
{
getUserId();
},invitation[1][1]);
async function getUserId() {
var keys = ["invitationFromURL","Alert"]
try {
await AsyncStorage.multiGet(keys , (err, item) =>
{
setInvitation(item);
});
} catch (error) {
// Error retrieving data
console.log(error.message);
}
}
console.log(invitation);
console.log(invitation[1][1]);
The error I get
Warning: %s received a final argument during this render, but not during the previous render. Even though the final argument is optional, its type cannot change between renders.%s, useEffect
I'm guessing you have something like
const [invitation, setInvitation] = useState([[], []]);
up at the top of your component. That means on the first useEffect run invitation[1][1] will be null. On second run it will have a value. This is not kosher in React Native. When you call useEffect the final argument must have the same type every time the component renders.
Should be a simple fix -- instead of
useEffect(() => {
getUserId();
}, invitation[1][1]);
do
useEffect(() => {
getUserId();
}, invitation);

Rx BehaviorSubject + scan pushing prior event to new subscriber?

I want to have an stream to which I can push reducer functions. Each time a reducer function is pushed, a state object should be passed to the reducer, the reducer should return a modified state value, and the updated state should be pushed to subscribers. I'm hoping my code can explain:
import Rx from 'rx';
import { Map } from 'immutable';
let initialState = Map({ counter: 0 });
export let upstream = new Rx.BehaviorSubject(Rx.helpers.identity);
export let downstream = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState);
let increment = state => {
return state.update('counter', counter => counter + 1);
};
upstream.onNext(increment);
downstream.subscribe(state => {
console.log('subscriptionA', state.get('counter'));
});
upstream.onNext(increment);
setTimeout(() => {
downstream.subscribe(state => {
console.log('subscriptionB', state.get('counter'));
});
}, 3000);
Here's the output I see:
subscriptionA 1
subscriptionA 2
subscriptionB 1
while I was hoping to see:
subscriptionA 1
subscriptionA 2
subscriptionB 2
Obviously I'm missing something fundamental here. It seems the BehaviorSubject is supposed to retain the latest value for new subscribers which would make me think that when subscriptionB subscribes to downstream that it would get the latest reduced value, but it looks like having the .scan in the middle fouls things up...or something.
What's going on here and how to I accomplish what I'm trying to accomplish? Thanks!
Can you try to see if everything is conform to your expectations if you replace
export let downstream = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState);
by
export let downstream = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState).shareReplay(1);
jsfiddle here : http://jsfiddle.net/cqaumutp/
If so, you are another victim of the hot vs. cold nature of Rx.Observable, or maybe more accurately the lazy instantiation of observables.
In short (not so short), what happens everytime you do a subscribe, is that a chain of observables is created by going upstream the chain of operators. Each operator subscribes to its source, and returns another observable up to the starting source. In your case, when you subscribe to scan, scan subscribes to upstream which is the last one. upstream being a subject, on subscription it just registers the subscriber. Other sources would do other things (like register a listener on a DOM node, or a socket, or whatever).
The point here is that every time you subscribe to the scan, you start anew, i.e. with the initialState. If you want to use the values from the first subscription to the scan, you have to use the share operator. On the first subscription to the share, it will pass your subscription request on to the scan. On the second and subsequent ones, it will not, it will register it, and forward to the associated observer all the values coming from the scan firstly subscribed to.
I have a solution that seems to be giving me the results I'm looking for. I would appreciate it if others could verify that it's an appropriate solution.
import Rx from 'rx';
import { Map } from 'immutable';
let initialState = Map({ counter: 0 });
export let upstream = new Rx.Subject();
let downstreamSource = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState);
export let downstream = new Rx.BehaviorSubject(initialState);
downstreamSource.subscribe(downstream);
let increment = state => {
return state.update('counter', counter => counter + 1);
};
upstream.onNext(increment);
downstream.subscribe(state => {
console.log('subscriptionA', state.get('counter'));
});
upstream.onNext(increment);
setTimeout(() => {
downstream.subscribe(state => {
console.log('subscriptionB', state.get('counter'));
});
}, 3000);

Categories

Resources