React Hooks - useState initializes empty array that becomes object? - javascript

In the following code, I believe I am initializing gameList as an empty array. The first console.log shows gameList is an empty array. I then use a console.log in useEffect() that displays gameList as an object but I do not believe that I am doing anything to transform gameList. Can anyone explain this? I was trying to pass an array of objects to a child component but even upon using Object.values() on the gameList "object" it is still returning as an object. Thanks!
Edit: Perhaps the way I should've has asked this is: "Why does gameList show up in child component as an object with gameList as property? Why does it not arrive in the GameList component as an empty array called gameList? This is happening before submitting my form by the way.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import GameList from './GameList';
const Searchbar = () => {
const [searchString, setSearchString] = useState('');
const [gameList, setGameList] = useState([]);
console.log(gameList); // []
useEffect(() => {
console.log('gameList is ' + typeof gameList); // gameList is object
});
const requestGames = searchString => {
axios
.get(`http://localhost:3001/game/${searchString}`)
.then(({ data }) => setGameList(data))
.catch(e => console.log(e));
};
return (
<div>
<form
onSubmit={e => {
e.preventDefault();
requestGames(searchString);
}}
>
<label htmlFor="search-string">Search</label>
<input
type="text"
placeholder="search.."
onChange={e => setSearchString(e.target.value)}
/>
</form>
<GameList gameList={gameList} />
</div>
);
};
export default Searchbar;

Arrays have type object in JavaScript:
console.log(typeof []) //=> "object"
You can read more about the typeof operator on MDN [1].
To check if something is an array you can do this:
console.log(Array.isArray([])) //=> true
Or this:
console.log(Object.prototype.toString.call([])) //=> "[object Array]"
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof

Related

How to fix Data.map is not a function in Reactjs?

So I have a Notesdata array containing object with properties title, tagline, description. Notesdata array is stored in the "data" state variable in the App component and I am sending this data state variable to notes view component to populate on the UI but when I am appending a new object inside the data state variable and then sending it to notesview component, I am getting the error "Data.map is not a function".
When I am printing the "data" state variable I am getting an array but when I am checking its type it's showing "object", I am confused in why it is showing like that.
I also tried using Array.from() on the "data" state variable before passing it to notesview but that is also showing the same error.
------------App component------------------
import React, { useState } from "react";
import './App.css';
import Input from './Components/Input';
import Navbar from './Components/Navbar';
import Notesview from './Components/Notesview';
import Notesdata from "./Data/Notesdata";
function App() {
// const [data, setdata] = useState(Notesdata);
const [data, setData] = useState(Notesdata);
function handleDelete(id) {
let newData = data.filter((item) => item.id !== id)
setData(newData)
}
function handlePost(value) {
let newval = data.push(value)
setData(newval)
console.log(typeof data)
console.log(data)
}
return (
<div className="App">
<header className="App-header">
<Navbar />
<Input data={data} handlePost={(value) => handlePost(value)} />
<Notesview handleDelete={handleDelete} Data={data} />
</header>
</div>
);
}
export default App;
-----------------notesview component-----------------------
import React from 'react'
import Notescard from './Notescard'
import "../Styles/Notes.css"
const Notesview = ({ Data, handleDelete }) => {
return (
<>
<div className='notes'>
{
Data.map((item) => { // here is the Data.map where the error is coming
return <Notescard item={item} handleDelete={handleDelete} />
})
}
</div>
</>
)
}
export default Notesview
There's a lot wrong right here:
let newval = data.push(value)
setData(newval)
console.log(typeof data)
console.log(data)
Array.prototype.push returns the length of the array, so you're setting data to a number. (Which, incidentally, does not have a .map() function.)
You're mutating a state value (data) before trying to update it. Just update it.
You're trying to examine the value after it's been updated, but you're examining the current value and not the new value. State updates are asynchronous.
To update the state correctly, you'd do something more like this:
setData([...data, value]);
If you might have a batch of updates and you want each state update to use the updating state in the batch rather than the current state in the render, you could use the callback version of the state setter:
setData(d => [...d, value]);
This creates a new array reference, which includes all of the elements in the data array as well as the new value element, and sets that reference as the updated state. Without mutating the current state for the current render.
You get this error because Data is null. you can check Data's existence before trying to map on it in Notesview like this:
Data && Data.map(...)

Why my useEffect don't work ? And why the asynchrone function don't work?

I don't understand why my "console.log(champion)" return nothing ...
Someone can explain me why the asynchrone function don't work ? Isn't setCahmp supposed to change the value of "champions"?
I guess it because axios take sometime to search datas... I don't know how I could fix it.
And then I would like to map "champion" but its an object, how I could do that ?
Thans you
import React, { useEffect, useState } from "react";
import axios from "axios";
const Champs = () => {
const [champions, SetChampions] = useState([]);
useEffect(() => {
axios
.get(
"http://ddragon.leagueoflegends.com/cdn/12.5.1/data/en_US/champion.json"
)
.then((res) => {
SetChampions(res.data.data);
console.log(res.data.data);
})
.then(
console.log(champions)
);
}, []);
return (
<div className="champs">
{/* {champions.map((champ) => {
return <p> {champ.id}</p>;
})} */}
</div>
);
};
export default Champs;
In your API response response.data.data is not an array of objects, it's nested objects and you are initializing the champions as an array. So, setChampions can't assign an object to an array.
Also, you can't use the map function to loop an object. You can use Object.keys to map the response.
You shouldn't do a double "then" on your code. If you want to know when the state champions is set you should use a second useEffect with "champions" in param :
useEffect(() => {
axios
.get(
"http://ddragon.leagueoflegends.com/cdn/12.5.1/data/en_US/champion.json"
)
.then((res) => {
SetChampions(res.data.data);
console.log(res.data.data);
});
}, []);
useEffect(() => {
console.log(champions)
}, [champions]);
If you want to map an object you should do this :
<div className="champs">
{Object.keys(champions).map((key) => {
const champ = champions[key]
return <p> {champ.id}</p>;
})}
</div>
Object.keys will return an array of key of your object, so you can map it. And to access to the value you can simply use the key like this : const champ = champions[key]
Hoping that can help you in your research
It could be that console.log(champion) isn't working because it's getting called before SetChampion is completed. You don't need the 2nd .then() call to check on champion. To make sure champion is getting set, you could make a useEffect that is called whenever champion gets set, like so:
useEffect(() => {
console.log(champion);
}, [champion])
This will get called when champion is initially set to [] with useState, and then when you set it with SetChampions().

Unable to update a list with react when its value is being changed using the hook useState function from React TypeError: map is not a function

I want to get a list of values updated whenever its value is changed through a hook setState function, however I am getting an error I don't know why... I am getting a .map is not a function TypeError
Down bellow is my code and I also have a codesandbox link: https://codesandbox.io/s/fervent-satoshi-zjbpg?file=/src/Incomes.js:23-1551
import axios from "axios";
import { useState, useEffect } from "react";
const fetchInvestment = async () => {
const res = await axios.get(
"https://6r3yk.sse.codesandbox.io/api/investments/60b2696de8be014bac79a2a1"
);
return res.data.invest.incomes;
};
export default function Incomes() {
const [incomes, setIncomes] = useState([]);
const [date, setDate] = useState(undefined);
const [value, setValue] = useState(undefined);
useEffect(() => {
const getInvestments = async () => {
const res = await fetchInvestment();
setIncomes(res);
};
if (incomes.length === 0) {
getInvestments();
}
console.log(incomes);
}, [incomes]);
return (
<div>
<h1>Hello CodeSandbox</h1>
<input
id="monthEl"
type="month"
value={date}
onChange={(e) => {
setDate(e.target.value);
}}
/>
<input
id="monthEl"
type="number"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button
onClick={() => {
const income = {};
income[date] = Number(value);
setIncomes(incomes.push(income));
setTimeout(() => {
console.log(incomes);
}, 2000);
}}
>
Save
</button>
<ul>
{incomes.map((income) => (
<li key={Object.keys(income)}>
{Object.keys(income)}: {Object.values(income)}
</li>
))}
</ul>
</div>
);
}
Replace this line:
setIncomes(incomes.push(income));
With this
setIncomes([...incomes, income]);
The .push method returns the length of the array, not the actual array. You can use the spread operator to spread the current array and then add on the new item to the end of it.
Doing this should also work:
incomes.push(incomes)
setIncomes(incomes)
It's possible you're getting that error because the data coming back from your API isn't an array. Judging by your code, I'm guessing you're expecting a key/value map, which in JS is an object. You might be able to use Object.keys(incomes).map(...), but without knowing the specific response format, I can't say for sure.
There are 2 other issues with your code:
First, you can't push onto incomes because it's a React state array. Instead, you need to use the setIncomes callback...
setIncomes([...incomes, income])
Additionally, the way you're using Object.keys and Object.values is not correct. Again, can't say what the correct way is without knowing specifics of your response format.

How to update props correctly in React functional component

I'm new to react, wanted to ask if this piece of code is good practice, because I have a feeling I'm doing something wrong but not sure what.
I have a main class component which has an array of packages which consists of width, height etc.
I'm passing this packages array as props to another functional component where I want to update these values. Currently my implementation looks like this:
<Card pck={pck} key={pck.packageId}/>
export default function Card(props) {
const widthProperties = useState(0);
props.pck.width = widthProperties[0]
const setWidth = widthProperties[1];
<input type="number" id={props.pck.packageId} className="form-control"
value={props.pck.width}
onChange={(e) => setWidth(parseInt(e.target.value))}
placeholder="Width" required/>
}
It works correctly, but as I said, I believe that I'm not using the useState with props correctly. Could someone explain what is wrong here? Because 3 lines of code to update props' state looks strange for me.
You never directly mutate props like you do here: props.pck.width = widthProperties[0].
To have a correct data flow, width and setWidth should be in the parent component and passed down to the children, so that the children can update their width by calling setWidth.
So, since the parent is a class component, you will have something like:
class CardsList extends Component {
state = {
packages: []
}
componentDidMount() {
fetch('api.com/packages')
.then(response => response.json())
.then(result => this.setState({ packages: result.items }))
}
setWidth = (width, packageId) => {
this.setState({
packages: this.state.packages.map(
pck => pck.packageId === packageId ? { ...pck, width } : pck
)
})
}
render() {
return (
<div className="cards-list">
{this.state.packages.map(pck => (
<Card pck={pck} setWidth={this.setWidth} key={pck.packageId}/>
))}
</div>
)
}
}
And a Card component like:
const Card = ({ pck, setWidth }) => (
<input value={pck.width} onChange={e => setWidth(e.target.value, pck.packageId)} />
)
It's common to destructure the value and setter function from useState like so:
[value, setValue] = useState(initialValue);
From what I gather from your question, the props.pck.width is the initial value of the input, so you may do something like this:
[width, setWidth] = useState(props.pck.width);
<input type="number" id={props.pck.packageId} className="form-control"
value={width}
onChange={(e) => setWidth(parseInt(e.target.value))}
placeholder="Width" required/>
You don't use useState like that. useState returns an array of two things:
The variable you are going to use
A function which is used to change that variable
So in you case it should look like this:
const [widthProperties, setWidthProperties] = useState({}); //Here you can either pass an empty object as an initial value or any structutre you would like.
setWidthProperties(props.pck.width); //Or whatever you want to set it to.
Remember never to change the variable manually. Do it only through the function useState gives you.

use object in useEffect 2nd param without having to stringify it to JSON

In JS two objects are not equals.
const a = {}, b = {};
console.log(a === b);
So I can't use an object in useEffect (React hooks) as a second parameter since it will always be considered as false (so it will re-render):
function MyComponent() {
// ...
useEffect(() => {
// do something
}, [myObject]) // <- this is the object that can change.
}
Doing this (code above), results in running effect everytime the component re-render, because object is considered not equal each time.
I can "hack" this by passing the object as a JSON stringified value, but it's a bit dirty IMO:
function MyComponent() {
// ...
useEffect(() => {
// do something
}, [JSON.stringify(myObject)]) // <- yuck
Is there a better way to do this and avoid unwanted calls of the effect?
Side note: the object has nested properties. The effects has to run on every change inside this object.
You could create a custom hook that keeps track of the previous dependency array in a ref and compares the objects with e.g. Lodash isEqual and only runs the provided function if they are not equal.
Example
const { useState, useEffect, useRef } = React;
const { isEqual } = _;
function useDeepEffect(fn, deps) {
const isFirst = useRef(true);
const prevDeps = useRef(deps);
useEffect(() => {
const isFirstEffect = isFirst.current;
const isSame = prevDeps.current.every((obj, index) =>
isEqual(obj, deps[index])
);
isFirst.current = false;
prevDeps.current = deps;
if (isFirstEffect || !isSame) {
return fn();
}
}, deps);
}
function App() {
const [state, setState] = useState({ foo: "foo" });
useEffect(() => {
setTimeout(() => setState({ foo: "foo" }), 1000);
setTimeout(() => setState({ foo: "bar" }), 2000);
}, []);
useDeepEffect(() => {
console.log("State changed!");
}, [state]);
return <div>{JSON.stringify(state)}</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
The above answer by #Tholle is absolutely correct. I wrote a post regarding the same on dev.to
In React, side effects can be handled in functional components using useEffect hook. In this post, I'm going to talk about the dependency array which holds our props/state and specifically what happens in case there's an object in the dependency array.
The useEffect hook runs even if one element in the dependency array changes. React does this for optimisation purposes. On the other hand, if you pass an empty array then it never re-runs.
However, things become complicated if an object is present in this array. Then even if the object is modified, the hook won't re-run because it doesn't do deep object comparison between these dependency changes for that object. There are couple of ways to solve this problem.
Use lodash's isEqual method and usePrevious hook. This hook internally uses a ref object that holds a mutable current property that can hold values.
It’s possible that in the future React will provide a usePrevious Hook out of the box since it is a relatively common use case.
const prevDeeplyNestedObject = usePrevious(deeplyNestedObject)
useEffect(()=>{
if (
!_.isEqual(
prevDeeplyNestedObject,
deeplyNestedObject,
)
) {
// ...execute your code
}
},[deeplyNestedObject, prevDeeplyNestedObject])
Use useDeepCompareEffect hook as a drop-in replacement for useEffect hook for objects
import useDeepCompareEffect from 'use-deep-compare-effect'
...
useDeepCompareEffect(()=>{
// ...execute your code
}, [deeplyNestedObject])
Use useCustomCompareEffect hook which is similar to solution #2
I prepared a CodeSandbox example related to this post. Fork it and check it yourself.
Your best bet is to use useDeepCompareEffect from react-use. It's a drop-in replacement for useEffect.
const {useDeepCompareEffect} from "react-use";
const App = () => {
useDeepCompareEffect(() => {
// ...
}, [someObject]);
return (<>...</>);
};
export default App;
Plain (not nested) object in dependency array
I just want to challenge these two answers and to ask what happen if object in dependency array is not nested. If that is plain object without properties deeper then one level.
In my opinion in that case, useEffect functionality works without any additional checks.
I just want to write this, to learn and to explain better to myself if I'm wrong. Any suggestions, explanation is very welcome.
Here is maybe easier to check and play with example: https://codesandbox.io/s/usehooks-bt9j5?file=/src/App.js
const {useState, useEffect} = React;
function ChildApp({ person }) {
useEffect(() => {
console.log("useEffect ");
}, [person]);
console.log("Child");
return (
<div>
<hr />
<h2>Inside child</h2>
<div>{person.name}</div>
<div>{person.age}</div>
</div>
);
}
function App() {
const [person, setPerson] = useState({ name: "Bobi", age: 29 });
const [car, setCar] = useState("Volvo");
function handleChange(e) {
const variable = e.target.name;
setPerson({ ...person, [variable]: e.target.value });
}
function handleCarChange(e) {
setCar(e.target.value);
}
return (
<div className="App">
Name:
<input
name="name"
onChange={(e) => handleChange(e)}
value={person.name}
/>
<br />
Age:
<input name="age" onChange={(e) => handleChange(e)} value={person.age} />
<br />
Car: <input name="car" onChange={(e) => handleCarChange(e)} value={car} />
<ChildApp person={person} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-
dom.development.js"></script>
<div id="root"></div>
You can just expand the properties in the useEffect array:
var obj = {a: 1, b: 2};
useEffect(
() => {
//do something when any property inside "a" changes
},
Object.entries(obj).flat()
);
Object.entries(obj) returns an array of pairs ([["a", 1], ["b", 2]]) and .flat() flattens the array into:
["a", 1, "b", 2]
Note that the number of properties in the object must remain constant because the length of the array cannot change or else useEffect will throw an error.

Categories

Resources