setState inside useEffect or useCallback, dependency issue - reactjs

I'm trying to remove console warnings from my code, but I'm confused about how to solve this dependency issue.
I have a useEffect hook, that calls a method, removeMessage, which is defined inside my component. I get a warning that this should be in the useEffect dependency array, but if I do that, I create an infinite loop, since the function reference is re-created when the component rerenders.
const Component = () => {
const [list, setList] = useState();
const removeMessage = (message: string) => {
const list = list.filter(x => x.message !== message);
setList(list);
}
useEffect(() => {
...
removeMessage("test");
});
So I read that I'm supposed to use the useCallback hook, to ensure the reference is not changed:
const removeMessage = useCallback((message: string) => {
const list = list.filter(x => x.message !== message);
setList(list);
}, [list]);
But, unless I provide my list as a dependency for that hook, I will get a similar warning. But if I do, I create yet another infinite loop.
This code, and usage of useEffect, is propably bad practice, but I don't know how to work around it, since my method removeMessage is dependent on the state to do its filtering.
Thank you.

I think this should work (using the setState function variant). That way you do not have a dependency on list (and it is also more correct in edge cases)
const Component = () => {
const [list, setList] = useState<Array<{ message: string }>>([]);
const removeMessage = useCallback((message: string) => {
setList(prev => prev.filter(x => x.message !== message));
}, []);
useEffect(() => {
removeMessage('test');
}, [removeMessage]);
};

Related

Function never uses updated state

In the sample component below, myFunc's state1 is never updated while the console.log in the useEffect outputs the updated state correctly. What could be the reason for this?
const TestComponent = () => {
const [state1, setState1] = useState();
useEffect(() => {
console.log(state1);
}, [state1]);
const myFunc(() => {
const newState1 = getNewState1();
console.log(state1); // never outputs updated state, even when myFunc is called multiple times
if (state1 !== newState1) {
console.log('updated state');
setState1(newState1);
}
});
}
Obviously, my real component is much more complicated, but the only time setState1 is called is in myFunc which is confirmed by the useEffect.
Edit:
const TestComponent2 = () => {
useFocusEffect(
useCallback(() => {
myFunc();
}),
);
};
I am trying to call myFunc when TextComponent2 is focused/loaded. I realize that useEffect with no dependencies may be the best option here. Thanks!
I was using useCallback with no relevant dependencies which most likely caused state1 to be the same.

Use Effect and Firebase causing infinite loop

I am using firebase and trying to load all my data at the start of the app using this code:
const [books, setBooks] = useState<BookType[]>([]);
const bookCollectionRef = collection(db, "books");
useEffect(() => {
const getBooks = async () => {
const data = await getDocs(bookCollectionRef);
const temp: BookType[] = data.docs.map((doc) => {
const book: BookType = {
//set properties
};
return book;
});
setBooks(temp);
};
getBooks();
}, [bookCollectionRef]);
This useEffect is getting run constantly leading me to believe that I have made an infinite loop. I don't see why this would be happening because I don't think I am updating bookCollectionRef inside the useEffect hook. Is there possibly a problem where firebase collection references constantly get updated? Any ideas help!
From what I can tell it may be that collection(db, "books") returns a new collection reference each time the component rerenders. Any time the component renders (triggered by parent rerendering, props updating, or updating the local books state) the new bookCollectionRef reference triggers the useEffect hook callback and updates the books state, thus triggering a rerender. Rinse and repeat.
If you don't need to reference the collection outside of the useEffect hook then simply omit bookCollectionRef and reference the collection directly. Trigger the useEffect only when the db value updates.
const [books, setBooks] = useState<BookType[]>([]);
useEffect(() => {
const getBooks = async () => {
const data = await getDocs(collection(db, "books"));
const temp: BookType[] = data.docs.map((doc) => {
const book: BookType = {
//set properties
};
return book;
});
setBooks(temp);
};
getBooks();
}, [db]);
If you only need to run the effect once when the component mounts then remove all dependencies, i.e. use an empty dependency array.

React Native + Firestore infinite loop, using hooks

just starting to learn hooks here.
I am getting data from firestore and trying to set it to state using hooks. when I uncomment the line doing so, I get stuck in an infinite loop. no error, but the console goes crazy with logging the state thousands of times.
Let me know if you need more info!
function Lists(props) {
const [lists, setLists] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const subscriber =
firestore().collection('users').doc(props.user).collection('lists')
.onSnapshot(QuerySnapshot => {
const items = []
QuerySnapshot.forEach(documentSnapshot => {
items.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
//setLists(items)
setLoading(false)
console.log(lists)
})
})
// unsubscribe from firestore
return () => subscriber();
})
//rest of func..
this issue happens becauase useEffect gets called over and over again. useEffect is like componentDidMount and componentDidUpdate if you are familiar with React class components.
so whenever you set the state inside the useEffect, you trigger an update, and then, useEffect gets called again, and thus the infinite loop.
to fix this, useEffect accepts a extra argument, which is an array of dependancies, which indicates that this useEffect call should only re-executed whenever a change happens to one of its dependancies. in your case you can provide an empty array, telling react that this useEffect should only be called one time.
useEffect(() => {
const subscriber =
firestore().collection('users').doc(props.user).collection('lists')
.onSnapshot(QuerySnapshot => {
const items = []
QuerySnapshot.forEach(documentSnapshot => {
items.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
//setLists(items)
setLoading(false)
console.log(lists)
})
})
// unsubscribe from firestore
return () => subscriber();
}, []) // <------------ the second argument we talked about

Invalid hook call when trying to fetch data using useCallback

I'm trying to call useState inside an async function like:
const [searchParams, setSearchParams] = useState({});
const fetchData = () => useCallback(
() => {
if (!isEmpty(searchParams)) {
setIsLoading(true); // this is a state hook
fetchData(searchParams)
.then((ids) => {
setIds(ids); // Setting the id state here
}).catch(() => setIsLoading(false));
}
},
[],
);
There are two states I am trying to set inside this fetchData function (setIsLoading and setIds), but whenever this function is executed am getting the error:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
What is this Rule of hooks I am breaking here?
Is there any way around to set these states from the function?
PS: I only used the useCallback hook here for calling this function with lodash/debounce
Edit: The function is called inside useEffect like:
const debouncedSearch = debounce(fetchSearchData, 1000); // Is this the right way to use debounce? I think this is created every render.
const handleFilter = (filterParams) => {
setSearchParams(filterParams);
};
useEffect(() => {
console.log('effect', searchParams); // {name: 'asd'}
debouncedSearch(searchParams); // Tried without passing arguments here as it is available in state.
// But new searchParams are not showing in the `fetchData`. so had to pass from here.
}, [searchParams]);
The hook rule you are breaking concerns useCallback because you are returning it as the result of your fetchData;
useCallback should be called on top level; not in a callback, like this:
const fetchData = useCallback(
() => {
if (!isEmpty(searchParams)) {
setIsLoading(true); // this is a state hook
fetchData(searchParams)
.then((ids) => {
setIds(ids); // Setting the id state here
}).catch(() => setIsLoading(false));
}
},
[],
);
The code you wrote is equivalent to
const fetchData = () => { return React.useCallback(...
or even
function fetchData() { return React.useCallback(...
To read more about why you can't do this, I highly recommend this blog post.
edit:
To use the debounced searchParams, you don't need to debounce the function that does the call, but rather debounce the searched value. (and you don't actually the fetchData function that calls React.useCallback at all, just use it directly in your useEffect)
I recommend using this useDebounce hook to debounce your search query
const [searchParams, setSearchParams] = React.useState('');
const debouncedSearchParams = useDebounce(searchParams, 300);// let's say you debounce using a delay of 300ms
React.useEffect(() => {
if (!isEmpty(debouncedSearchQuery)) {
setIsLoading(true); // this is a state hook
fetchData(debouncedSearchParams)
.then((ids) => {
setIds(ids); // Setting the id state here
}).catch(() => setIsLoading(false));
}
}, [debouncedSearchParams]); // only call this effect again if the debounced value changes

useEffect re-renders too many times

I have this component, that needs to fetch data, set it to state and then pass it to the children.
Some of the data also needs to be set in context.
My problem is that using useEffect, once called the API, it will re-render for each setvalue() function I need to execute.
I have tried passing to useEffect an empty [] array, still getting the same number of re-renders, due to the fact that the state is changing.
At the moment the array is containg the set...functions to prevent eslint to throw warnings.
Is there a better way to avoid this many re-renders ?
const Home = (props) => {
console.log("TCL: Home -> props", props);
const classes = useStyles();
const [value, setValue] = React.useState(0);
//CONTEXT
const { listSavedJobs, setListSavedJobs, setIsFullView} = useContext(HomeContext);
const {
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
} = useContext(UserContext);
// STATE
const [searchSettings, setSearchSettings] = useState([]);
const [oppData, setOppData] = useState([]);
const handleChange = (event, newValue) => {
setValue(newValue);
};
const handleChangeIndex = index => {
setValue(index);
};
//API CALLS
useEffect(() => {
const triggerAPI = async () => {
setIsFullView(false);
const oppResponse = await API.getOpportunity();
if(oppResponse){
setOppData(oppResponse.response);
}
const profileResponse = await API.getUserProfile();
if(profileResponse){
setUserName(profileResponse.response.first_name);
setUserLastName(profileResponse.response.last_name);
setUserEmail(profileResponse.response.emailId);
}
const profileExtData = await API.getUserProfileExt();
if(profileExtData){
setAvatarProfile(profileExtData.response.avatar);
setListSavedJobs(profileExtData.response.savedJobs);
setSearchSettings(profileExtData.response.preferredIndustry);
}
};
triggerAPI();
}, [
setOppData,
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
setListSavedJobs,
setIsFullView,
]);
...```
Pass just an empty array to second parameter of useEffect.
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
Source
Edit: Try this to avoid rerenders. Use with caution
Only Run on Mount and Unmount
You can pass the special value of empty array [] as a way of saying “only run on mount and unmount”. So if we changed our component above to call useEffect like this:
useEffect(() => {
console.log('mounted');
return () => console.log('unmounting...');
}, [])
Then it will print “mounted” after the initial render, remain silent throughout its life, and print “unmounting…” on its way out.
Prevent useEffect From Running Every Render
If you want your effects to run less often, you can provide a second argument – an array of values. Think of them as the dependencies for that effect. If one of the dependencies has changed since the last time, the effect will run again. (It will also still run after the initial render)
const [value, setValue] = useState('initial');
useEffect(() => {
// This effect uses the `value` variable,
// so it "depends on" `value`.
console.log(value);
}, [value])
For more clarification useEffect
If you are using React 18, this won't be a problem anymore as the new auto batching feature: https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching
If you are using an old version, can refer to this solution: https://statics.teams.cdn.office.net/evergreen-assets/safelinks/1/atp-safelinks.html

Resources