Can't push from useEffect a new item in array - reactjs

I have the next situation in my react application:
export default function App() {
const arr = []
useEffect(() => {
arr.push('test')
},[])
console.log(arr)
return (
<div className="App">
{
arr.map((i) => <span>{i}</span>)
}
</div>
);
}
Above i try to push a new item in the array. So in the console.log(arr) i expect to have ['test']. Why it does not happen? demo: https://codesandbox.io/s/reverent-mayer-pxswq?file=/src/App.js:59-294

useEffect runs in the next frame, so it won't be populated until later.
You should try this:
const arr = []
useEffect(() => {
arr.push('test')
console.log(arr)
},[])
You also don't want to be setting state like that, use useState.
const [arr, setArr] = useState([])
useEffect(() => {
setArr(a => [...a, 'test'])
}, [arr])

You should use the state hook for your arr.
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([]);
useEffect(() => {
setArr([...arr, "test"]);
}, []);
console.log(arr);
return (
<div className="App">
{arr.map((i) => (
<span>{i}</span>
))}
</div>
);
}

The code you shared is working but the console.log is not at the good place.
Your console.log is login before the useEffect();
export default function App() {
const arr = []
useEffect(() => {
arr.push('test');
console.log('After UseEffect', arr);
},[])
console.log('Before UseEffect',arr)
return (
<div className="App">
{
arr.map((i) => <span>{i}</span>)
}
</div>
);
}

In your case, useEffect run after console.log so arr still empty when log,
Agree with #Stoobish, but:
Tracking arr change should use useDebugValue or log inside useEffect
If you want to prevent duplicate render when state change, use useEffectLayout instead of useEffect.
import { useEffect, useLayoutEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([]);
useLayoutEffect(() => {
setArr([...arr, "test"]);
}, []);
useEffect(() => {
console.log(arr);
}, [arr])
return (
<div className="App">
{arr.map((i) => (
<span key={i}>{i}</span>
))}
</div>
);
}

You should consider three point below:
You should use useState hook.
So Use
const [arr, setArr] = useState([]);
Not
const arr = []
When you use React you must consider useState because when you update any variable using useState Hook then React will update the state and can be able to rerender your component. Otherwise, your component will not be updated.
You should not push directly into the array arr.
So Use
setArr(item => [...item, newItem])
Not
arr.push('test')
As I explain it above, if you dont use setArr then React will not update your component.
You should use key when you use multiple html element using map function.
So Use
{arr.map((a, i) => (
<span key={i}>{a}</span>
))}
Not
arr.map((i) => <span>{i}</span>)
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.
Here is the complete example:
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([]);
useEffect(() => {
let newItem = 'test';
setArr(item => [...item, newItem]); // Here we are using `spread operator` to use previous items from `arr`
console.log(arr);
}, []);
return (
<div className="App">
{arr.map((a, i) => (
<span key={i}>{a}</span>
))}
</div>
);
}

Related

useEffect and useState to fetch API data

I want to use useEffect(on mount) to fetch from API and store it in useState. Fetch API is used to get the data. The problem is when initial page loading and also when I reload the page, it outputs an error called test.map is not a function. Why this happening and how to avoid this ?
import { useEffect, useState } from 'react';
function App() {
const[test, setTest] = useState({})
useEffect(() => {
testfunc()
}, [])
async function testfunc(){
let api = await fetch('https://jsonplaceholder.typicode.com/users')
let apijson = await api.json()
setTest(apijson)
}
return (
<div className="App">
{
test.map((item) => {
return(
<div>
{item.name}
</div>
)
})
}
</div>
);
}
export default App;
You can't map on an object {}, so you should need to define an array [] for the base state :
const[test, setTest] = useState([])
You have to change {} to array first to be able to map over it. You can easily place ? after test like this. or make in the default value of the state a default value for item name. because this error results as you map over an empty object.
import { useEffect, useState } from 'react';
function App() {
const[test, setTest] = useState([{name:"default"}])
useEffect(() => {
testfunc()
}, [])
async function testfunc(){
let api = await fetch('https://jsonplaceholder.typicode.com/users')
let apijson = await api.json()
setTest(apijson)
}
return (
<div className="App">
{
test?.map((item) => {
return(
<div>
{item.name}
</div>
)
})
}
</div>
);
}
export default App;
As already mentioned, you can't use the .map for objects.
Instead of this, you can make something like that
Object.keys(test).map(key => {
const currentSmth = test[key]
return(
<div>
{currentSmth.name}
</div>
)
})
})
I think it helps you to solve your problem.
Be careful using the correct data structures and methods.

Array in a react state is not updating correctly

I basically try to update filter the items from the all locations array to dropDownLocation array, but it is not updating correctly. on the first change in input field it wrongly update the array and the second change it does not update it.
import logo from "./logo.svg";
import "./App.css";
import { useState, useEffect } from "react";
function App() {
// location entered by the user
const [location, setlocation] = useState("");
const [allLocations, setallLocations] = useState(["F-1", "F-2", "G-1", "G-2"]);
const [dropDownLocations, setdropDownLocations] = useState([]);
const filterLocations = (userInput) => {
console.log("user input ", location);
allLocations.map((i) => {
if (i.includes(location)) {
console.log("true at ", i);
setdropDownLocations([...dropDownLocations, i]);
} else {
setdropDownLocations([]);
}
});
console.log("after map ", dropDownLocations);
};
return (
<div className="App">
<div>
<input
value={location}
onChange={(e) => {
setlocation(e.target.value);
filterLocations(e.target.value);
}}
/>
<ul>
{dropDownLocations.map((i) => (
<li key={i}>{i}</li>
))}
</ul>
</div>
</div>
);
}
export default App;
You don't need to make that complicated, Just filter the array based on the user's input
const filterLocations = (userInput) => {
setdropDownLocations(
allLocations.filter((location) => location.includes(userInput))
);
};
I made it simpler for you in this working example:
The setState is an asynchronous function and so your current implementation isn't working properly as you are trying to read the state before it is updated.
Update your filterLocations function like following:
const filterLocations = (e) => {
const location = e.target.value;
const filteredLocation = allLocations.filter(i => i.includes(location));
setlocation(location);
setdropDownLocations(filteredLocation)
};
And update your input tag like following:
<input value={location} onChange={filterLocations} />
It is not working because for each location, you are setting dropdown location, and if it doesn't contain the location, you set it to empty array [] again.
allLocations.map((i) => {
if (i.includes(location)) {
console.log("true at ", i);
setdropDownLocations([...dropDownLocations, i]);
} else {
setdropDownLocations([]);
}
});
A better approach would be:
setDropDownLocation([...allLocations].filter((i) => i.includes(userInput))
There is some mistakes what you have done, I have made some changes try to run the code which I have written.
import logo from "./logo.svg";
import "./App.css";
import { useState, useEffect } from "react";
const ALL_LOCATIONS = ['F-1', 'F-2', 'G-1', 'G-2'];
function App() {
// location entered by the user
const [location, setLocation] = useState("");
const [dropDownLocations, setDropDownLocations] = useState([]);
function onLocationInputChange(event){
setLocation(event.target.value);
setDropDownLocations(ALL_LOCATIONS.filter((item)=>item.includes(event.target.value)))
}
return (
<div className="App">
<div>
<input value={location} onChange={onLocationInputChange} />
<ul>
{dropDownLocations.map((loc) => (
<li key={`${location}-${loc}`}>{loc}</li>
))}
</ul>
</div>
</div>
);
}
export default App;
This is caused by the fact that your onChange handler is defined right in the JSX, causing React to recreate a new function at every render (same goes for filterLocations one).
You should always try to extract every single piece of JS logic outside of the component, or at least memoize them, here's how:
import React, { useState, useCallback } from "react";
import logo from "./logo.svg";
import "./App.css";
const ALL_LOCATIONS = ['F-1', 'F-2', 'G-1', 'G-2'];
function App() {
// location entered by the user
const [location, setLocation] = useState("");
// locations shown to the user in dropdown (filterable)
const [dropDownLocations, setDropDownLocations] = useState([]);
const onLocationInputChange = useCallback(
(ev) => {
// In case no target passed to callback, do nothing
if (!ev || !ev.target || !ev.target.value) {
return;
}
const userInput = ev.target.value;
// Filter so that if user input matches part of the location
// it gets not filtered out
setDropDownLocations([
...ALL_LOCATIONS.filter(
(loc) =>
loc.startsWith(userInput) ||
loc.endsWith(userInput) ||
loc.indexOf(userInput) !== -1
),
]);
// Finally update the location var
setLocation(userInput);
},
[setDropDownLocations]
);
return (
<div className="App">
<div>
<input value={location} onChange={onLocationInputChange} />
<ul>
{dropDownLocations.map((loc) => (
<li key={`${location}-${loc}`}>{loc}</li>
))}
</ul>
</div>
</div>
);
}
export default App;

I would expect this useRef and useEffect combo to fail...why does it succeed?

I would expect this useEffect to fail on the first render, since I would assume the innerCarouselRef.current would be undefined on the first render and it makes a call to getBoundingClientRect. Why does it work/why is the innerCarouselRef.current defined when the useEffect runs?
import React from 'react';
import { debounce } from 'lodash';
export default function Carousel({ RenderComponent }) {
const [innerCarouselWidth, setInnerCarouselWidth] = React.useState(0);
const [itemWidth, setItemWidth] = React.useState(0);
const innerCarouselRef = useRef();
const itemRef = useRef();
const content = data.map((el, i) => {
return (
<div key={`item-${i}`} ref={i === 0 ? itemRef : undefined}>
<RenderComponent {...el} />
</div>
);
});
useEffect(() => {
const getElementWidths = () => {
setInnerCarouselWidth(innerCarouselRef.current.getBoundingClientRect().width); // why doesn't this call to getBoundingClientRect() break?
setItemWidth(itemRef.current.getBoundingClientRect().width);
};
getElementWidths();
const debouncedListener = debounce(getElementWidths, 500);
window.addEventListener('resize', debouncedListener);
return () => window.removeEventListener('resize', debouncedListener);
}, []);
return (
<div className="inner-carousel" ref={innerCarouselRef}>
{content}
</div>
)
}
React runs the effects after it has updated the DOM (we typically want it to work that way). In your case, the effect runs after the component has mounted and so innerCarouselRef.current is set.
I would recommend reading the useEffect docs to gain a better understanding.

React hooks, update one state in response of another state update

How to have second state produced from first state and when the first one changes, make second state to react in response to update it ?
It worked but I am not sure how reliable is doing it that way, any suggestions ? Thanks.
export default function App() {
const [arr, setArr] = useState([1, 2, 3, 4, 5]);
const [length, setLength] = useState(arr.length);
console.log(arr)
const clickHandler = () => {
setArr([...arr, arr.length+1])
};
useEffect(() => {
setLength(length + 1)
}, [arr])
return (
<div className="App">
<h2>{length}</h2>
<button onClick={clickHandler}>click</button>
</div>
);
}
so you want the 'second state' (length) to be updated every time the length of the array changes. For that, you need an effect that will take the length of the array as a dependency (arr.length), and then set length to that value.
Also added a change to the clickHandler function as the state is computed from the previous state (check this for further details)
This is how the final code would look
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [arr, setArr] = useState([1, 2, 3, 4, 5]);
const [length, setLength] = useState(arr.length);
console.log(arr);
const clickHandler = () => {
setArr((arr) => [...arr, arr.length + 1]); // Change #1
};
useEffect(() => {
setLength(arr.length); // Change #2
}, [arr.length]);
return (
<div className="App">
<h2>{length}</h2>
<button onClick={clickHandler}>click</button>
</div>
);
}

Counter increment on setInterval

import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [lat, setLat] = useState(0);
const [long, setLong] = useState(0);
const [count, setCount] = useState (0);
const Counter = () => {
setCount(count + 1)
console.log(count)
}
const Location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setLat ( result.coords.latitude)
)
Plugins.Geolocation.getCurrentPosition().then(
result => setLong (result.coords.longitude)
)
}
const interval = () => {
setInterval (() =>
{
Location();
Counter();
}, 5000 );
}
return (
<div>
<div>
<button onClick = {interval}>
Get Location
</button>
</div>
<div>
{long}
</div>
<div>
{lat}
</div>
</div>
)
}
export default Maps;
I'm trying to get the counter to increment on every iteration of setInterval, through the counter function, but when I log count, it does not increment and always remains as 0.
I've tried running setCount itself within setInterval without any success, it still does not increment count.
Its a stale closure. Change to this setCount(prevCount => prevCount + 1).
Using the updater form of set state like above, you can guarantee that you will be using the most recent value of state.
You can think of it as count in your function being a snapshot of what its value was when the setInterval was declared. This will stop your updates from appearing to work.
In addition, setting state is async, so the console.log(count) will most likely not reflect the new value. Log in an effect or outside the function body to see the updated value each render.
A note about your implementation:
You are creating a setInterval each time the button is clicked. This could lead to some interesting side-effects if clicked more than once. If you click the button twice for example, you will have two setIntervals running every 5 seconds.
In addition to #BrianThompson answer. Try this to avoid innecessary rerenders
import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [state, setState] = useState({
latLng:{lat:0,lng:0},
counter: 0
})
const interval = useRef()
//Use camelCase for methods
const location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setState ( ({counter}) => {
counter = counter+1
console.log(counter)
return ({
latLng: {
lat: result.coords.latitude,
lng: result.coords.longitude
},
counter
})
})
)
}
const startInterval = () => {
if(interval.current) return;
interval.current = setInterval (() => {
location();
}, 5000 );
}
const stopInterval = () ={
clearInterval(interval.current)
interval.current = null
}
useEffect(()=>{
//Because interval is causing state updates, remember to clear interval when component will unmount
return stopInterval
},[])
return (
<div>
<div>
<button onClick = {startInterval}>
Get Location
</button>
</div>
<div>
{state.latLng.lng}
</div>
<div>
{state.latLng.lat}
</div>
</div>
)
}

Resources