There is a container ul that stays at a set height so when the content inside the ul overflows, it should remove the first item from the list. Each list item is shown as a search result and the ul can only hold 6 results without overflowing. The list is meant to be changed using setRecent as it was declared using useState, but when I try to change the list by removing the first item using [...list].splice(1), it returns this error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
When I remove the setRecent and just return the value [...list].splice(1) it returns the list without the first item as it should, however the setRecent is needed to change the value of the list.
How do I fix this?
const [recent, setRecent] = useState(["Dubai", "Paris", "London", "paris", "new york", "dubai", "New York"]);
const InsideSection = () => {
const ulRef = createRef();
useLayoutEffect(() => {
if (onSearch) {
const element = ulRef.current;
if (element.scrollHeight > element.clientHeight) { // checks for overflow
console.log("overflow");
console.log(recent, "before"); // returns [] instead of the list
console.log([...recent].slice(1));
// this should return ["Paris", "London", "paris", "new york", "dubai", "New York"] (without the first "Dubai")
// but when there is a setRecent inside the if statement, it causes an error as recent is []
setRecent([...recent].slice(1)); // causes the error
console.log(recent, "after"); // returns []
}
}
}, [ulRef]);
if (onSearch) {
return (
<div className = "search-container" >
...
<ul ref = {ulRef} >
<RecentSearches />
</ul>
</div >
);
}
}
It's not perfect, but it kinda works if you use a debouncing strategy with a ref and a timeout:
https://codesandbox.io/s/pensive-gould-4bmhns?file=/src/App.js
import { createRef, useLayoutEffect, useState, useRef } from "react";
export default function App() {
const [recent, setRecent] = useState([
"Dubai",
"Paris",
"London",
"paris",
"new york",
"dubai",
"New York"
]);
const [updatedRecent, setUpdatedRecent] = useState();
const onSearch = true;
const ulRef = createRef();
let timeoutId = useRef();
useLayoutEffect(() => {
console.log("> debounced", updatedRecent);
setRecent(() => updatedRecent);
timeoutId.current = null;
}, [updatedRecent]);
useLayoutEffect(() => {
if (onSearch) {
const element = ulRef.current;
if (element.scrollHeight > element.clientHeight) {
if (!timeoutId.current) {
timeoutId.current = setTimeout(() => {
console.log("update");
setUpdatedRecent(() => [...recent].slice(1));
});
}
}
}
}, [onSearch, ulRef, recent, timeoutId]);
// [...]
Your problem is defining your InsideSection component inside the parent component. When from the InsideSection you call the setRecent state handler of the parent, you are causing a re-rendering which causes the parent component to be executed again and your InsideSection component to be re-created.
This triggers your useLayoutEffect once more, the useLayoutEffect re-update the state of the parent which re-renders and re-create the InsideSection which re-triggers useLayoutEffect again and again.
Each time, it slices your recent array from ["Dubai", "Paris", "London", "paris", "new york", "dubai", "New York"] to ["Paris", "London", "paris", "new york", "dubai", "New York"] to ["London", "paris", "new york", "dubai", "New York"] and so on until it is [] and cannot be sliced more (this is where it throws an error).
The solution is to move the InsideSection component out of the parent and, if the recent variable is only used by the InsideSection component, define it there and not in the parent.
If the recent variable needs to be used by both, parent and child, then a "less" clean solution is to pass it as a prop.
The possible solution looks like this:
const InsideSection = (props) => {
const { onSearch, recent, setRecent } = props;
const ulRef = createRef();
useLayoutEffect(() => {
if (onSearch) {
const element = ulRef.current;
if (element.scrollHeight > element.clientHeight) {
setRecent([...recent].slice(1));
}
}
}, [ulRef]);
if (onSearch) {
return (
<div className = "search-container" >
...
<ul ref = {ulRef} >
<RecentSearches />
</ul>
</div >
);
}
return <></>;
}
const Parent = () => {
const onSearch = true; // Dunno where it comes from
const [recent, setRecent] = useState(["Dubai", "Paris", "London", "paris", "new york", "dubai", "New York"]);
return (
<InnerSection onSearch={onSearch} recent={recent} setRecent={setRecent} />
);
}
The other solution is to use Context, I leave you the documentation: https://reactjs.org/docs/context.html
PS: The code might need some adjustment, it is just an example
EDIT: Gist containing a complete example: https://gist.github.com/ThomasSquall/cdbea86f1204d8e4c0b627aacc92c02f
Related
I am just trying to figure out a weird issue in my React project. So I am executing some code in my useEffect. As you can see, I am trying to grab the currentObj from an array of objects based on searchTerm. However, there seems to be a race condition
in getting the value searchTerm. The toggleFilter ans setAvailabilities functions below are throwing an error saying that the id is not defined. And the page crashes.
useEffect(() => {
const search = window.location.search;
const params = new URLSearchParams(search);
let param = "";
if (search.includes("neighborhood")) {
param = params.get("neighborhood");
const searchTerm = search.toLowerCase();
if (searchTerm) {
const currentObj = searchTerm && locations.find(item => item.linktitle.toLowerCase().includes(searchTerm));
console.log(searchTerm, currentObj);
toggleFilter(currentObj.id);
setAvailabilitiesFilter([currentObj.id]);
}
}
return () => resetAllFilters();
}, []);
However, if I hardcode a value into it, it actually renders ok.
const currentObj = searchTerm && locations.find(item => item.linktitle.toLowerCase().includes('manhattan'));
But of course, I don't want to hardcode the value as I expect to dynamically render the searchTerm
Is there any way I can in the useEffect wait until searchTerm is defined before running that .find method? I also tried adding the searchTerm in the dependency array but that doesn't seem right as it is not giving suggestions there and seems not to be scoped there.
Any help appreciated. Thanks!
I attempted to replicate what you were doing to some extent in a code sandbox and the functionality seems to work. Originally, it worked fine until I passed a string which was not in the locations array and I got the same undefined error.
So basically, what you need to do is first run the find function. Then only, if there is a match, i.e. the find function does not return undefined, will the togglefilters and other code be run. You can see the code below.
import { useEffect, useState } from "react";
import "./styles.css";
const locations = [
{
id: 1,
linktitle: "Paris",
country: "France"
},
{
id: 2,
linktitle: "Madrid",
country: "Spain"
},
{
id: 3,
linktitle: "Shanghai",
country: "China"
},
{
id: 4,
linktitle: "London",
country: "England"
}
];
export default function App() {
const [filter, setFilter] = useState("");
const toggleFilter = (id) => {
console.log("toggled id:", id);
setFilter(id);
};
useEffect(() => {
const search = window.location.search;
// this statement avoids any matches with empty strings
if (search === "") return;
const searchTerm = search.slice(1).toLowerCase();
// first check whether a location is matched
// before running any other functionality
const location = locations.find((item) =>
item.linktitle.toLowerCase().includes(searchTerm)
);
if (location) {
toggleFilter(location.id);
console.log(searchTerm, location);
}
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
The code sandbox link is here where you can see what is happening and view the console logs:
https://codesandbox.io/s/currying-sound-wyb0iz?file=/src/App.js:0-831
You can see the console logs of the objects appear if they are cities in the location array which have been queried in the query params. Also, there is no undefined with the IDs which are filtered.
I sliced the searchTerm so that the initial "?" would be removed from the string.
Let's say I have an array like this:
[
{
country: '',
'city/province': '',
street: ''
},
{
country: '',
'city/province': '',
street: ''
}
]
How do I have the useEffect() hook run every time the value of the 'country' field in any item inside the array changes?
Normally you wouldn't want to do that, but just to answer your question, it can be done, so let me propose the following assuming your list is called items:
useEffect(() => {
}, [...items.map(v => v.country)])
What the above code does is to spread all items (with its country property) into the useEffect dependency array.
The reason why this can be adhoc is mainly because React doesn't like to have a variable length of dependency. In the source code, when the length changes, it only appreciates the element change from the existing elements. So you might run into problem if you switch from 1 elements to 2 elements.
However if you have fixed number of elements, this should do what you wanted. Keep in mind the items has to be an array at all time.
NOTE: to accommodate the length issue, maybe we can add an additional variable length to the dependency array :)
}, [items.length, ...items.map(v => v.country)])
As i mentioned, most of time, you should avoid doing this, instead try to change the entire items every time when an item changes. And let the Item display to optimize, such as React.memo.
I don't think you can specifically tackle it in the dependency array, however, you can do your check inside the useEffect to have the same overall outcome.
Basically, the dependency array is passed the full data state, which will trigger the effect every change, then you do a further check if the sub property has changed.
I'm leverage lodash for brevity, but you can run any function to determine if the data has changed.
Codepen: https://codepen.io/chrisk7777/pen/mdMvpvo?editors=0010
const { useState, useEffect, useRef } = React;
const { render } = ReactDOM;
const { isEqual, map } = _;
const App = () => {
const [data, setData] = useState([
{
country: "",
"city/province": "",
street: ""
},
{
country: "",
"city/province": "",
street: ""
}
]);
const prevData = useRef(data);
// hacky updates just to demonstrate the change
// change country - should trigger useEffect
const update1 = () => {
setData((s) => [s[0], { ...s[1], country: s[1].country + "a" }]);
};
// change street - should not trigger useEffect
const update2 = () => {
setData((s) => [s[0], { ...s[1], street: s[1].street + "a" }]);
};
useEffect(() => {
if (!isEqual(map(prevData.current, "country"), map(data, "country"))) {
console.log("country changed");
}
prevData.current = data;
}, [data]);
return (
<div>
<button onClick={update1}>change country - trigger effect</button>
<br />
<button onClick={update2}>change street - do not trigger effect</button>
</div>
);
};
render(<App />, document.getElementById("app"));
Just map the countries into the effect dependency array.
const countries = data.map((x) => x.country);
useEffect(() => {
console.log(countries);
}, countries);
I've got the following state:
const [places, setPlaces] = useState(false)
const [selectedPlaces, setSelectedPlaces] = useState([])
I asynchronously populate places by calling an API that returns an array of objects that looks something like:
[
{name: "Shop #1", id: 1},
{name: "Shop #2", id: 2}
]
My goal is to render these objects, and have their ID to be added/removed from the selectedPlaces state.
Render:
return (
<div>
<div>
You have selected {selectedPlaces.length} total places
</div>
(places === false)
? <div>Loading...</div>
: places.map(place => { // render places from the places state when loaded
let [name, id] = [place.name, place.id]
return <div onClick={() => {
setSelectedPlaces(selected => {
selected.push("dummy data to simplify")
return selected
})
}}>{name}</div>
})
</div>
)
I've removed the key and added dummy data to make the movements simpler.
The problem arises when clicking on a div, the "You have selected ... total places" doesn't refresh until I force a re-render using fast refresh or through other methods (using browser/NextJS). Is this correct behaviour? It's as-if the state isn't being changed, but a console.log on the setSelectedPlaces displays fresh data symbolizing it is being changed.
I've tried:
Creating a useEffect handler for the selectedPlaces state which would setAmtPlaces using the length of the selected places. The same issue arises.
Searched/read-through multiple posts/GitHub issues like this and this
Replacing the list state with true/false in previous times I've encountered this issue, but I cannot use that approach with this problem since it's a dynamic amount of data being loaded.
Add a {} wrapper for the ternary operator:
{
places === false
? (...)
: (....)
}
push mutates the state. Use spread or concat in setSelectedPlaces
setSelectedPlaces(selected =>
selected.concat("abc")
)
let [name, id] = [place.name, place.id] can be change to let { name, id } = place
Here's a snippet:
const { useState } = React;
function App() {
const [places, setPlaces] = useState([
{ name: "Shop #1", id: 1 },
{ name: "Shop #2", id: 2 }
])
const [selectedPlaces, setSelectedPlaces] = useState([])
const onPlaceClick = id => setSelectedPlaces(state => state.concat(id))
return (
<div>
<div> You have selected {selectedPlaces.length} total places </div>
{ (places === false)
? <div>Loading...</div>
: places.map(({ id, name }) =>
<div onClick={_ => onPlaceClick(id)}>{name}</div>
)
}
<span>{selectedPlaces.join()}</span>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I have a list of country names and when clicked on one of the countries, the state of the picked country should be updated with the name of the newly selected country. This state then triggers other changes in a useEffect(). The state of pickedCountry is handled in a parent component, but the setPickedCountry() function is passed to the child component (which creates the country list) as a prop.
When I now add a onPress={props.setCountry.bind(this, country.name)} to each of the list items I am getting a warning stating:
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
Right now I don't know how a useEffect would help me here. Could somebody help me out?
These are my components:
Country Data
[
{
"name": "Germany",
"data": [*SOME DATA*]
},
{
"name": "France",
"data": [*SOME DATA*]
}, ...
]
Parent Component
const [pickedCountry, setPickedCountry] = useState(null);
useEffect(() => {
if (pickedCountry!= null) {
//Do Something
}
}, [pickedCountry]);
return (
<Child setPickedCountry={setPickedCountry} />
);
Child Component
const [child, setChild] = useState([]);
useEffect(() => {
const myComponents= [];
for (country of myData) {
myComponents.push(
<TouchableOpacity
onPress={props.setCountry.bind(this, country.name)} />
)
}
setChild(myComponents);
}, [someProps];
return (
{child.map((x) => x)}
)
Functional components are instanceless, therefore, there is no this to do any binding anyway. It looks as if you simply want to pass a country name as a new state value, i.e. onPress={() => props.setCountry(country.name)}.
useEffect(() => {
const myComponents= [];
for (country of myData) {
myComponents.push(<TouchableOpacity onPress={() => props.setCountry(country.name)} />)
}
setChild(myComponents);
}, [someProps];
Or create a curried function handler so only a single handler is defined and passed. The country name as saved in the enclosure.
useEffect(() => {
const myComponents= [];
const onPressHandler = name => () => props.setCountry(name);
for (country of myData) {
myComponents.push(<TouchableOpacity onPress={onPressHandler(country.name)} />)
}
setChild(myComponents);
}, [someProps];
I'm displaying cards with a map function on my state.
The state looks like this:
[{ position: "0", state: "pending" },
{ position: "1", state: "pending" },
{ position: "2", state: "pending" }]
In a function, I'm changing the "state" from "pending" to "right", but it does not refresh anything on the page.
export const Card: React.FC<ICardOwnProps> = ({ }) => {
const [rollingCards, setRollingCards] = useState<IRollingCard[]>([{ position: "0", state: "pending" }, { position: "1", state: "pending" }, { position: "2", state: "pending" }])
const handleClick = () => {
let newArray = rollingCards;
newArray.splice(0, 1)
setRollingCards(newArray);
}
useEffect(() => { console.log("check changes") })
return (
<StyledCard className="Card">
{rollingCards.map((v, i, a) =>
<Container key={i} className={"position" + v.position} >{v.state}</Container>
)}
<div className="card__container--button">
<button onClick={handleClick}>Oui</button>
<button>Non</button>
{rollingCards[0].state}
</div>
</StyledCard>
);
}
From now, nothings changes, but when I add a new state piece that I update at the same time as the rollingCards, it'll update. I added a counter, and with that, it updates the page.
(I also tried to splice the array to see if React would update, it did not)
Any idea why I've got this weird behavior ?
Thanks.
This is because React does a referential equality comparison of props/state when making a decision to rerender.
newArray has the same reference as rollingCards due to which the functional component does not re render on setRollingCards
Change
setRollingCards(newArray)
to
setRollingCards([...newArray])
This creates a new array (new reference) with the elements from newArray
Working solution:
https://codesandbox.io/s/admiring-wave-9kgqq
Because react uses Object.is to determine if the state has changed.
In your code, newArray is the same reference as rollingCards, you can use Object.is(newArray, rollingCards) to check.
const handleClick = () => {
let newArray = rollingCards;
newArray.splice(0, 1) // Object.is(newArray, rollingCards) is true
setRollingCards(newArray);
}
So you can change it like this.
setRollingCards(rollingCards.filter((item, index) => index !== 0);
Not only this method, as long as it is a different reference