Wrong local state value when a handling a window event (React) - javascript

I have a state value ccFormDetails with a an empty object as default value.
const [ccFormDetails, setCCFormDetails] = useState({})
In first useEffect, I call a function the populates ccFormDetails with relevant data.
const getCCFormData = async () => {
const ccFormResult = await ContractService.getContractsCCFormData()
const { ccFormData } = ccFormResult
setCCFormDetails(ccFormData)
}
In second useEffect, I create an a event named message and assign a handleCgEvent handler to it.
useEffect(() => {
window.addEventListener('message', handleCgEvent)
return () => window.removeEventListener('message', handleCgEvent)
}, [])
I'm rendering an iframe with a submit button that emits the message event when clicked.
Then, when I click the sumbit button in the iframe, the handleCgEvent handler fires and should extract the correct (updated, fecthed) value of ccFormDetails. (Which I can see populated correctly in the React components tree)
const handleCgEvent = e => {
if (e.data === 'reload_cg') {
console.log('fail')
}
if (e.data['event_id'] === 'cg-success') {
console.log('success')
console.log('ccFormDetails1 ', ccFormDetails)
}
}
But what I get is {} meaning the original default state.
This should not happen per my knowledge of React.
Am I missing something/Does event handling messes state?

The problem here is you're using the handleCgEvent that's created when the component is mounted, and ccFormDetails is encapsulated within it using its default value. In order to get the most current state, you're going to have to use useRef. Something like:
const [ccFormDetails, setCCFormDetails] = useState({})
const formRef = useRef();
formRef.current = ccFormDetails;
const handleCgEvent = e => {
if (e.data === 'reload_cg') {
console.log('fail')
}
if (e.data['event_id'] === 'cg-success') {
console.log('success')
console.log('ccFormDetails1 ', formRef.current)
}
}
useEffect(() => {
window.addEventListener('message', handleCgEvent)
return () => window.removeEventListener('message', handleCgEvent)
}, []);
If it's okay for you to add and remove the event listener, then I would just add [ccFormDetails] as a dependency, assuming the entire object is recreated when its properties change.
For further reading about stale values in closures, this is a great blog entry: https://dmitripavlutin.com/react-hooks-stale-closures/

Related

Javascript click event listener with react function

I want to create a custom hook in which I add a click event listener to a DOM element which calls a function defined in a React component which uses a state variable.
I add the event listener, but when I call the function, it does not reflect any state changes, it is always taking the initial state value.
const useCustomHook = (functionDefinedInComponent) => {
// logic to get the dom element
element.addEventListener('click', () => functionDefinedInComponent(item));
};
const CustomComponent = () => {
const [state, setState] = useState(...);
const customFunction = (item) => {
setState(...); // change the 'state' variable
// use item and state to do something
}
useCustomHook(customFunction);
return ...;
}
When I click the DOM element to which I added the click event, the customFunction triggers with initial state value. Is there any to solve this?
I meant something like this.
you might have to wrap your callback function in React.useCallback as well.
const useCustomHook = (functionDefinedInComponent) => {
React.useEffect(() => {
// logic to get the dom element
element.addEventListener('click', () => functionDefinedInComponent());
}, [functionDefinedInComponent])
};
Can you try this out and let me know what sort of problem you get.
Here is a code sandbox that you were trying to do.
https://codesandbox.io/s/rakeshshrestha-nvgl1?file=/src/App.js
Explanation for the codesandbox example
Create a custom hook
const useCustomHook = (callback) => {
React.useEffect(() => {
// logic to get the dom element
const el = document.querySelector(".clickable");
el.addEventListener("click", callback);
// we should remove the attached event listener on component unmount so that we dont have any memory leaks.
return () => {
el.removeEventListener("click", callback);
};
}, [callback]);
};
so, I created a custom hook named useCustomHook which accepts a function as a parameter named callback. Since, we want to attach an event on element with class clickable, we should wait till the element gets painted on the browser. For this purpose, we used useEffect which gets called after the component has been painted on the screen making .clickable available to select.
const [input, setInput] = React.useState("");
const logger = React.useCallback(() => {
alert(input);
}, [input]);
useCustomHook(logger);
// render
Here, I have a state input which holds the state for the textbox. And also a function named logger which I passed to my custom hook. Notice, that the function logger has been wrapped inside of useCallback. You don't need to wrap it in this case, but it was there so that every time the component rerenders a new logger function won't be created except the changes in the input state.
You can use a public component like this:
const ClickableComponent = props => {
const { title, handleClick, component: Component } = props;
return (
<Component onClick={handleClick}>{title}</button>
)
};
export default ClickableComponent;
You can use this component like below:
<ClickableComponent title="your title" handleClick={handleClick} component={<button/> } />

state inside realm listener is undefined (Realm and React Native)

I am facing the following problem with Realm and React Native.
Inside my component I want to listen to changes of the Realm, which does work. But inside the listener, my state is always undefined - also after setting the state inside the listener, the useEffect hook does not trigger. It looks like everything inside the listener doesn't have access to my state objects.
I need to access the states inside the listener in order to set the state correctly. How can I do this?
edit: The state seems to be always outdated. After hot reloading, the state is correct, but still lags behind 1 edit always.
const [premium, setPremium] = useState<boolean>(true);
const [settings, setSettings] = useState<any>({loggedIn: true, userName: 'XYZ'});
useEffect(() => {
console.log('updated settings'); // never gets called
}, [settings]);
useEffect(() => {
const tmp: any = settingsRealm.objects(MyRealm.schema.name)[0];
tmp.addListener(() => {
console.log(premium, settings); // both return undefined
if (premium) {
setSettings(tmp);
}
// for demonstration purposes
setSettings(tmp);
})
}, []);
For anyone having the same problem:
Adding a useRef referencing the state, and updating both(!) at the same time (state and ref) will solve the problem. Now all instances (inside the listener) of "premium" will have to be changed to "premiumRef.current".
https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559
const [premium, _setPremium] = useState<boolean>(true);
const [settings, setSettings] = useState<any>({loggedIn: true, userName: 'XYZ'});
const premiumRef = useRef<boolean>(premium);
const setPremium = (data) => {
premiumRef.current = data;
_setPremium(data);
}
useEffect(() => {
const tmp: any = settingsRealm.objects(MyRealm.schema.name)[0];
tmp.addListener(() => {
if (premiumRef.current) {
setSettings(tmp);
}
})
}, []);
[1]: https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559

React useEffect and useState interaction

I am using React and Material UI to create a table (XGrid) with some buttons. When you click the row, it should set the row id using useState. When you click the delete button, it should delete the row. It seems that the delete click handler is not using the value from use state. This is either some kind of closure thing or some kind of React thing.
const MyTableThing: React.FC = (props) =>
{
const { data } = props;
const [filename, setFilename] = React.useState<string>("")
const [columns, setColumns] = React.useState<GridColDef[]>([])
const handleDelete = () =>
{
someFunctionThatDeletes(filename); // filename is always ""
setFilename(""); // Does not do anything.. !
}
React.useEffect(() =>
{
if (data)
{
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) =>
{
if (column.field === "delete")
{
column.renderCell = (cellParams: GridCellParams) =>
{
return <Button onClick={handleDelete}>Delete</Button>
}
}
})
setColumns(columns)
}
}, [data?.files])
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) =>
{
console.log(`set selected row to ${param.data.id}`) // This works every time
setFilename(param.data.id)
}
}
The reason for this behavior is that React does not process setState action synchronously. It is stacked up with other state changes and then executed. React does this to improve performance of the application. Read following link for more details on this.
https://linguinecode.com/post/why-react-setstate-usestate-does-not-update-immediately
you can disable your deleteRow button till the filename variable is updated. you can use useEffect or setState with callback function.
useEffect(() => {
//Enable your delete row button, fired when filename is updated
}, filename)
OR
this.setFilename(newFilename, () => {
// ... enable delete button
});
Let me know if this helps! Please mark it as answer if it helps.
The main problem I see here is that you are rendering JSX in a useEffect hook, and then saving the output JSX into columns state. I assume you are then returning that state JSX from this functional component. That is a very bizarre way of doing things, and I would not recommend that.
However, this explains the problem. The JSX being saved in state has a stale version of the handleDelete function, so that when handleDelete is called, it does not have the current value of filename.
Instead of using the useEffect hook and columns state, simply do that work in your return statement. Or assign the work to a variable and then render the variable. Or better yet, use a useMemo hook.
Notice that we add handleDelete to the useMemo dependencies. That way, it will re-render every time handleDelete changes. Which currently changes every render. So lets fix that by adding useCallback to handleDelete.
const MyTableThing: React.FC = (props) => {
const { data } = props;
const [filename, setFilename] = React.useState<string>('');
const handleDelete = React.useCallback(() => {
someFunctionThatDeletes(filename); // filename is always ""
setFilename(''); // Does not do anything.. !
}, [filename]);
const columns = React.useMemo(() => {
if (!data) {
return null;
}
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) => {
if (column.field === 'delete') {
column.renderCell = (cellParams: GridCellParams) => {
return <Button onClick={handleDelete}>Delete</Button>;
};
}
});
return columns;
}, [data?.files, handleDelete]);
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) => {
console.log(`set selected row to ${param.data.id}`); // This works every time
setFilename(param.data.id);
};
return columns;
};

How to make useEffect listening to any change in localStorage?

I am trying to have my React app getting the todos array of objects from the localStorage and give it to setTodos. To do that I need to have a useEffect that listen to any change that occurs in the local storage so this is what I did:
useEffect(() => {
if(localStorage.getItem('todos')) {
const todos = JSON.parse(localStorage.getItem('todos'))
setTodos(todos);
}
}, [ window.addEventListener('storage', () => {})]);
The problem is that useEffect is not triggered each time I add or remove something from the localStorage.
Is this the wrong way to have useEffect listening to the localStorage?
I tried the solution explained here but it doesn't work for me and I sincerely I do not understand why it should work because the listener is not passed as a second parameter inside the useEffect
You can't re-run the useEffect callback that way, but you can set up an event handler and have it re-load the todos, see comments:
useEffect(() => {
// Load the todos on mount
const todosString = localStorage.getItem("todos");
if (todosString) {
const todos = JSON.parse(todosString);
setTodos(todos);
}
// Respond to the `storage` event
function storageEventHandler(event) {
if (event.key === "todos") {
const todos = JSON.parse(event.newValue);
setTodos(todos);
}
}
// Hook up the event handler
window.addEventListener("storage", storageEventHandler);
return () => {
// Remove the handler when the component unmounts
window.removeEventListener("storage", storageEventHandler);
};
}, []);
Beware that the storage event only occurs when the storage is changed by code in a different window to the current one. If you change the todos in the same window, you have to trigger this manually.
const [todos, setTodos] = useState();
useEffect(() => {
setCollapsed(JSON.parse(localStorage.getItem('todos')));
}, [localStorage.getItem('todos')]);

React state resets when set from prop event handler

Cannot for the life of me figure out what is going on, but for some reason when "Click me" is clicked, the number increments as you'd expect. When a click is triggered by the Child component, it resets the state and ALWAYS prints 0.
function Child(props: {
onClick?: (id: string) => void,
}) {
const ref = useCallback((ref) => {
ref.innerHTML = 'This Doesnt';
ref.addEventListener('click',() => {
props.onClick!('')
})
}, [])
return (<div ref={ref}></div>)
}
function Parent() {
const [number, setNumber] = useState(0);
return <div>
<div onClick={() => {
setNumber(number + 1);
console.log(number);
}}>
This Works
</div>
<Child
onClick={(id) => {
setNumber(number + 1);
console.log(number);;
}}
/>
</div>
}
And here is a demonstration of the problem: https://jscomplete.com/playground/s333177
Both the onClick handlers in parent component are re-created on every render, rightly so as they have a closure on number state field.
The problem is, the onClick property sent to Child component is used in ref callback, which is reacted only during initial render due to empty dependency list. So onClick prop received by Child in subsequent renders does not get used at all.
Attempt to resolve this error, by either removing dependency param or sending props.onClick as in dependency list, we land into issue due to caveat mentioned in documentation. https://reactjs.org/docs/refs-and-the-dom.html
So you add null handling, and you see your updated callback now getting invoked, but... all earlier callbacks are also invoked as we have not removed those event listeners.
const ref = useCallback((ref) => {
if(!ref) return;
ref.innerHTML = 'This Doesnt';
ref.addEventListener('click',() => {
props.onClick!('')
})
}, [props.onClick])
I believe this just an experiment being done as part of learning hooks, otherwise there is no need to go in roundabout way to invoke the onClick from ref callback. Just pass it on as prop to div.
Edit:
As per your comment, as this is not just an experiment but simplification of some genuine requirement where click handler needs to be set through addEventListener, here is a possible solution:
const ref = useRef(null);
useEffect(() => {
if(!ref.current) return;
ref.current.innerHTML = 'This Doesnt';
const onClick = () => props.onClick!('');
ref.current.addEventListener('click',onClick)
// return the cleanup function to remove the click handler, which will be called before this effect is run next time.
return () => {ref.current.removeEventListener("click", onClick)}
}, [ref.current, props.onClick]);
Basically, we need to use useEffect so that we get a chance to remove old listener before adding new one.
Hope this helps.
function Child(props: {
onClick?: (id: string) => void,
}) {
function handleClick() {
props.onClick('')
}
const ref = useRef();
useEffect(() => {
ref.current.innerHTML = 'This Doesnt';
ref.current.addEventListener('click', handleClick)
return () => { ref.current.removeEventListener('click', handleClick); }
}, [props.onClick])
return (<div ref={ref}></div>)
}
#ckder almost works but console.log displayed all numbers from 0 to current number value.
Issue was with event listener which has not been remove after Child component umount so to achive this I used useEffect and return function where I unmount listener.

Categories

Resources