MutationObserver is reading old React State - reactjs

I'm attempting to use a MutationObserver with the Zoom Web SDK to watch for changes in who the active speaker is. I declare a state variable using useState called participants which is meant to hold the information about each participant in the Zoom call.
My MutationObserver only seems to be reading the initial value of participants, leading me to believe the variable is bound/evaluated rather than read dynamically. Is there a way to use MutationObserver with React useState such that the MutationCallback reads state that is dynamically updating?
const [participants, setParticipants] = useState({});
...
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if(name in participants) {
// do something
} else {
setParticipants({
...participants,
[name] : initializeParticipant(name)
})
}
})
}
...
useEffect(() => {
if(!speechObserverOn) {
setSpeechObserverOn(true);
const speechObserver = new MutationObserver(onSpeechMutation);
const speechConfig = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
subtree: true,
}
const participantsList = document.querySelector('.participants-selector');
if(participantsList) {
speechObserver.observe(participantsList, speechConfig);
}
}
}, [speechObserverOn])

If you are dealing with stale state enclosures in callbacks then generally the solution is to use functional state updates so you are updating from the previous state and not what is closed over in any callback scope.
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if (name in participants) {
// do something
} else {
setParticipants(participants => ({
...participants, // <-- copy previous state
[name]: initializeParticipant(name)
}));
}
})
};
Also, ensure to include a dependency array for the useEffect hook unless you really want the effect to trigger upon each and every render cycle. I am guessing you don't want more than one MutationObserver at-a-time.
useEffect(() => {
if(!speechObserverOn) {
setSpeechObserverOn(true);
const speechObserver = new MutationObserver(onSpeechMutation);
const speechConfig = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
subtree: true,
}
const participantsList = document.querySelector('.participants-selector');
if(participantsList) {
speechObserver.observe(participantsList, speechConfig);
}
}
}, []); // <-- empty dependency array to run once on component mount
Update
The issue is that if (name in participants) always returns false
because participants is stale
For this a good trick is to use a React ref to cache a copy of the current state value so any callbacks can access the state value via the ref.
Example:
const [participants, setParticipants] = useState([.....]);
const participantsRef = useRef(participants);
useEffect(() => {
participantsRef.current = participants;
}, [participants]);
...
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if (name in participantsRef.current) {
// do something
} else {
setParticipants(participants => ({
...participants,
[name]: initializeParticipant(name)
}));
}
})
};

Related

Stop useEffect if a condition is met

I have a useEffect set up how I thought would only run once on initial render but it continues to rerun.
This breaks a function that is supposed to set a piece of state to true if a condition is truthy and show appropriate UI.
This sort of works but then the useEffect runs again flicks back to false immediately. I am also using a use effect to check on first render if the condition is truthy and show appropriate UI if so.
Basically when setIsPatched is true I don't want the useEffect to rerun because it flicks it back to false and breaks the UI
Here is the function:
const [isPatched, setIsPatched] = useState<boolean>(false);
useEffect(() => {
getApplied(x);
}, []);
const getApplied = (x: any) => {
console.log(x);
if (x.Log) {
setIsPatched(true);
return;
} else {
setIsPatched(false);
}
};
I also pass getApplied() to child component which passes a updated data to the function for use in this parent component:
const updatePatch = async (id: string) => {
//check if patch already in db
const content = await services.data.getContent(id);
const infoToUpdate = content?.data[0] as CN;
if (!infoToUpdate.applyLog && infoToUpdate.type == "1") {
// add applyLog property to indicate it is patched and put in DB
infoToUpdate.applyLog = [
{ clID: clID ?? "", usID: usID, appliedAt: Date.now() },
];
if (content) {
await services.data
.updateX(id, content, usId)
.then(() => {
if (mainRef.current) {
setDisabled(true);
}
});
}
getApplied(infoToUpdate);
} else {
console.log("retrying");
setTimeout(() => updatePatch(id), 1000); // retries after 1 second delay
}
};
updatePatch(id);
}

useState not updating rendered values after getting document snapshots

I am having a problem assigning data to useState by fetching data using reference type value from firebase.
const [preOil, setPreOil] = useState([]);
const [oilChange, setOilChange] = useState([]);
useEffect(() => {
getDocs(query(collection(db, "oil"), orderBy("timestamp"))).then(
(snapshot) => {
setPreOil(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}))
);
}
);
}, []);
useEffect(() => {
let current = preOil.length > 0 ? [...preOil] : [];
current.map((_oil, i) => {
getDoc(_oil.oilReference).then((oilRef) => {
current[i].oilReference = oilRef.data();
});
});
setOilChange(current);
}, [preOil]);
In the first useEffect, the data is fetched successfully in this form,
preOil = {
id:"RxbAOAIs8d3kGOHNskJ4",
oilReference: Ta {converter: null, _key: ut, type: 'document', firestore: Na},
customerName: "USAMA",
}
In the second useEffect based on [preOil], I need to reassign the oilReference with the data fetched from firestorm through its reference(oilReference), The data is successfully fetched from the database and assigned to current but The main problem is when I set to state setOilChange(current) it updates my oilChange state when I inspect in inspect tools in chrome but in JSX the changes don't reflect
I am updating the state in useEffect
I am having desired data assigned in a local variable and assign that variable to the state
Then What am I missing?
In your second useEffect(), more specifically, in
current.map((_oil, i) => {
getDoc(_oil.oilReference).then((oilRef) => {
current[i].oilReference = oilRef.data();
});
});
setOilChange(current);
You are mutating the content of the current variable. This mutation, because it is async, will happen after the setOilChange call. Such mutation will thus not trigger a re-render.
What you need is to instead first wait for all the docs to be loaded and only after that set the state. Example:
const _docs = current.map((_oil, i) => {
return getDoc(_oil.oilReference).then((oilRef) => { // changed here
return { // changed here
...current[i], // changed here
oilReference: oilRef.data() // changed here
} // changed here
}); // changed here
});
Promise.all(_docs).then(() => {
setOilChange(_docs);
});
Also notice I didn't mutate current, rather I returned a new object. This is not mandatory but just a best practice overall.

Why useSelector is not selecting redux state

(state) => state?.clientAddress?.getAddress
);
useEffect(() => {
dispatch(GET_CLIENT_ADDRESS({}))
}, [dispatch]);
const onSavedAddressSelect = (addressId) => {
debugger
if (addressId) {
const { data: selectedAddress = [] } = clientAddress ?? {} ;
console.clear()
console.log(selectedAddress)
const addressObj = selectedAddress?.filter(
(addressItem) => addressItem?.value === addressId
)?.[0];
setAddress(addressObj);
setSelectedVenue(addressObj?.value);
}
props.drawer.setDrawer({
open: false,
});
};
My clientAddress inside onSavedAddressSelect is not getting updated with latest redux state even if I dispatch on first render on useEffect.
Please help.
isn't clientAddress is not updated because you call
dispatch(GET_CLIENT_ADDRESS({})) with empty object, so it's stays the same? Try to pass there some object with values

Using useState of complex object not working as expected in ReactJS

I have a function component and I am declaring a useState for a complex object like this:
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
I now try to set the state of the order by calling setOrder in a useEffect like this:
useEffect(() => {
(async function() {
let dh = new DataInterface("some string");
let errMsg = "";
// Get the sales order.
try
{
await dh.FetchOrder();
}
catch(error: any)
{
errMsg = error;
};
setOrder(salesOrder => ({...salesOrder, IsRetrieving: false, ErrorMsg: errMsg, DataInterface: dh}));
})();
}, []);
As is, this seems to work fine. However, I have a setInterval object that changes the screen message while order.IsRetrieving is true:
const [fetchCheckerCounter, setFetchCheckerCount] = useState<number>(0);
const statusFetcherWatcher = setInterval(() => {
if (order.IsRetrieving)
{
if (fetchCheckerCounter === 1)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "This can take a few seconds..."}));
}
else if (fetchCheckerCounter === 2)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "Almost there!.."}));
}
setFetchCheckerCount(fetchCheckerCounter + 1);
}
else
{
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
The issue is that order.IsRetrieving is always true for that code block, even though it does change to false, and my website changes to reflect that, even showing the data from dh.FetchOrder(). That means my timer goes on an infinite loop in the background.
So am I setting the state of order correctly? It's incredibly difficult to find a definite answer on the net, since all the answers are invariably about adding a new item to an array.
Issues
You are setting the interval as an unintentional side-effect in the function body.
You have closed over the initial order.isRetreiving state value in the interval callback.
Solution
Use a mounting useEffect to start the interval and use a React ref to cache the state value when it updates so the current value can be accessed in asynchronous callbacks.
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
const orderRef = useRef(order);
useEffect(() => {
orderRef.current = order;
}, [order]);
useEffect(() => {
const statusFetcherWatcher = setInterval(() => {
if (orderRef.current.IsRetrieving) {
if (fetchCheckerCounter === 1) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "This can take a few seconds...",
}));
} else if (fetchCheckerCounter === 2) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "Almost there!..",
}));
}
setFetchCheckerCount(counter => counter + 1);
} else {
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
return () => clearInterval(statusFetcherWatcher);
}, []);

Setting the state several times inside useEffect sets the last one only

I got a component which has a form to add a new item and its supposed to also update an existing item. I'm trying to set the value of the form fields such that if a user chooses to edit an item, he will have all of the data of the existing item already in the form, which he just needs to edit.
I'm using useEffect for that:
useEffect(() => {
if(props.editedItem)
{
inputChangedHandler(props.editedItem.companyName, "company");
inputChangedHandler(props.editedItem.name, "name");
inputChangedHandler(props.editedItem.description, "description");
}
}, [props.editedItem])
the method inputChangedHandler is setting the form value of a specific field (company, name, description):
const inputChangedHandler = (newVal, inputIdentifier) =>
{
const updatedOrderForm = {
...formSettings
};
const updatedFormElement = {
...updatedOrderForm[inputIdentifier]
};
updatedFormElement.value = newVal;
updatedOrderForm[inputIdentifier] = updatedFormElement;
setFormSettings(updatedOrderForm);
}
The problem here is that only the last field is changed (description in the case of the code). If I changed the lines order and the "name" will be the last, the name info will appear and not the description.
How can I fix it?
You may be overriding your form with staled values (due to closures).
// closure on `updatedOrderForm` value, staled state
const updatedFormElement = {
...updatedOrderForm[inputIdentifier]
};
Try using functional update which provides the most updated state.
const inputChangedHandler = (newVal, inputIdentifier) => {
setFormSettings((prev) => {
const updatedOrderForm = {
...formSettings,
};
const updatedFormElement = {
...updatedOrderForm[inputIdentifier],
value: newVal,
};
return { ...prev, [inputIdentifier]: updatedFormElement };
});
};
Not sure how is your inputChangedHandler method definition is but you can take the value and use the respective hook to set it.
function inputChangedHandler({companyName,name, description}){
setName(name);
setCompanyName(companyName);
...
}
useEffect(() => {
if(props.editedItem)
{
inputChangedHandler(...props.editedItem);
}
}, [props.editedItem])

Resources