useEffect localStorage loop - reactjs

I'm trying to storage a single state and I cannot do that apparently because of a infinite loop. Could you help me?
import React, { useState, useEffect } from "react";
const App = () => {
const [rows, setRows] = useState("Inicial State");
function init() {
const data = localStorage.getItem("my-list");
if (data) {
setRows(JSON.parse(data));
}
localStorage.setItem("my-list", JSON.stringify(rows));
}
useEffect(() => {
init();
});
return (
<div>
<button onClick={() => setRows("Loaded state!")}>Load!</button>
<div>{rows}</div>
</div>
);
};
export default App;

You call init() every time component re-render. Document how to use useEffect here: https://reactjs.org/docs/hooks-effect.html. You should only call one time like componentDidMount in class component by:
useEffect(() => {
init();
}, []);
useEffect(() => {
localStorage.setItem("my-list", JSON.stringify(rows));
}, [rows]);

If you are using useEffect for initialisation, it needs to have an empty dependency array to make sure it only runs onthe first render, not on every render:
useEffect(() => init(), []);

Related

useEffect getting triggered repeatedly even after applying [] brackets

I was learning react and came across the concept of useEffect. So I was trying useEffects with resize event listeners and when doing so even after applying the square bracket which should be used only to run the useEffect once, I am repeatedly getting the updated size of the screen when trying the change the browser size.
below is the code, could some one please let me know if I have some code issue or is this how it is supposed to work?
import React, { useState, useEffect } from "react";
// cleanup function
// second argument
const UseEffectCleanup = () => {
const [size, setsize] = useState(window.innerWidth);
const checkSize = () => {
console.log("check size");
setsize(window.innerWidth);
};
useEffect(() => {
console.log("use effect");
window.addEventListener("resize", checkSize);
return () => {
console.log("Cleanup");
window.removeEventListener("resize", checkSize);
};
}, []);
return (
<>
<h2>Windows Width</h2>
<h2>{size}</h2>
</>
);
};
export default UseEffectCleanup;

React use-effect [duplicate]

I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.

how to prevent useEffect on the previous is screen

I have 2 screens, on my first screen I have a useEffect which has a watcher on a redux state.
Screen 1
...
useEffect(() => {
if (props.data) {
navigation.navigate('To Other Screen');
}
}, [props.data]);
...
Then on my second Screen.
I have a function that will update the redux 'data'.
Then, the useEffect on the previous was triggered.
How will I prevent this one.
Thank you.
I found this hoc on the react-nativgation and combine this with useEffect;
https://reactnavigation.org/docs/1.x/with-navigation-focus/
import { withNavigationFocus } from 'react-navigation';
...
const Page = (props) => {
useEffect(() => {
if (props.isFocused) {
// function call;
}
}, [props.isFocused]);
};
export default withNavigationFocus(Page);
Try this,
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
if(props.data){
navigation.navigate('To Other Screen')
}
})
return unsubscribe
}, [navigation, props.data])

How does react useEffect work with useState hook?

Can someone explain what am I'm doing wrong?
I have a react functional component, where I use useEffect hook to fetch some data from server and put that data to state value. Right after fetching data, at the same useHook I need to use that state value, but the value is clear for some reason. Take a look at my example, console has an empty string, but on the browser I can see that value.
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
console.log(value);
};
fetchData();
}, [value]);
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;
Link to codesandbox.
The useEffect hook will run after your component renders, and it will be re-run whenever one of the dependencies passed in the second argument's array changes.
In your effect, you are doing console.log(value) but in the dependency array you didn't pass value as a dependency. Thus, the effect only runs on mount (when value is still "") and never again.
By adding value to the dependency array, the effect will run on mount but also whenever value changes (which in a normal scenario you usually don't want to do, but that depends)
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
console.log(value);
};
fetchData();
}, [value]);
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;
Not sure exactly what you need to do, but if you need to do something with the returned value from your endpoint you should either do it with the endpoint returned value (instead of the one in the state) or handle the state value outside the hook
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
// handle the returned value here
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
};
fetchData();
}, []);
// Or handle the value stored in the state once is set
if(value) {
// do something
}
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;

Why is React useEffect clean-up function being run right after the useEffect callback, and then never again?

I have a small React app with a component that has a button that opens a small menu, and I'd like it to close the menu when the user clicks anywhere outside the component.
function setupDocumentClickEffect(onClick = (() => {})) {
console.log('Add doc click');
document.addEventListener('click', onClick);
return () => { // Clean-up
console.log('Remove doc click');
document.removeEventListener('click', onClick);
};
}
function MyComponent() {
const [open, setOpen] = useState(false);
// Set up an effect that will close the component if clicking on the document outside the component
if (open) {
const close = () => { setOpen(false); };
useEffect(setupDocumentClickEffect(close), [open]);
}
const stopProp = (event) => { event.stopPropagation(); };
const toggleOpen = () => { setOpen(!open); };
// ...
// returns an html interface that calls stopProp if clicked on the component itself,
// or toggleOpen if clicked on a specific button.
}
When the component is first opened, it will run both the callback and the cleanup immediately. Console will show: Add doc click and Remove doc click. If the component is closed and then re-opened, it acts as expected with just Add doc click, not running clean-up... but then clean-up is never run again.
I suspect I'll have to re-structure this so it doesn't use if (open), and instead runs useEffect each time? But I'm not sure why the clean-up runs the way it does.
A few things are wrong here. The first argument to a useEffect should be a callback function, which you're returning from setupDocumentClickEffect, this means that the return value of setupDocumentClickEffect(close) will just be run immediately on mount, and never again.
It should look more like this:
useEffect(() => {
if (!open) {
return;
}
console.log('Add doc click');
document.addEventListener('click', close);
return () => { // Clean-up
console.log('Remove doc click');
document.removeEventListener('click', close);
};
}, [open]);
The other thing that is wrong here is that you are breaking the rules of hooks: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
You should not define a hook in a conditional.
EDIT
To elaborate on what is happening in your current useEffect, it basically boils down to if you wrote something like this:
if (open) {
const close = () => { setOpen(false); };
console.log('Add doc click');
document.addEventListener('click', close);
useEffect(() => {
console.log('Remove doc click');
document.removeEventListener('click', close);
}, [open]);
}
So you would want to throw that function inside of the useEffect() hook and avail yourself of useRef like so:
import React, { useEffect, useState, useRef } from 'react';
const MyComponent = ({ options, selected }) => {
const [open, setOpen] = useState(false);
const ref = useRef();
useEffect(() => {
const setupDocumentClickEffect = (event) => {
// this if conditional logic assumes React v17
if (ref.current && ref.current.contains(event.target)) {
return;
}
setOpen(false);
};
document.body.addEventListener('click', setupDocumentClickEffect);
return () => {
document.body.removeEventListener('click', setupDocumentClickEffect);
};
}, []);
}
So since it's a menu, I imagine you build your list via a map() function somewhere that in this example, I am calling options which is why you see it passed as props in your MyComponent and you want to render that list of options from the menu:
import React, { useEffect, useState, useRef } from 'react';
const MyComponent = ({ label, options, selected, onSelectedChange }) => {
const [open, setOpen] = useState(false);
const ref = useRef();
useEffect(() => {
const setupDocumentClickEffect = (event) => {
// this if conditional logic assumes React v17
if (ref.current && ref.current.contains(event.target)) {
return;
}
setOpen(false);
};
document.body.addEventListener('click', setupDocumentClickEffect);
return () => {
document.body.removeEventListener('click', setupDocumentClickEffect);
};
}, []);
const renderedOptions = options.map((option) => {
if (option.value === selected.value) {
return null;
}
return (
<div
key={option.value}
className="item"
onClick={() => {
onSelectedChange(option);
}}
>
{option.label}
</div>
);
});
return (
<div ref={ref} className="ui form">
// the rest of your JSX code here including
// renderedOptions below
{renderedOptions}
</div>
);
};
export default MyComponent;
So I added some props to your MyComponent and also showed you how to implement that useRef which will be important in pulling this off as well.
I suspect it's because you're calling setupDocumentClickEffect(close) immediately inside of useEffect(). Using a deferred call like useEffect(() => setupDocumentClickEffect(close), []) is what you want.
It might not break the useEffect hook, but it would be better practice to incorporate your if(open) within setupDocumentClickEffect() instead of wrapping your hook in it.

Resources