useEffect Hook Not Firing After State Change - javascript

I have two sibling components that share state via context in react.
The shared state between the components is an array.
If I update arr state in one component, I want the other component to listen for that update and do something accordingly. When I use useEffect in the second component, I listen for changes in the arr state variable.
For example:
// App Component -------
const App = props => {
const { arr, setArr } = useContext(GlobalContext)
const handleChange = () => {
const newArr = arr
[10, 20, 30, 40].map(v => {
newArr.push(v)
setArr(newArr)
})
return (...)
}
// App2 (Sibling) Component
const App2 = props => {
const { arr, setArr } = useContext(GlobalContext)
const [localArr, setLocalArr] = useState(0)
useEffect(
() => {
updateLocalState()
},
// fire if "arr" gets updated
[arr]
)
const updateLocalState = () => {
setLocalArr(localArr + 1)
}
return (...)
}
The useEffect hook is only fired on the initial render, though the state of arr updates.
I know that declaring a new variable const newArr = arr to my state variable is a reference, so newArr.push(v) is technically a state mutation. However, the state still updates, no warning is thrown, and useEffect does nothing.
Why does useEffect not get called though the state gets updated? Is it because of the state mutation?
Second Question: Why is there no warning or error thrown regarding a state mutation? State mutations are dangerous - If it happens, I'd expect some sort of warning.
Live demo here:

The array you pass as second argument to useEffect only checks if the elements in the array are === to the elements in it in the previous render. const newArr = arr; will lead to newArr === arr since it doesn't create a new array, which is not what you want.
Create a new array with all the elements in arr and it will work as expected.
const App = props => {
const { arr, setArr } = useContext(GlobalContext)
const handleChange = () => {
const newArr = [...arr]
[10, 20, 30, 40].forEach(v => {
newArr.push(v)
})
setArr(newArr)
}
return <>{/* ... */}</>
}

When you want to update array using useState hook. Make sure to spread the array into new array and update the new array so that your useEffect listening for this state will be called.
UseEffect will not call in the below code snippet as you are directly updating array.
const [skills, selectedSkills] = useState([])
const onSelect = (selectedList) => {
selectedSkills(selectedList)
}
useEffect(() => {
MyLogger('useEffect called')
}, [skills])
UseEffect will call in the below code snippet as we are keeping new reference to the array.
const [skills, selectedSkills] = useState([])
const onSelect = (selectedList) => {
const tempSelectedList = [...selectedList]
selectedSkills(tempSelectedList)
}
useEffect(() => {
MyLogger('useEffect called')
}, [skills])

Related

Can't stop an infinite loop at nested component at useEffect hook

I am trying to curb useEffect related to nested components. Here are the components:
Parent (it receives data from API):
const ListOfLots = (props) => {
const initial = {listLots: props.lots, form: props.form}
const [lots, setLots] = useState(initial);
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
});
return (
<div>
{
lots.listLots.map(function(lot) {
return <Lot key={lot.uuid} lot={lot} procForm={lots.form}/>
})
}
</div>
)
}
Nested:
const Lot = (props) => {
const initial = {currLot: props.lot, form: props.form};
const [lot, setLot] = useState(initial);
let getWinningBid = (offers) => {
for (let i = 0; i < offers.length; i++) {
console.log("BOOM!!!");
if (offers[i].isTrue === true) {
return offers[i].pip;
}
}
}
return (
...
)
}
While I am using no dependencies at parent's useEffect, I have got an infinite invoking of console.log("BOOM!!!"), that is, of course, unacceptable, but my Nested component rerendered. When I try to use the following type of dependencies at useEffect: [], [lots.form], [lots.listLots] or [lots.listLots.length] my Nested component is not rerendered: it stays blank. So the result is the following: I have an infinite useEffect loop or not-working(?) useEffect.
Is there any way in this case to handle the useEffect?
Use
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
}, [props.lots, props.form]);
This triggers the callback only if the value of props.lots, props.form is changed else it won't be triggered on every rerender as in case of no second argument.
A similar question here might help you find better explanations.

How to make React functional component recreate callback function and read updated props

I have a very simple functional component in React. When this component is rendered by the parent component, initially myList is an empty array, and then eventually when it finishes loading, it is a list with a bunch of items.
The problem is, the value of myList inside onSearchHandler never gets updated, it's always [].
const MyComponent = ({ myList }) => {
const [filteredList, setFilteredList] = useState(myList);
console.log(myList); // <<< This outputs [], and later [{}, {}, {}] which is expected.
useEffect(() => {
setFilteredList(myList);
}, [myList]);
const onSearchHandler = (searchText) => {
console.log(myList); /// <<< When this function is called, this always outputs []
const filteredItems = myList.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredList(filteredItems);
};
return <AnotherComponent items={filteredList} onSearch={onSearchHandler} />
};
Is there a way to force onSearchHandler to re-evaluate the value of myList? What would be the recommended approach for this sort of operation?
It sounds like AnotherComponent does not take into consideration the changed prop - this should be considered to be a bug in AnotherComponent. Ideally, you'd fix it so that the changed prop gets used properly. Eg, just for an example, maybe it's doing
const [searchHandler, setSearchHandler] = useState(props.onSearch);
and failing to observe prop changes as it should. Or, for another random example, this could happen if the listener prop gets passed to an addEventListener when the component mounts but again doesn't get checked for changes and removed/reattached.
If you can't fix AnotherComponent, you can use a ref for myList in addition to the prop:
const MyComponent = ({ myList }) => {
const myListRef = useRef(myList);
useEffect(() => {
myListRef.current = myList;
setFilteredList(myList);
}, [myList]);
const [filteredList, setFilteredList] = useState(myList);
const onSearchHandler = (searchText) => {
const filteredItems = myListRef.current.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredList(filteredItems);
};
It's ugly, but it might be your only option here.

Data from API call are stored in a Array but when i try to use that array in a function for further use it shows that array is empty . why?

React code for storing Data from API to an Array and Using the same Array's event_date value for further use.
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
const [holidayPlans, setHolidayPlans] = useState([]);
const [dateArray, setDate] = useState([]);
useEffect(() => {
getHolidayPlans();
}, []);
const getHolidayPlans = async () => {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
if (holidayResp) {
setCities(() => holidayResp.cityModule);
setHolidayPlans(() => holidayResp.holidayModule);
setDate(() => holidayResp.holidayModule);
}
let today = new Date();
console.log(holidayPlans);
holidayPlans.filter((date) => {
const eventDate = new Date(date.event_date);
console.log(eventDate);
});
};
So what the thing is when i use the Same (holidayPlans) array to display some contents in html it shows the values and displays properly but when i use inside a function it shows there is no data inside the array .
console.log(holidayPlans) shows this
Same Array used to display in html
Here's a challenge: write a JavaScript function useState such that the console.log outputs a 4 and then a 5:
function render() {
let [thing, setThing] = useState(4);
console.log(thing); // 4
setThing(5);
console.log(thing); // 5
}
No matter what you do, you'll never be able to write this function, because no external JavaScript function will be able to set the value of the thing variable; that's because an external JavaScript has no way to modify the thing variable. All useState would be able to do is set its own internal state and change what it returns. Silly example here:
let hiddenState;
function useState(initialValue) {
if (hiddenState === undefined) {
hiddenState = initialValue;
}
const setState = value => {
hiddenState = value;
}
return [hiddenState, setState];
}
That means render will only be able to get a new value if useState is called again:
function render() {
let [thing, setThing] = useState(4);
console.log(thing); // 4
setThing(5);
[thing, setThing] = useState(4);
console.log(thing); // 5
}
This is essentially what useState does but in a way where the hidden state is unique per instance. As you can see, setState is to be considered "asynchronous" in that state changes aren't reflected until the next render. setState queues up a re-render request. The next time your render function is called, useState will be called again, and it will return a new value.
Notice with these code modifications, rather than us referencing the state variable before it has updated, we can still reference your response object to get the data:
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
// On the first rendering of `UpcomingHolidays`, holidayPlans will be [].
// After setHolidayPlans is called, a re-render will be queued, and this
// UpcomingHolidays function will be called again. When useState is called
// the second time, it will have the value passed into setHolidayPlans.
const [holidayPlans, setHolidayPlans] = useState([]);
// Same for dateArray.
const [dateArray, setDate] = useState([]);
useEffect(() => {
getHolidayPlans();
}, []);
async function getHolidayPlans() {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
if (!holidayResp) {
return;
}
// These will flag the component as needing to re-render after the effect
// completes. They do not change the local variables; they update the
// internal data of the useState hooks so that the next time those useState
// calls occur, they'll return new values.
setCities(holidayResp.cityModule);
setHolidayPlans(holidayResp.holidayModule);
setDate(holidayResp.holidayModule.map(date => new Date(date.event_date));
// If you want to log here, don't reference state, which hasn't updated yet.
// Either store response data as variables or reference the response itself.
console.log('Holidays are', holidayResp.holidayModule);
}
return <div>Your content</div>;
}
If you move your console.log(holidayPlans); out of getHolidayPlans function, you get an updated value.
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
const [holidayPlans, setHolidayPlans] = useState([]);
const [dateArray, setDate] = useState([]);
useEffect(() => {
const getHolidayPlans = async () => {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
if (holidayResp) {
setCities(holidayResp.cityModule);
setHolidayPlans(holidayResp.holidayModule); // you may filter data here
setDate(holidayResp.holidayModule);
}
};
getHolidayPlans();
}, []);
console.log(holidayPlans);
This happens because when you use the useState hook, you are assigning the state values holidayPlans and dateArray to local constants (or variables, this does not matter), and these values are assigned each time the component is rendered. This means that the constant value in your component will not get updated immediately, but it will be reflected in the next render, which will be triggered by the state updates that you do within getHolidayPlans. This is why, if you place the console.log() call outside getHolidayPlans, the value is printed properly.
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
const [holidayPlans, setHolidayPlans] = useState([]);
const [dateArray, setDate] = useState([]);
useEffect(() => {
getHolidayPlans();
}, []);
const getHolidayPlans = async () => {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
if (holidayResp) {
setCities(() => holidayResp.cityModule);
setHolidayPlans(() => holidayResp.holidayModule);
setDate(() => holidayResp.holidayModule);
}
// ...
};
console.log(holidayPlans);
Basically this is what happens:
First render
|
V
useEffect executes getHolidayPlans()
|
V
getHolidayPlans() performs state changes,
triggering a new render cycle
|
V
Second render,
which will have new state values
It is important to notice that in the end UpcomingHolidays is just a function, and its body is executed on each render cycle.
Based on this, the recommended way to go is to use constant/variables local to the caller function (getHolidayPlans()) instead of using the state constant/variables immediately after their respective setState function has been called, because they are updated after the completion of the function that it was called in.
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
const [holidayPlans, setHolidayPlans] = useState([]);
const [dateArray, setDate] = useState([]);
useEffect(() => {
getHolidayPlans();
}, []);
const getHolidayPlans = async () => {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
const holidayPlansLocal = holidayResp.holidayModule;
if (holidayResp) {
setCities(() => holidayResp.cityModule);
setHolidayPlans(() => holidayResp.holidayModule);
setDate(() => holidayResp.holidayModule);
}
let today = new Date();
console.log(holidayPlansLocal);
holidayPlansLocal.filter((date) => {
const eventDate = new Date(date.event_date);
console.log(eventDate);
});
};

Why I can't use result of function as a state in useState

I try to use useState with an array of objects as a value. Something like this:
const [state, setState] = useState(arg)
Arg is the result of function which returns an array like this:
[{a:1, b:2}, {a:3, b:4}]
But when I try to use it, my state is empty and nothing happens. How to use an arg in useState?
I see another similar questions, but these solutions doesn't work. Or maybe I don't understand.
//take users list from DB
const dispatch = useDispatch()
const items = useSelector((state) => state.users.list)
useEffect(() => {
dispatch(funcThatGetUsersList)
}, [dispatch])
//use items for handler
const [list, setList] = useState([])
function checkboxHandler = (event) => {
do smth with setList(list)
}
Added a little piece of code
If you use lazy initial state, the function will be evaluated only once.
If it returns different values in subsequent renders, those other values will be ignored.
const condition = false
const getArg = () => condition ? arg : []
useState(getArg) // getArg() will be called only once
you cannot use useState directly. Perhaps my solution will help. I passed the value with useEffect. such as
useEffect(() => {
if (list)
setList(yourFunc)
)
}, [yourFunc])

With a React Hooks' setter, how can I set data before the component renders?

I export a JS object called Products to this file, just to replace a real API call initially while I am building/testing. I want to set the function's state to the object, but mapped. I have the component looking like this:
function App() {
const [rooms, setRooms] = useState([]);
const [days, setDays] = useState([]);
const roomsMapped = products.data.map(room => ({
id: room.id,
title: room.title
}))
useEffect(() => {
setRooms(roomsMapped);
})
return ( etc )
This returns the following error: Error: Maximum update depth exceeded.
I feel like I'm missing something really obvious here, but am pretty new to React and Hooks. How can I set this data before the component renders?
Just declare it as initial value of rooms
const Component = () =>{
const [rooms, setRooms] = useState(products.data.map(room => ({
id: room.id,
title: room.title
})))
}
You can also use lazy initial state to avoid reprocessing the initial value on each render
const Component = () =>{
const [rooms, setRooms] = useState(() => products.data.map(room => ({
id: room.id,
title: room.title
})))
}
Change useEffect to this
useEffect(() => {
setRooms(roomsMapped);
},[])
With Lazy initialisation with function as a parameter of useState
import React, { useState } from "react";
function App() {
const [rooms, setRooms] = useState(() => {
// May be a long computation initialization
const data = products.data || [];
return data.map(({ id, title }) => ({ id, title }));
});
return (
// JSX stuffs
)
}
You can use default props for this.set initial value with empty list .
You are getting 'Error: Maximum update depth exceeded', because your useEffect function doesn't have dependency array. Best way to fix this is to pass empty array as the second argument to useEffect like this:
useEffect(() => {
setRooms(roomsMapped);
},[]) <= pass empty array here
this will prevent component to re render, it you want your component to re render on props change you can pass the props in the array like this:
useEffect(() => {
setRooms(roomsMapped);
},[props.props1,props.props2])
here you can pass as many props as you want...

Categories