How to handle an unchanging array in useEffect dependency array? - javascript

I have a component like this. What I want is for the useEffect function to run anytime myBoolean changes.
I could accomplish this by setting the dependency array to [myBoolean]. But then I get a warning that I'm violating the exhaustive-deps rule, because I reference myArray inside the function. I don't want to violate that rule, so I set the dependency array to [myBoolean, myArray].
But then I get an infinite loop. What's happening is the useEffect is triggered every time myArray changes, which is every time, because it turns out myArray comes from redux and is regenerated on every re-render. And even if the elements of the array are the same as they were before, React compares the array to its previous version using ===, and it's not the same object, so it's not equal.
So what's the right way to do this? How can I run my code only when myBoolean changes, without violating the exhaustive-deps rule?
I have seen this, but I'm still not sure what the solution in this situation is.
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
useEffect(() => {
if(myBoolean) {
setMyString(myArray[0]);
}
}, [myBoolean, myArray]
}

Solution 1
If you always need the 1st item, extract it from the array, and use it as the dependency:
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const item = myArray[0];
useEffect(() => {
if(myBoolean) {
setMyString(item);
}
}, [myBoolean, item]);
}
Solution 2
If you don't want to react to myArray changes, set it as a ref with useRef():
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const arr = useRef(myArray);
useEffect(() => { arr.current = myArray; }, [myArray]);
useEffect(() => {
if(myBoolean) {
setMyString(arr.current);
}
}, [myBoolean]);
}
Note: redux shouldn't generate a new array, every time the state is updated, unless the array or it's items actually change. If a selector generates the array, read about memoized selectors (reselect is a good library for that).

I have an idea about to save previous props. And then we will implement function compare previous props later. Compared value will be used to decide to handle function change in useEffect with no dependency.
It will take up more computation and memory. Just an idea.
Here is my example:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function arrayEquals(a, b) {
return Array.isArray(a) &&
Array.isArray(b) &&
a.length === b.length &&
a.every((val, index) => val === b[index]);
}
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const previousArray = usePrevious(myArray);
const previousBoolean = usePrevious(myBoolean);
const handleEffect = () => {
console.log('child-useEffect-call-with custom compare');
if(myBoolean) {
setMyString(myArray[0]);
}
};
//handle effect in custom solve
useEffect(() => {
//check change here
const isEqual = arrayEquals(myArray, previousArray) && previousBoolean === myBoolean;
if (!isEqual)
handleEffect();
});
useEffect(() => {
console.log('child-useEffect-call with sallow compare');
if(myBoolean) {
setMyString(myArray[0]);
}
}, [myBoolean, myArray]);
return myString;
}
const Any = () => {
const [array, setArray] = useState(['1','2','3']);
console.log('parent-render');
//array is always changed
// useEffect(() => {
// setInterval(() => {
// setArray(['1','2', Math.random()]);
// }, 2000);
// }, []);
//be fine. ref is not changed.
// useEffect(() => {
// setInterval(() => {
// setArray(array);
// }, 2000);
// }, []);
//changed ref but value in array are not changed -> handle this case
useEffect(() => {
setInterval(() => {
setArray(['1','2', '3']);
}, 2000);
}, []);
return <div> <MyComponent myBoolean={true} myArray={array}/> </div>;
}

Related

React with TypeScript - LocalStorage won't work

I am working with persistence on a todo application written with React and TypeScript.
I am using localStorage to get the persistence I want.
Let me show you some code.
const [todos, setTodos] = useState<todoModel[]>([]);
useEffect(() => {
localStorage.setItem("todoItem", JSON.stringify(todos));
}, [todos])
const storesTodos = () => {
const storedValues = localStorage.getItem("todoItem");
if(!storedValues) { return todos; }
return JSON.parse(storedValues);
}
useEffect(() => { getToDoList(); storesTodos();
console.log("default") }, [])
useEffect(() => {
if (!props.reload) return;
console.log(props.reload)
getToDoList();
storesTodos();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.reload])
I am adding the StoresTodo() function into my useEffects, I tried both of them.
But it won't work. I get the data into my localStorage, but when reloading the page, it gets back to default values.
What am I missing here?
It seems like you are getting the same initial value because you never update the todos, judging based on shared code.
You can do this
...
const storesTodos = () => {
const storedValues = localStorage.getItem("todoItem");
if(!storedValues) {
setTodos(todos)
return todos;
}
return JSON.parse(storedValues);
}
Or you can use a local storage state hook from here
function useLocalStorageState({
key,
initialValue,
serialize = v => v,
deserialize = v => v,
}) {
const [state, setState] = React.useState(
() => deserialize(window.localStorage.getItem(key)) || initialValue,
)
const serializedState = serialize(state)
React.useEffect(() => {
window.localStorage.setItem(key, serializedState)
}, [key, serializedState])
return [state, setState]
}
When you refresh the page, even before the useEffect sets in
const [todos, setTodos] = useState<todoModel[]>([]);
already will set your initial todos array to an empty array.
Perhaps you'll need to perform the check on localStorage on it like so
const [todos, setTodos] = useState<todoModel[]>(() => {
const storedValues = localStorage.getItem("todoItem");
return storedValues ? JSON.parse(storedValues) : [];
});

react useState() not changing the value(an object) in the first render

I am using react usestate() and I want to update device state(It is an object)
my problem is when ShowRelays component renders for the first time device is an empty object and It does not get updated during first rendering, but for the next renders everything is fine
How can I update device state for the first time rendering?
(sorry for my bad english)
.
function ShowRelays(props) {
const [device, setDevice] = useState({})
let reduxDevices = useSelector(state => state.devicesReducer.devices)
let findDevice = () => {
let myDevice = reduxDevices.find(x => x._id === props.id)
setDevice(prevState => {
return {
...prevState,
...myDevice
}
})
}
useEffect(() => {
if (props.show) {
findDevice()
}
}, [props.show])
return
(
<div>
test
</div>
)
}
myDevice object is like:
{active: true, name: "device1", id: "deviceId"}
You can pass a function to your useState-hook which will calculate your initial value for device.
see lazy init state
function ShowRelays(props) {
let reduxDevices = useSelector(state => state.devicesReducer.devices);
const [device, setDevice] = useState(() => {
return reduxDevices.find(x => x._id === props.id)
});
return <div>test</div>;
}
An other possible solution for your problem without using a separate state could be the following (directly select the right device from your selector function):
function ShowRelays(props) {
const device = useSelector(state => {
return state.devicesReducer.devices.find(x => x._id === props.id);
});
return <div>test</div>;
}

React.js: useEffect() dependency freezes my app

I'm trying to display modal when no products have been selected by user before. I ended up having an infinite loop of useEffect() dependency. I'm not sure how to do it correctly in React.
import React, { useState, useEffect, useCallback } from 'react';
const MyComponent = ({ products }) => {
const [modals, setModals] = useState({});
const [currentModalName, setCurrentModalName] = useState('');
const setCurrentModal = useCallback(
(modalName, data = {}) => {
if (modalName) {
setModals({
...modals,
[modalName]: {
...modals[modalName],
...data
}
});
}
setCurrentModalName(modalName);
},
[modals]
);
useEffect(
() => {
if (!products.length) {
setCurrentModal('chooseProduct')
}
},
[products, setCurrentModal] // setCurrentModal causes infinite loop
);
return (
<div>...</div>
);
}
export default MyComponent;
I can just remove setCurrentModal from the dependencies, but I'm warned about it. If I add it, my React app freezes.
How can I organize my code to avoid freezing?
Why it loops?
The callback is always changing since it depends on the modals, which is always a different object even though it has the exact same properties as before, which always triggers the useEffect since it depends on the setCurrentModal callback value, which is always different since (() => {}) !== (() => {}).
Solution
Always use the functional update when the current state is needed to set the next state.
It'll prevent the need for the modals state as a dependency, which will limit the times when the callback is updated, fixing the infinite loop at the same time.
In addition to solving today's problem, functional update of the state is less prone to race-conditions, where multiple updates batched by React would overwrite each others.
const setCurrentModal = useCallback(
(modalName, data = {}) => {
if (!modalName) return; // precondition fail? early return.
// Happy path here!
// Note that I've used a different name to highlight that
// it's a different variable and to avoid shadowing the
// `modals` var from the outer scope.
setModals((currentModals) => ({ // use functional update.
...currentModals,
[modalName]: {
...currentModals[modalName],
...data
}
}));
setCurrentModalName(modalName);
},
// remove `modals` from the dependencies.
// setter functions are stable anyway, so it should remove any warning.
[setModals, setCurrentModalName]
);
useEffect(() => {
if (!products.length) {
setCurrentModal('chooseProduct')
}
},
[products, setCurrentModal]
);
Since the setCurrentModal callback is now stable (never ever changing), the useEffect will only be called when products value changes.
Missing dependencies warnings
The missing dependencies warnings come from the eslint-plugin-react-hooks, specifically, the react-hooks/exhaustive-deps rule. It's totally optional, but it helps keep the code clean and safe.
You could also choose to disable the warning just for this line:
const setCurrentModal = useCallback(
(modalName, data = {}) => {
// ...
setModals(/* ... */);
setCurrentModalName(modalName);
},
[] // eslint-disable-line react-hooks/exhaustive-deps
);
I think you can simplify it, without using useCallback.
(tested with Next.js and had no warnings, but if you still have some, you should use the answer of #Emile Bergeron)
import React, { useState, useEffect } from 'react'
const MyComponent = ({ products }) => {
const [modals, setModals] = useState({})
const [currentModalName, setCurrentModalName] = useState('')
const setCurrentModal = (name, data) => {
if (name) {
setModals(prev => {
return { ...prev, [name]: { ...prev[name], ...data }}
})
setCurrentModalName(name)
}
}
useEffect(() => {
if (!products || !products.length) {
const modalName = 'chooseProduct'
const data = { data: 'data' }
setCurrentModal(modalName, data)
}
}, [products])
const modalsJsx = modals ? Object.keys(modals).map((x, i) => {
return <li key={`modal-${i}`}>{x}</li>
}) : ''
const addModal = () => {
const name = 'test' + Math.floor(Math.random() * Math.floor(300))
setCurrentModal(name, { data: 'Hey' })
}
return (
<div>
<p>Current Modal : {currentModalName}</p>
<p>Modals : </p>
<ul>
{modalsJsx}
</ul>
<button onClick={addModal}>Test</button>
</div>
)
}
export default MyComponent
The function with useCallback to avoid warnings :
const setCurrentModal = useCallback((name, data = {}) => {
if (name) {
setModals(prev => {
return { ...prev, [name]: { ...prev[name], ...data }}
})
setCurrentModalName(name)
}
}, [setModals, setCurrentModalName])

Can I use a React hook containing a ref without triggering the exhaustive deps lint?

Quite a particular issue.
I'm using an isMounted value to prevent a useEffect function executing. This doesn't trigger the exhaustive deps lint when it's contained within the component itself:
const MyComponent = ({ value }) => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
}, []);
useEffect(() => {
if (isMounted.current) {
console.log(value);
}
}, [value]); // no exhaustive deps error
return <div />
}
But if we abstract that out to a hook, like so:
// useIsMounted.js
export const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
};
// MyComponent.js
const MyComponent = ({ value }) => {
const isMounted = useIsMounted();
useEffect(() => {
if (isMounted.current) {
console.log(value);
}
}, [value]); // has exhaustive deps error
return <div />
}
Then we get the linting error.
It's not the end of the world but I'd like to be able to use the linting rule and not have unnecessarily repeating code all over the place. Is there something I can do about that?
Thanks
Edit:
I've actually refactored slightly to use comparisons to previous values in my effect hooks instead of mount checks and it's slightly cleaner this way for my particular case. The inconsistency with the linting error described above is still odd to me though.

useEffect with debounce

I'm trying to create an input field that has its value de-bounced (to avoid unnecessary server trips).
The first time I render my component I fetch its value from the server (there is a loading state and all).
Here is what I have (I omitted the irrelevant code, for the purpose of the example).
This is my debounce hook:
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
(I got this from: https://usehooks.com/useDebounce/)
Right, here is my component and how I use the useDebounce hook:
function ExampleTitleInput(props) {
const [title, setTitle] = useState(props.title || "");
const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
const [commitsCount, setCommitsCount] = useState(0);
const debouncedTitle = useDebounce(title, 1000);
useEffect(() => {
setTitle(props.title || "");
}, [props.title]);
useEffect(() => {
if (debouncedTitle !== lastCommittedTitle) {
setLastCommittedTitle(debouncedTitle);
setCommitsCount(commitsCount + 1);
}
}, [debouncedTitle, lastCommittedTitle, commitsCount]);
return (
<div className="example-input-container">
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<div>Last Committed Value: {lastCommittedTitle}</div>
<div>Commits: {commitsCount}</div>
</div>
);
}
Here is the parent component:
function App() {
const [title, setTitle] = useState("");
useEffect(() => {
setTimeout(() => setTitle("This came async from the server"), 2000);
}, []);
return (
<div className="App">
<h1>Example</h1>
<ExampleTitleInput title={title} />
</div>
);
}
When I run this code, I would like it to ignore the debounce value change the first time around (only), so it should show that the number of commits are 0, because the value is passed from the props. Any other change should be tracked. Sorry I've had a long day and I'm a bit confused at this point (I've been staring at this "problem" for far too long I think).
I've created a sample:
https://codesandbox.io/s/zen-dust-mih5d
It should show the number of commits being 0 and the value set correctly without the debounce to change.
I hope I'm making sense, please let me know if I can provide more info.
Edit
This works exactly as I expect it, however it's giving me "warnings" (notice dependencies are missing from the deps array):
function ExampleTitleInput(props) {
const [title, setTitle] = useState(props.title || "");
const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
const [commitsCount, setCommitsCount] = useState(0);
const debouncedTitle = useDebounce(title, 1000);
useEffect(() => {
setTitle(props.title || "");
// I added this line here
setLastCommittedTitle(props.title || "");
}, [props]);
useEffect(() => {
if (debouncedTitle !== lastCommittedTitle) {
setLastCommittedTitle(debouncedTitle);
setCommitsCount(commitsCount + 1);
}
}, [debouncedTitle]); // removed the rest of the dependencies here, but now eslint is complaining and giving me a warning that I use dependencies that are not listed in the deps array
return (
<div className="example-input-container">
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<div>Last Committed Value: {lastCommittedTitle}</div>
<div>Commits: {commitsCount}</div>
</div>
);
}
Here it is: https://codesandbox.io/s/optimistic-perlman-w8uug
This works, fine, but I'm worried about the warning, it feels like I'm doing something wrong.
A simple way to check if we are in the first render is to set a variable that changes at the end of the cycle. You could achieve this using a ref inside your component:
const myComponent = () => {
const is_first_render = useRef(true);
useEffect(() => {
is_first_render.current = false;
}, []);
// ...
You can extract it into a hook and simply import it in your component:
const useIsFirstRender = () => {
const is_first_render = useRef(true);
useEffect(() => {
is_first_render.current = false;
}, []);
return is_first_render.current;
};
Then in your component:
function ExampleTitleInput(props) {
const [title, setTitle] = useState(props.title || "");
const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
const [updatesCount, setUpdatesCount] = useState(0);
const is_first_render = useIsFirstRender(); // Here
const debouncedTitle = useDebounce(title, 1000);
useEffect(() => {
setTitle(props.title || "");
}, [props.title]);
useEffect(() => {
// I don't want this to trigger when the value is passed by the props (i.e. - when initialized)
if (is_first_render) { // Here
return;
}
if (debouncedTitle !== lastCommittedTitle) {
setLastCommittedTitle(debouncedTitle);
setUpdatesCount(updatesCount + 1);
}
}, [debouncedTitle, lastCommittedTitle, updatesCount]);
// ...
You can change the useDebounce hook to be aware of the fact that the first set debounce value should be set immediately. useRef is perfect for that:
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
const firstDebounce = useRef(true);
useEffect(() => {
if (value && firstDebounce.current) {
setDebouncedValue(value);
firstDebounce.current = false;
return;
}
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
I think you can improve your code in some ways:
First, do not copy props.title to a local state in ExampleTitleInput with useEffect, as it may cause excessive re-renders (the first for changing props, than for changing state as an side-effect). Use props.title directly and move the debounce / state management part to the parent component. You just need to pass an onChange callback as a prop (consider using useCallback).
To keep track of old state, the correct hook is useRef (API reference).
If you do not want it to trigger in the first render, you can use a custom hook, such as useUpdateEffect, from react-use: https://github.com/streamich/react-use/blob/master/src/useUpdateEffect.ts, that already implements the useRef related logic.

Categories

Resources