react-hooks/exhaustive-deps causing dependency warning, fix hangs code - javascript

Im working on a project and have incorporated Hooks for the first time. When using the useEffects and useState hooks, Im encountering a wierd warning from eslint.
My Code:
import React, { useState, useEffect } from 'react';
import { Card } from 'antd';
import Search from 'antd/lib/input/Search';
import { IPatient } from 'types/IPatient';
const SearchBox = () => {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<IPatient[]>([]);
const handleChange = (event: any) => {
setSearchTerm(event.target.value);
};
const cards: IPatient[] = [
{
id: 1,
name: 'Erling',
description: ['tall', 'male', 'sick'],
age: 98,
isHuman: true,
},
// other data...
];
useEffect(() => {
const results: IPatient[] = cards.filter((card) =>
card.name.toLowerCase().includes(searchTerm),
);
setSearchResults(results);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchTerm]);
return (
<>
<div className="searchbox">
<Search onChange={handleChange} />
</div>
<div>
{searchResults.map((data) => (
<Card key={data.id} hoverable>
<h1>{data.name}</h1>
<p>Patient ID: {data.id} </p>
<p>Age: {data.age} years old.</p>
<p>
Description:{' '}
{data.description[0] +
' ' +
data.description[1] +
' ' +
data.description[2]}
</p>
</Card>
))}
</div>
</>
);
};
export default SearchBox;
Now, the issue is that eslint is calling an error on my dependency array, and if I put both variables (cards and searchTerms) inside the array, it results in the code hanging and the webapp crashing. The eslint-line is currently in place to suppress the warning, but this is less than ideal.
So I guess my question is how to circumvent this. I am sure this a Beginners mistake, as it is my first time with Hooks. Any help would be appreciated!

The problem when adding cards to the dependency array is that you are creating a new reference of cards array on each rerender and hence the useEffect runs again, causing an infinite loop
Since Card array seems to be const you can take it out of functional component and then add it to dependency array
const cards: IPatient[] = [
{
id: 1,
name: 'Erling',
description: ['tall', 'male', 'sick'],
age: 98,
isHuman: true,
},
// other data...
];
const SearchBox = () => {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<IPatient[]>([]);
const handleChange = (event: any) => {
setSearchTerm(event.target.value);
};
useEffect(() => {
const results: IPatient[] = cards.filter((card) =>
card.name.toLowerCase().includes(searchTerm),
);
setSearchResults(results);
}, [searchTerm, cards]);
...

Related

props getting added twice to the array variable that is declared outside the component in React / Javascript

I am trying to add, new data that user enters, to the array declared outside the component named DUMMY_MEALS and then render it as a list.
The problem here is the 'data' which is an object adds twice or more to the DUMMY_MEALS and renders twice in the page. Why is this happening?
The component with issue
"use strict";
import React from "react";
let DUMMY_MEALS = [
{id: "m1", name: "Sushi"},
];
const MealList = ({data}) => {
//const data = {id: "m5", name: "pushi"}
let mealslist = [];
DUMMY_MEALS = [data, ...DUMMY_MEALS];
mealslist = DUMMY_MEALS.map((meal) => <li>{meal.name}</li>);
return <ul>{mealslist}</ul>;
};
export default MealList;
Parent component
const Header = () => {
const [data, setData] = useState({});
const sendInputData = (inputData) => {
setData(inputData);
}
return (
<>
<MealsList data={data}/>
<MealForm getInputData={sendInputData}/>
</>
);
};
export default Header;
Sibling Component
const MealForm = (props) => {
const [name, setName] =useState("");
const formSubmitHandler = (e) => {
e.preventDefault();
let inputData = {
key : Math.random(),
name : name,
}
props.getInputData(inputData);
inputData = {};
}
return (
<form onSubmit={formSubmitHandler}>
<label htmlFor="name">name</label>
<input type="text" id="name" value={name} onChange={(e)=>setName(e.target.value)}></input>
<button>Submit</button>
</form>
);
};
export default MealForm;
You should use useState hook instead of let mealslist = []; Inside your MealList component.
And don't use DUMMY_MEALS as the component state. use useEffect hook to add the new meal to the state just once.
Check out this tested code CodeSandbox
MealList component changed as follow :
const MealList = ({ data }) => {
const [mealslist, setMealList] = useState([]);
useEffect(() => {
if (data)
setMealList([data, ...DUMMY_MEALS]);
}, []);
return <ul>{
mealslist.map((meal)=>{ <ListRender meal={meal} />})
}
</ul>;
};
And here is your App component:
const data = {
id: "k123",
name: "Falafel",
description: "An Iranian food.",
price: 16.5
};
export default function App() {
return (
<MealList data={data} />
);
}

Display data from API using react component and useEffect

I have this react component which every time it's rendered show the country information that receives via props (country) and using the weather stack API must show also the capital weather at the current time. The first part (displaying the country data that comes from the props) works fine but I'm struggling to get the data from the weather API. I see on console that I'm getting the current weather but can't assign it to weather variable using setState() therefore my app crashes.
This is the component code I have so far, I've tried using async/await and .then synataxis in case I was mispelling something but always I get the same result:
const CountryDetails = async ({country}) => {
const [weather, setWeather] = useState({});
// const hook = async () => {
// const result = await axios.get(`http://api.weatherstack.com/current?access_key=${WEATHER_API}&query=${country.capital}`);
// console.log(result.data);
// setWeather(result.data.current);
// console.log(weather);
// }
const hook = () => {
axios.get(`http://api.weatherstack.com/current?access_key=${WEATHER_API}&query=${country.capital}`).then((response) => {
console.log('then');
setWeather({
temperature: response.data.current.temperature,
img: response.data.current.weather_icons,
wind: response.data.current.wind_speed,
dir: response.data.current.wind_direction
});
console.log(response.data.current);
});
}
useEffect(hook, []);
console.log(weather);
return (
<>
<h1>{country.name}</h1>
<p>capital {country.capital}</p>
<p>population {country.population}</p>
<h2>languages</h2>
<ul>
{country.languages.map((lang) => {
<li key={lang.name}>{lang.name}</li>;
})}
</ul>
<img src={country.flag}></img>
<h2>Weather in {country.capital}</h2>
<p><b>temperature: </b>{weather.current.temperature}</p>
<img src={weather.current.weather_icons} />
<p><b>wind: </b>{weather.current.wind_speed} direction {weather.current.wind_direction}</p>
</>
);
};
sandbox with the whole code: https://codesandbox.io/s/vigilant-ride-h3t1j
Here is a codesandbox I created playing around with your code. Since you stated that you're receiving the data from the API successfully, I'm mocking that with my getWeather function. In addition to what #Viet answered, there were other issues in the code you provided. See if this helps or if the error still persists, please provide with a reproduced example of the snippet:
https://codesandbox.io/s/competent-dhawan-fds81?file=/src/App.js:52-62
import { useEffect, useState } from "react";
const getWeather = (country) => {
return Promise.resolve({
data: {
current: {
temperature: "<temperature>",
weather_icons: "<weather_icons>",
wind_speed: "<wind_speed>",
dir: "<wind_direction>"
}
}
});
};
const CountryDetails = ({ country }) => {
const [weather, setWeather] = useState({});
const hook = () => {
getWeather(country).then((response) => {
console.log("then");
setWeather({
temperature: response.data.current.temperature,
img: response.data.current.weather_icons,
wind: response.data.current.wind_speed,
dir: response.data.current.dir,
wind_speed: response.data.current.wind_speed
});
console.log(response.data.current);
});
};
useEffect(hook, [country]);
// You should get {} logged here, not undefined
console.log(weather);
return (
<>
<h1>{country.name}</h1>
<p>Capital: {country.capital}</p>
<p>Population: {country.population}</p>
<h2>Languages</h2>
<ul>
{/* You were not returning anything in the callback of the map function */}
{country.languages.map((lang, i) => (
<li key={i}>{lang.name}</li>
))}
</ul>
<img src={country.flag}></img>
<h2>Weather in {country.capital}</h2>
<p>
<b>temperature: </b>
{/* As #Veit mentioned, you were accessing the wrong property */}
{weather.temperature}
</p>
<img src={weather.weather_icons} />
<p>
<b>Wind: </b>
{weather.wind_speed} Direction: {weather.dir}
</p>
</>
);
};
export default (props) => {
const country = {
languages: [{ name: "<name>" }],
flag: "<flag name>",
capital: "<capital name>",
name: "<Coutry Name>",
population: "<POPULATION>"
};
return <CountryDetails country={country} />;
};
You are just extracting wrong properties from weather state. This works:
import axios from "axios";
import { useState, useEffect } from "react";
const WEATHER_API = "xxx";
const CountryDetails = ({ country }) => {
const [weather, setWeather] = useState({});
const hook = () => {
axios
.get(
`http://api.weatherstack.com/current?access_key=${WEATHER_API}&query=${country.capital}`
)
.then((response) => {
console.log("then", response);
setWeather({
temperature: response.data.current.temperature,
img: response.data.current.weather_icons,
wind: response.data.current.wind_speed,
dir: response.data.current.wind_dir
});
console.log(JSON.stringify(weather));
});
};
useEffect(hook, []);
console.log(weather);
return (
<>
<h2>languages</h2>
<p><b>temperature: </b>{weather.temperature}</p>
<p>
<b>wind: </b>
{weather.wind} direction {weather.dir}
</p>
</>
);
};
export default function App() {
return (
<div className="App">
<CountryDetails country={{ capital: "London" }} />
</div>
);
}

How to use useState hook to map JSON response from API

My API returns complex json like these.
[
{id: 1, pub_date: "2021-01-06T20:24:57.547721Z"},
{id: 2, pub_date: "2021-01-06T20:24:57.547721Z"},
{id: 3, pub_date: "2021-01-06T20:24:57.547721Z"}
]
So my trial is like this
const [result, setResult] = useState({});
const [result, setResult] = useState(null);
const [result, setResult] = useState([]);
useEffect(() => {
axios.get('http://localhost:8000/api/results/')
.then(res=>{
console.log(res.data); // correctly received
setResult(res.data); // error
console.log(result); // nothing appears
})
.catch(err=>{console.log(err);});
}, []);
However for any try, it shows the error like
Error: Objects are not valid as a React child (found: object with keys {id, pub_date}). If you meant to render a collection of children, use an array instead.
I have some trial and errors.
There is still difficult behaiver to understand.
const [cnt,setCnt] = useState(0);
useEffect(() => {
axios.get('http://localhost:8000/api/results/')
.then((res)=> {
setCnt(2);
console.log(cnt);//shows 0
})
.catch(err=>{console.log(err);});
}, []);
why setCnt is not workd?? I am more confused......
This error comes from your JSX render, where you're certainly trying to display directly your datas from API
useEffect(...)
...
return (
<ul>
{
result.map(r => (
<li key={r.id}>{r.id} - {r.pub_date}</li>
))
}
</ul>
)
If you are calling setResult(res.data), then your result state should be of type [].
import React, { useEffect, useState } from "react";
const fetchData = () =>
Promise.resolve({
data: [
{ id: 1, pub_date: "2021-01-06T20:24:57.547721Z" },
{ id: 2, pub_date: "2021-01-06T20:24:57.547721Z" },
{ id: 3, pub_date: "2021-01-06T20:24:57.547721Z" }
]
});
const ListItem = ({ id, pub_date }) => (
<li>
{id} — {pub_date}
</li>
);
const ListItems = ({ items }) => (
<ul>
{items.map((props) => (
<ListItem key={props.id} {...props} />
))}
</ul>
);
const App = () => {
const [result, setResult] = useState([]);
useEffect(() => {
fetchData().then((res) => {
setResult(res.data);
});
}, []);
return (
<div className="App">
<ListItems items={result} />
</div>
);
};
export default App;

"TypeError: props.todos.map is not a function" can't figure out what the cause is?

I am trying to create my first Todo list with React.js. I am trying to change the state from
const [todos, setTodos] = useState([])
To:
const [todos, setTodos] = useState({
todo: [],
isCompleted: false,
})
Just to try and add in a isCompleted state. However, when I change it, I get an error when running my application from a previously working map. The error is in the title.
Could somebody tell me what is wrong?
Code:
TodosApp.js
import React, { useState } from "react"
import Todos from "./Todos"
const TodoApp = () => {
const [todos, setTodos] = useState({
todo: [],
isCompleted: false,
})
const [input, setInput] = useState("")
const handleCurrentInput = (e) => {
setInput(e.target.value)
}
const handleSubmit = (e) => {
e.preventDefault()
console.log(input)
setInput("")
setTodos({
...todos,
task: input,
isCompleted: false,
})
}
const handleDelete = ({ index }) => {
const newTodos = [...todos]
newTodos.splice(index, 1)
setTodos(newTodos)
}
return (
<div id="todoForm">
<div class="container">
<div class="todo_form">
<div class="todo_input">
<form onSubmit={handleSubmit}>
<input
type="text"
id="input_todo"
onChange={handleCurrentInput}
value={input}
/>
</form>
<Todos todos={todos} handleDelete={handleDelete} />
</div>
</div>
</div>
</div>
)
}
export default TodoApp
Todos.js
import React, { useState } from "react"
const Todos = (props) => {
return (
<ul>
{props.todos.map((todo, index) => {
return (
<li key={todo}>
{todo}
<button onClick={() => props.handleDelete({ index })}>
Delete
</button>
</li>
)
})}
</ul>
)
}
export default Todos
You need to focus on each todo item including 2 props task, isCompleted instead of isCompleted of todos.
const [todos, setTodos] = useState([]);
var newTodo = {
task: 'React JS',
isCompleted: false
};
setTodos([...todos, newTodo]);
Then your todos's structure like below:
[
{
task: 'Study React JS',
isCompleted: false
},
{
task: 'Study React Redux',
isCompleted: false
},
];
Your state is an object containing an array of todos. This is what you're passing to your Todos component.
So you have two options:
Either pass todos.todos as a prop or
(Better way) Rethink your state. isCompleted seems that it should be part of each todo, because each todo should be completed not the list itself. A list is completed if every todo isCompleted
So your state would be const [todos, setTodos] = useState([])
I hope it's clear what I mean. Typing this from the phone is not so easy :-)
It's because you don't set to state the right way, todos got overwritten with the wrong value. You should write:
// handleSubmit
setTodos(s => {
...s,
task: input,
isCompleted: false,
});
and
// handleDelete
const newTodos = [...todos]
newTodos.splice(index, 1)
setTodos(s => ({ ...s, todos: newTodos }))
Working App: Stackblitz
import React, { useState, useEffect } from "react";
const TodoApp = () => {
/* initialize todos with array
instead of an object 👇*/
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
const handleCurrentInput = e => {
setInput(e.target.value);
};
const handleSubmit = e => {
e.preventDefault();
console.log(input);
/* update the state by appending an object having
key todo and isCompleted to copy of our main state,
todos.👇
*/
setTodos([...todos, { todo: input, isCompleted: false }]);
setInput("");
};
const handleDelete = ({ index }) => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
useEffect(() => {
console.log(JSON.stringify(todos));
}, [todos]);
return (
<div id="todoForm">
<div class="container">
<div class="todo_form">
<div class="todo_input">
<form onSubmit={handleSubmit}>
<input
type="text"
id="input_todo"
onChange={handleCurrentInput}
value={input}
/>
</form>
<Todos todos={todos} handleDelete={handleDelete} />
</div>
</div>
</div>
</div>
);
};
export default TodoApp;
const Todos = props => {
return (
<>
<ul>
{props.todos.map((todo, index) => {
return (
<li key={index}>
{/**use null propogation to avoid accessing the null todo value which will not exist in first render. */}
{todo?.todo}
<button onClick={() => props.handleDelete({ index })}>
Delete
</button>
</li>
);
})}
</ul>
</>
);
};
isCompleted should be associated with each todo item.
So, you should use todos as array and store objects within that array. Each object will have isCompleted and a task property along with a unique Id as well.
const [todos, setTodos] = useState([]);
And your submit input would look like:
const handleSubmit = (e) => {
e.preventDefault();
const todo = {
task: input,
id: new Date().getTime().toString(),
isCompleted: false
};
const updatedTodos = [...todos, todo];
setTodos(updatedTodos);
console.log(updatedTodos);
setInput("");
};
Note: To generate unique Ids you can use uuid library. I have generated unique ids here using id: new Date().getTime().toString().
FULL WORKING CODE SANDBOX LINK: https://codesandbox.io/s/todosissue-2mc26?file=/src/TodoApp.js
Have modified handleDelete function as well :)

State is not updated properly when using React effects

I'm working on a small todo app as an exercise using React. I have a mock service like this:
export default class TodoService {
constructor(todos) {
this.todos = new Map();
todos.forEach(todo => {
this.todos.set(todo.id, todo);
});
}
findAll() {
return Array.from(this.todos.values());
}
saveTodo(todo) {
this.todos[todo.id] = todo
}
completeTodo(todo) {
this.todos.delete(todo.id)
}
}
and in my React app I have some state which contains the todos:
const [todos, setTodos] = useState([]);
const [flip, flipper] = useState(true);
const completeTodo = (todo) => {
todoService.completeTodo(todo);
flipper(!flip);
}
useEffect(() => {
setTodos(todoService.findAll());
}, [flip])
completeTodo is a function which I pass into my Todo component to be used when I want to complete a todo like this:
import React from "react";
const Todo = ({ todo, completeFn }) => {
return (
<form className="todo">
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
value=""
name={todo.id}
id={todo.id}
onClick={() => {
console.log(`completing todo...`)
completeFn(todo)
}} />
<label className="form-check-label" htmlFor={todo.id}>
{todo.description}
</label>
</div>
</form>
)
}
export default Todo
So what happens is that whenever the user clicks the checkbox completeFn is called with the todo, it gets deleted from the service object and the state is supposed to update, but the weirdest thing happens.
When TodoService.completeTodo() is called the todo gets deleted properly, but when findAll() is called the old todo is still there! If I write the contents to the console I can see the item being deleted then somehow teleported back when I call findAll. Why does this happen? I this because of some React magic I don't understand?
Edit: What's even more insane is that if I modify this to only use effects for the initial loading like this:
const [todos, setTodos] = useState([]);
const completeTodo = (todo) => {
todoService.completeTodo(todo);
setTodos(todoService.findAll());
}
useEffect(() => {
setTodos(todoService.findAll());
}, [])
I get a super weird result:
Can someone explain this to me?
Edit2: This is a complete reproducible example (without the index.html with a <div id="root"></div> in it).
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const Todo = ({ todo, completeFn }) => {
return (
<div>
<input
type="checkbox"
name={todo.id}
id={todo.id}
onClick={() => {
console.log(`completing todo...`)
completeFn(todo)
}} />
<label className="form-check-label" htmlFor={todo.id}>
{todo.description}
</label>
</div>
)
}
class TodoService {
constructor(todos) {
this.todos = new Map();
todos.forEach(todo => {
this.todos.set(todo.id, todo);
});
}
findAll() {
return Array.from(this.todos.values());
}
saveTodo(todo) {
this.todos[todo.id] = todo
}
completeTodo(todo) {
this.todos.delete(todo.id)
}
}
const App = () => {
let todoService = new TodoService([{
id: 1,
description: "Let's go home."
}, {
id: 2,
description: "Take down the trash"
}, {
id: 3,
description: "Play games"
}]);
const [todos, setTodos] = useState([]);
const [flip, flipper] = useState(true);
const completeTodo = (todo) => {
todoService.completeTodo(todo);
flipper(!flip);
}
useEffect(() => {
setTodos(todoService.findAll());
}, [flip])
return (
<div>
{todos.map(todo => <Todo key={todo.id} todo={todo} completeFn={completeTodo} />)}
</div>
)
};
ReactDOM.render(<App />, document.getElementById("root"));
You don't need to call useEffectin this scenario. You've put a dependency in the useEffect which is fine to use it to stop infinite loop. but it's unnecessary here. You're not really doing any fetch
You can update your code to be like this.
import React, { useState, useCallback, useEffect } from "react";
const Todo = ({ todo, completeFn }) => {
const handleOnclick = useCallback(() => {
// useCallback since function is passed down from parent
console.log(`completing todo...`);
completeFn(todo);
}, [completeFn, todo]);
return (
<div>
<input
type="checkbox"
name={todo.id}
id={todo.id}
onClick={handleOnclick}
/>
<label className="form-check-label" htmlFor={todo.id}>
{todo.description}
</label>
</div>
);
};
class TodoService {
constructor(todos) {
this.todos = new Map();
todos.forEach(todo => {
this.todos.set(todo.id, todo);
});
}
findAll() {
console.log(Array.from(this.todos.values()));
return Array.from(this.todos.values());
}
saveTodo(todo) {
this.todos[todo.id] = todo;
}
completeTodo(todo) {
this.todos.delete(todo.id);
}
}
const todoService = new TodoService([
{
id: 1,
description: "Let's go home."
},
{
id: 2,
description: "Take down the trash"
},
{
id: 3,
description: "Play games"
}
]);
export default function App() {
const [todos, setTodos] = useState([]); // Set initial state
const completeTodo = todo => {
todoService.completeTodo(todo);
setTodos(todoService.findAll()); // Update state
};
useEffect(() => {
setTodos(todoService.findAll());
}, []); // Get and update from service on first render only
return (
<div>
{todos.map(todo => (
<Todo key={todo.id} todo={todo} completeFn={completeTodo} />
))}
</div>
);
}
working example
https://codesandbox.io/s/cranky-hertz-sewc5?file=/src/App.js

Categories