React Hook useEffect has a missing dependency: 'getContacts' - javascript

Before posting the below, I have reviewed similar posts on stackoverflow but none resolved my issue.
I'm new to react and fetching data from firestore database. The below code works as required but getting this prompt within react
import React, {useState, useEffect} from 'react'
import {db} from '../firebase'
const ListRecord = () => {
const [details, setDetails] = useState([]);
useEffect(() => {
getContacts()
},[]);
const getContacts = async() => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({id: doc.id, value: doc.data()})
});
setDetails(arr);
});
console.log(details);
return details
}
return (
<div>
<h2>List Contact Details</h2>
</div>
)
}
export default ListRecord
As per other similar posts I tried moving the getContacts function inside useEffect body which make the prompt disapper but the getContacts function goes in a continuous loop.
I'm not sure what I'm missing here and any help would be appreciated.

There are different potential solutions:
1. Move getContacts() inside the useEffect() hook:
If you call getContacts() only once and only when the component mounts for the first time, this is probably the most logic solution.
useEffect(() => {
const getContacts = async () => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
setDetails(arr);
});
//console.log(details);
//return details // why are you returning details?
}
getContacts()
}, [setDetails]); // setDetails() is granted to never change therefore the hook will never re-run
or, of course, you can use an IIFE:
useEffect(() => {
(async function() {
// ... same body as getContacts
})()
}, [setDetails])
2. Use a useCallback() hook:
This is something you might want to do if getContacts() is called more than once (for example, when the component mounts and every time some prop changes or when you click on some button)
const getContacts = useCallback(async () => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
setDetails(arr);
});
//console.log(details);
//return details // why are you returning details?
}, [setDetail]); // setDetails() is granted to never change therefore getContacts() will never be re-created
useEffect(() => {
getContacts()
}, [getContacts]); // as getContacts() never changes, this will run only once
3. Move getContacts() out of the component and make it an independent function:
This can make sense if you want to reuse the same logic into other components:
// getContacts.js file
// OR this code could be in the ListRecord.js file but **outside** the component,
// although, in this case, solutions (1) or (2) would make more sense
import { db } from 'path/to/firebase'
export async function getContacts() {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
return arr; // this time you HAVE TO return arr
});
}
// ListRecord.js file
import React, { useState, useEffect } from 'react';
import { getContacts } from 'path/to/getContacts.js';
const ListRecord = () => {
const [details, setDetails] = useState([]);
useEffect(async () => {
const arr = await getContacts();
if (arr && arr.length > 0) setDetails(arr);
}, [setDetails]);
//...
}

I suggest you have a look at how useEffect and its dependency list works in the official document.
In short, do the following:
useEffect(() => {
getContacts()
}, [getContacts]);
This means when getContacts changes, the useEffect will be re-run.

Related

React: ES2020 dynamic import in useEffect blocks rendering

From what I understand, async code inside useEffect runs without blocking the rendering process. So if I have a component like this:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
const data = await response.json();
console.log("end");
};
getData();
}, []);
return null;
};
The console output is (in order):
begin
window loaded
end
However if I use ES2020 dynamic import inside the useEffect:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const data = await import("./data.json");
console.log("end");
};
getData();
}, []);
return null;
};
The output is (in order):
begin
end
window loaded
This is a problem because when data.json is very large, the browser hangs while importing the large file before React renders anything.
All the necessary and useful information is in Terry's comments. here is the implementation of what you want according to the comments:
First goal:
I would like to import the data after window has loaded for SEO reasons.
Second goal:
In my case the file I'm trying to dynamically import is actually a function that requires a large dataset. And I want to run this function whenever some state has changed so that's why I put it in a useEffect hook.
Let's do it
You can create a function to get the data as you created it as getData with useCallback hook to use it anywhere without issue.
import React, {useEffect, useState, useCallback} from 'react';
function App() {
const [data, setData] = useState({});
const [counter, setCounter] = useState(0);
const getData = useCallback(async () => {
try {
const result = await import('./data.json');
setData(result);
} catch (error) {
// do a proper action for failure case
console.log(error);
}
}, []);
useEffect(() => {
window.addEventListener('load', () => {
getData().then(() => console.log('data loaded successfully'));
});
return () => {
window.removeEventListener('load', () => {
console.log('page unmounted');
});
};
}, [getData]);
useEffect(() => {
if (counter) {
getData().then(() => console.log('re load data after the counter state get change'));
}
}, [getData, counter]);
return (
<div>
<button onClick={() => setCounter((prevState) => prevState + 1)}>Increase Counter</button>
</div>
);
}
export default App;
Explanation:
With component did mount, the event listener will load the data.json on 'load' event. So far, the first goal has been met.
I use a sample and simple counter to demonstrate change in the state and reload data.json scenario. Now, with every change on counter value, the getData function will call because the second useEffect has the counter in its array of dependencies. Now the second goal has been met.
Note: The if block in the second useEffect prevent the getData calling after the component did mount and first invoking of useEffect
Note: Don't forget to use the catch block for failure cases.
Note: I set the result of getData to the data state, you might do a different approach for this result, but the other logics are the same.

how to get check setState function callled or not in react js using hooks?

I am trying to test custom hook. I want to know is setState function fire or not.
here is my custom hook
import React from "react";
import axios from "axios";
export default () => {
const [state, setState] = React.useState([]);
const fetchData = async () => {
const res = await axios.get("https://5os4e.csb.app/data.json");
setState(res.data);
};
React.useEffect(() => {
(async () => {
await fetchData();
})();
}, []);
return { state };
};
now I am trying to test this custom hook. I want to know is setState function fire or not .
I tried like this
import moxios from "moxios";
import React from "react";
import { act, renderHook, cleanup } from "#testing-library/react-hooks";
import useTabData from "./useTabData";
describe("use tab data", () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
describe("non-error response", () => {
// create mocks for callback arg
const data = [
{
name: "hello"
}
];
let mockSetCurrentGuess = jest.fn();
beforeEach(async () => {
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: data
});
});
});
test("calls setState with data", async () => {
React.useState = jest.fn(() => ["", mockSetCurrentGuess]);
const { result, waitForNextUpdate } = renderHook(() => useTabData());
console.log(result);
//expect(mockSetCurrentGuess).toHaveBeenCalledWith(data);
});
});
});
You should not mock the React internals. This is incorrect. Either ways, this code has no effect in mocking due to closures. Even if it worked, no point in testing if you are mocking the real implementation, isn't it ? :)
I would recommend to try to get grasp of what react hook is doing in your code.
You have a state in your custom hook:
const [state, setState] = React.useState([]);
.
.
return [state]; //you are returning the state as ARRAY
#testing-library/react-hooks allows you to debug and get value of current outcome of hook.
const { result, waitForNextUpdate } = renderHook(() => useTabData());
const [foo] = result.current; // the array you returned in hook
expect(foo).toEqual('bar'); //example assertion
I would stop here and allow you to learn and debug.

How to use if/else inside useEffect() while dispatching an action in react.js

My redux related imports are as follows -
source: https://github.com/theairbend3r/pokedex
import { useDispatch, useSelector } from "react-redux"
import {
fetchPokemonNameUrl,
NUMBER_OF_POKEMON,
selectorPokemon,
} from "./pokemonCardsSlice"
const dispatch = useDispatch()
const pokemonList = useSelector(selectorPokemon)
I have a useEffect block as follows -
useEffect(() => {
return dispatch(fetchPokemonNameUrl())
}, [dispatch])
What I want to do -
useEffect(() => {
if (pokemonList.length !== NUMBER_OF_POKEMON) {
return dispatch(fetchPokemonNameUrl())
}
}, [dispatch])
But when I do this, I get a warning -
React Hook useEffect has a missing dependency: 'pokemonList.length'. Either include it or remove the dependency array.eslint(react-hooks/exhaustive-deps)
What am I doing wrong?
Add pokemonList to the dependency array as suggested, your callback depends on the value of pokemonList (.length) which may change.
When pokemonList will be updated, the callback will run again with the updated length.
Also, you don't need to return from useEffect as it is for cleaning up an effect.
useEffect(() => {
if (pokemonList.length !== NUMBER_OF_POKEMON) {
dispatch(fetchPokemonNameUrl());
}
}, [dispatch,pokemonList]);
Edit: Seems like fetchPokemonNameUrl implemented as middleware, you can rewrite as something like:
const fetchPokemonNameUrl = async (dispatch) => {
const response = await axios.get(URL);
const data = response.data.results;
data.map(async (poke) => {
const responseDetails = await axios.get(poke.url);
let tempDetails = {
name: responseDetails.data.species.name,
baseExperience: responseDetails.data.base_experience,
height: responseDetails.data.height,
weight: responseDetails.data.weight,
type: responseDetails.data.types[0].type.name,
sprites: responseDetails.data.sprites.front_default,
};
dispatch(getData(tempDetails));
});
};
// And call it:
useEffect(() => {
if (pokemonList.length !== NUMBER_OF_POKEMON) {
fetchPokemonNameUrl(dispatch);
}
}, [dispatch,pokemonList]);

How to use setTimeout() along with React Hooks useEffect and setState?

I want to wait for 10 seconds for my API call to fetch the category list array from backend and store in hook state. If nothing is fetched in 10 sec, I want to set error hook state to true.
But the problem is even after the array is fetched initially, the error state is set to true and categoriesList array in state blanks out after 10 sec.
import React, { useState, useEffect } from "react";
import { doGetAllCategories } from "../helper/adminapicall.js";
const ViewCategories = () => {
let [values, setValues] = useState({
categoriesList: "",
error: false,
});
let { categoriesList, error } = values;
const preloadCategories = () => {
doGetAllCategories()
.then((data) => {
if (data.error) {
return console.log("from preload call data - ", data.error);
}
setValues({ ...values, categoriesList: data.categories });
})
.catch((err) => {
console.log("from preload - ", err);
});
};
useEffect(() => {
preloadCategories();
let timerFunc = setTimeout(() => {
if (!categoriesList && !error) {
setValues({
...values,
error: "Error fetching category list... try after some time !",
});
}
}, 10000);
return () => {
clearTimeout(timerFunc);
};
}, []);
//...further code
The problem is that the useEffect callback is a closure over categoriesList, so you'll always see the initial categories list inside the callback, and you won't see any changes to it. Now one could add categoriesList as a dependency to the useEffect hook, that way the hook will be recreated on every categoriesList change, and thus you can see the changed version:
useEffect(/*...*/, [categoriesList]);
Now the good thing is, that by updating the hook the timeout also gets canceled, so if the category list is set, we just don't have to create a new timeout:
useEffect(() => {
if(!categoriesList && !error) {
let timerFunc = setTimeout(() => {
setValues({
...values,
error: "Error fetching category list... try after some time !",
});
}, 10000);
return () => clearTimeout(timerFunc);
}
}, [!categoriesList, !error]); // some minor optimization, changes to the list don't bother this hook
I recommend you to read this blog post on useEffect by Dan Abramov.
The problem with your code is that you expect to have a change of the state of the component inside the useEffect hook. Instead, you create two variables inside the useEffect that track whether the limit of 10 sec has passed or the data is fetched. In contrary to state variables, you can expect these variables to change because they lie within the same useEffect.
export default function App() {
const [data, setData] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
let didCancel = false;
let finished = false;
async function fetchData() {
const data = await subscribeAPI();
if (!didCancel) {
finished = true;
setData(data);
}
}
const id = setTimeout(() => {
didCancel = true;
if (!finished) {
setError("Errorrrr");
}
}, 10000);
fetchData();
return () => {
clearTimeout(id);
};
}, []);

Data initialization in useEffect triggers multiple requests

This is a follow-up question to this one.
Simply, I'm fetching the same date in two different ways. One by one and all together for updates. I have a simple store with context and useReducer.
My current code is like that:
import React, { useEffect } from "react";
import axios from "axios";
import { useGlobalState } from "./state";
const arr = Array.from(Array(100), (x, i) => i + 1);
function App() {
const [{ posts, init }, dispatch] = useGlobalState();
useEffect(() => {
const getInc = () => {
arr.forEach(async id => {
const res = await axios(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
dispatch({
type: "INC",
payload: res.data
});
});
};
const getAll = async () => {
const promises = arr.map(id =>
axios(`https://jsonplaceholder.typicode.com/posts/${id}`)
);
const res = await Promise.all(promises);
dispatch({
type: "ALL",
payload: res.map(el => el.data)
});
};
if (init) {
getInc();
} else {
getAll();
}
setInterval(() => getAll(), 10000);
}, [dispatch, init]);
return (
<>
<div>{posts.length}</div>
</>
);
}
export default App;
In every interval getAll is triggered twice.Here is a working sandbox.
I added a console.log to reducer part so you can see it runs twice. I can also see it in the network tab.
Try Separate Concerns when using useEffect, like you mentioned "One by one and all together for updates".
function App() {
const [{ init, posts }, dispatch] = useGlobalState();
useEffect(() => {
setInterval(() => getAll(dispatch), 10000);
}, [dispatch]);
useEffect(() => {
init ? getInc(dispatch) : getAll(dispatch);
}, [init, dispatch]);
...
}
Note:
useEffect(() => {
init ? getInc(dispatch) : getAll(dispatch);
}, [init, dispatch]);
After init turns to true, getAll gets called twice, once from upcoming interval and one from the useEffect above.
So in summary, on your Network, first 3 post request intervals with ids [0-99] are:
from getInc
from getAll in useEffect with init
from getAll in interval

Categories

Resources