i am trying to paginate the data from my rest server using CoreUI Table Component.
i have problem getting the updated data from redux store after dispatch request in useEffect, i am using redux thunk, i know that dispatch is async, but is there a way to wait for the dispatch to be completed? i tired making the dispatch a Promise but it did not work.
I successfully get the updated result from action and reducer but in ProductsTable its the previous one, i checked redux devtools extension and i can see the state being changed.
i never get the latest value from store.
Also the dispatch is being called so many times i can see in the console window, it nots an infinite loop, it stops after sometime.
const ProductsTable = (props) => {
const store = useSelector((state) => state.store);
const dispatch = useDispatch();
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [pages, setPages] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0);
useEffect(() => {
setLoading(true);
const payload = {
params: {
page,
},
};
if (page !== 0)
dispatch(getAllProducts(payload));
console.log("runs:" + page)
console.log(store.objects)
if(!(Object.keys(store.objects).length === 0)){
setItems(store.objects.results)
setPages(store.objects.total.totalPages)
setLoading(false)
} else{
console.log("error")
setFetchTrigger(fetchTrigger + 1);
}
}, [page, fetchTrigger]);
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={["title", "slug", {
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false
}]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
<CPagination
pages={pages}
activePage={page}
onActivePageChange={setPage}
className={pages < 2 ? "d-none" : ""}
/>
</CCard>
)
}
export default ProductsTable
The reason the ProductsTable always has the previous state data is because the effect you use to update the ProductsTable is missing the store as dependency or more specifically store.objects.results; when the page and the fetchTrigger change the effect becomes stale because it isn't aware that when those dependencies change the effect should change.
useEffect(() => {
// store.objects is a dependency that is not tracked
if (!(Object.keys(store.objects).length === 0)) {
// store.objects.results is a dependency that is not tracked
setItems(store.objects.results);
// store.objects.total.totalPages is a dependency that is not tracked
setPages(store.objects.total.totalPages);
setLoading(false);
}
// add these dependencies to the effect so that everything works as expected
// avoid stale closures
}, [page, fetchTrigger, store.objects, store.objects.results, store.objects.total.totalPages]);
The dispatch is being called many times because you have a recursive case where fetchTrigger is a dependency of the effect but you also update it from within the effect. By removing that dependency you'll see much less calls to this effect, namely only when the page changes. I don't know what you need that value for because I dont see it used in the code you've shared, but if you do need it I recommend using the callback version of setState so that you can reference the value of fetchTrigger that you need without needing to add it as a dependency.
useEffect(() => {
// code
if (!(Object.keys(store.objects).length === 0)) {
// code stuffs
} else {
// use the callback version of setState to get the previous/current value of fetchTrigger
// so you can remove the dependency on the fetchTrigger
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
}
// remove fetchTrigger as a dependency
}, [page, store.objects, store.objects.results, store.objects.totalPages]);
With those issues explained, you'd be better off not adding new state for your items, pages, or loading and instead deriving that from your redux store, because it looks like thats all it is.
const items = useSelector((state) => state.store.objects?.results);
const pages = useSelector((state) => state.store.objects?.total?.totalPages);
const loading = useSelector((state) => !Object.keys(state.store.objects).length === 0);
and removing the effect entirely in favor of a function to add to the onActivePageChange event.
const onActivePageChange = page => {
setPage(page);
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
dispatch(getAllProducts({
params: {
page,
},
}));
};
return (
<CPagination
// other fields
onActivePageChange={onActivePageChange}
/>
);
But for initial results you will still need some way to fetch, you can do this with an effect that only runs once when the component is mounted. This should do that because dispatch should not be changing.
// on mount lets get the initial results
useEffect(() => {
dispatch(
getAllProducts({
params: {
page: 1,
},
})
);
},[dispatch]);
Together that would look like this with the recommended changes:
const ProductsTable = props => {
const items = useSelector(state => state.store.objects?.results);
const pages = useSelector(state => state.store.objects?.total?.totalPages);
const loading = useSelector(state => !Object.keys(state.store.objects).length === 0);
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0);
// on mount lets get the initial results
useEffect(() => {
dispatch(
getAllProducts({
params: {
page: 1,
},
})
);
},[dispatch]);
const onActivePageChange = page => {
setPage(page);
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
dispatch(
getAllProducts({
params: {
page,
},
})
);
};
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={[
'title',
'slug',
{
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false,
},
]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
/>
<CPagination
pages={pages}
activePage={page}
onActivePageChange={onActivePageChange}
className={pages < 2 ? 'd-none' : ''}
/>
</CCard>
);
};
export default ProductsTable;
first of all you don't need any synchronous to achieve the results you want, you have to switch up your code so it doesn't use the state of react since you are already using some kind of global store ( i assume redux ); what you need to do is grab all the items straight from the store don't do an extra logic on the component (read for the separation of concerns); Also I would suggest to do the pagination on the server side not just paginate data on the front end. (getAllProducts() method to switch on fetching just a page of results and not all the products); Your code have alot of dispatches because you are using page and fetchTrigger as dependencies of useEffect hook that means every time the page or fetchTrigger value changes the code inside useEffect will run again resulting in another dispatch;
Here is a slightly modified part of your code, you need to add some extra stuff on your action and a loading param in your global state
const ProductsTable = (props) => {
const dispatch = useDispatch();
// PUT THE LOADING IN THE GLOBAL STATE OR HANDLE IT VIA A CALLBACK OR ADD A GLOBAL MECHANISM TO HANLE LOADINGS INSIDE THE APP
const loading = useSelector(() => state.store.loading)
const items = useSelector((state) => state.store.objects?.results); // ADD DEFAULT EMPTY VALUES FOR objects smthg like : { objects: { results: [], total: { totalPages: 0 } }}
const pages = useSelector((state) => state.store.objects?.total?.totalPages);
const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0); // I DONT UNDERSTAND THIS ONE
const fetchData = () => dispatch(getAllProducts({ params: { page }}));
useEffect(() => {
fetchData();
}, []);
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={["title", "slug", {
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false
}]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
<CPagination
pages={pages}
activePage={page}
onActivePageChange={setPage}
className={pages < 2 ? "d-none" : ""}
/>
</CCard>
)
}
export default ProductsTable
Related
I want to set multiple states and call the useEffect function at once. Here's some code for better understanding.
const [activePage, setActivePage] = useState(1);
const [skip, setSkip] = useState(0);
const [limit, setLimit] = useState(10);
const getUserList = ()=>{
// do something
}
useEffect(() => {
getUserList();
}, [activePage, skip, limit]);
Here you can see I have three dependencies. setting all dependencies calls the getUserList() three times. all three dependencies are independent.
I want to call the getUserList() only once when I need to change all three states, similar to this.setState() in class components like:
this.setState({
activePage: //new value,
skip: // new value,
limit: // new value
},()=>getUserList())
Can someone please let me know if there is any method to achieve this?
This might not be the best solution but I think it should work. I imagine that when the state is changing three times at once, it's a special case, so you could create another state that would store a boolean that would be set to true in the case that it does happen. Then, the useEffect could be set for that boolean instead of all three states.
Special case where all 3 states are being updated:
setActivePage()
setSkip()
setLimit()
setAllChanged(true)
...
useEffect(() => {
getUserList();
}, [allChanged])
For your example, maybe immer.js will solve it better as your intended.
import produce from 'immer'
function FooComponent() {
const [state, setState] = useState({
activePage: 1,
skip: 0,
limit: 10
})
const getUserList = () => {}
// use immer to assign `payload` to `state`
const changeState = (payload) => {
setState(produce(draft => {
Objest.keys(payload).forEach(key => {
draft.key = payload.key
})
}))
}
changeState({
activePage: 2
})
useEffect(() => {
getUserList();
}, [state.activePage, state.skip, state.limit]);
return (< />)
}
I have a React component using an infinite scroll to fetch information from an api using a pageToken.
When the user hits the bottom of the page, it should fetch the next bit of information. I thought myself clever for passing the pageToken to a useEffect hook, then updating it in the hook, but this is causing all of the api calls to run up front, thus defeating the use of the infinite scroll.
I think this might be related to React's derived state, but I am at a loss about how to solve this.
here is my component that renders the dogs:
export const Drawer = ({
onClose,
}: DrawerProps) => {
const [currentPageToken, setCurrentPageToken] = useState<
string | undefined | null
>(null);
const {
error,
isLoading,
data: allDogs,
nextPageToken,
} = useDogsList({
pageToken: currentPageToken,
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting) {
setCurrentPageToken(nextPageToken);
}
},
[nextPageToken],
);
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
}, [handleObserver]);
return (
<Drawer
onClose={onClose}
>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
useDogsList essentially looks like this with all the cruft taken out:
import { useEffect, useRef, useState } from 'react';
export const useDogsList = ({
pageToken
}: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
null,
);
const [allDogs, setAllDogs] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result =
await myClient.listDogs(
getDogsRequest,
{
token,
},
);
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken);
// if API returns a pageToken, that means there are more dogs to add to the list
if (nextPageToken) {
setAllDogs((previousDogList) => [
...(previousDogList ?? []),
...newDogs,
]);
}
}
} catch (responseError: unknown) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
};
fetchData();
}, [ pageToken, nextPageToken]);
return {
data: allDogs,
nextPageToken,
error,
isLoading,
};
};
Basically, the api call returns the nextPageToken, which I want to use for the next call when the user hits the intersecting point, but because nextPageToken is in the dependency array for the hook, the hook just keeps running. It retrieves all of the data until it compiles the whole list, without the user scrolling.
I'm wondering if I should be using useCallback or look more into derivedStateFromProps but I can't figure out how to make this a "controlled" component. Does anyone have any guidance here?
I suggest a small refactor of the useDogsList hook to instead return a hasNext flag and fetchNext callback.
export const useDogsList = ({ pageToken }: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
pageToken // <-- initial token value for request
);
const [allDogs, setAllDogs] = useState([]);
// memoize fetchData callback for stable reference
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await myClient.listDogs(getDogsRequest, { token: nextPageToken });
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken ?? null);
setAllDogs((previousDogList) => [...previousDogList, ...newDogs]);
} catch (responseError) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
}, [nextPageToken]);
useEffect(() => {
fetchData();
}, []); // call once on component mount
return {
data: allDogs,
hasNext: !!nextPageToken, // true if there is a next token
error,
isLoading,
fetchNext: fetchData, // callback to fetch next "page" of data
};
};
Usage:
export const Drawer = ({ onClose }: DrawerProps) => {
const { error, isLoading, data: allDogs, hasNext, fetchNext } = useDogsList({
pageToken // <-- pass initial page token
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting && hasNext) {
fetchNext(); // <-- Only fetch next if there is more to fetch
}
},
[hasNext, fetchNext]
);
useEffect(() => {
const option = {
root: null,
rootMargin: "20px",
threshold: 0
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
// From #stonerose036
// clear previous observer in returned useEffect cleanup function
return observer.disconnect;
}, [handleObserver]);
return (
<Drawer onClose={onClose}>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
Disclaimer
Code hasn't been tested, but IMHO it should be pretty close to what you are after. There may be some minor tweaks necessary to satisfy any TSLinting issues, and getting the correct initial page token to the hook.
While Drew and #debuchet's answers helped me improve the code, the problem around multiple renders ended up being solved by tackling the observer itself. I had to disconnect it afterwards
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
return () => {
observer.disconnect();
};
}, [handleObserver]);
I need to change the useState without rendering the page.
First is it possible?
const UsersComponent = ({valueProp}) => {
const [users, setUsers] = useState(valueProp);
const [oldUsers, setoldUsers] = useState(value);
const allUsers = useSelector((state) =>
state.users
);
useEffect(() => {
dispatch(getUsersData());
}, [dispatch]);
useEffect(() => {
// assign users to state oldUsers
}, [dispatch]);
const onClickMergeTwoArrayOfUsers = () => {
let oldUsers = collectData(oldUsers);
const filteredUsers = intersectionBy(oldUsers, valueProp, "id");
setUsers(filteredUsers); // most important
console.log("filteredUsers", filteredUsers); // not changed
};
I tried everything nothing helps me.
useEffect(() => {
let oldUsers = collectData(oldUsers);
const filteredUsers = intersectionBy(oldUsers, valueProp, "id");
setUsers(filteredUsers); // most important
}, [users]); // RETURN INFINITIVE LOOP
I am also try ->
useEffect(() => {
let oldUsers = collectData(oldUsers);
const filteredUsers = intersectionBy(oldUsers, valueProp, "id");
setUsers(filteredUsers); // most important
}, []);
Load only one and that doesn't mean anything to me..
I am try with useRef ,but that doesn't help me in this case.
I will try to explain the basis of the problem.
I need to get one get data. After that get on the click of a button, I need to merge oldUsers and users without rendering, change the state. That is problem.
If there is no solution to this problem, tell me what I could do to solve the problem?
I am googling but without succes ... I am also try this solution from interent ->
const [state, setState] = useState({});
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
no work.
I am also try with ->
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
Here is problem because I need to asynchronous get only after that can I looping.
Using a ref is probably a better option for whatever it is you're ultimately trying to do.
Yes, it is possible, but it violates one of the core rules of React state: Do Not Modify State Directly.
React compares state values using Object.is equality, so if you simply mutate an object in state instead of replacing it with a new value that is not object-equal, then the state "update" will not cause a re-render (but this is considered a bug in your program!). Anyway, this is how you'd do it:
<div id="root"></div><script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.17.1/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useCallback, useState} = React;
function Example () {
const [state, setState] = useState([1]);
const logState = useCallback(() => console.log(state.join(', ')), [state]);
// Don't actually do this!!!
const mutateState = () => {
setState(arr => {
arr.push(arr.at(-1) + 1);
return arr;
});
};
return (
<>
<div>{state.join(', ')}</div>
<button onClick={mutateState}>Mutate state</button>
<button onClick={logState}>Log state</button>
</>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
I am trying to implement a simple search algorithm for my products CRUD.
The way I thought to do it was entering the input in a search bar, and the products that matched the search would appear instantly every time the user changes the input, without needing to hit a search button.
However, the way I tried to do it was like this:
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
const queryProducts = filterProducts(queryString, productList);
setProductList(queryProducts);
}, [queryString, productList]);
I know that productList changes every render, and that's probably why it isn't working. But I didn't figure out how can I solve the problem. I've seen other problems here and solutions with useReducer, but I none of them seemed to help me.
The error is this one below:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
what you are doing here is fetching a product list and filtering it based on the query string and using that filtered list to render the UI. So ideally your filteredList is just a derived state based on your queryString and productList. So you can remove the filterProducts from your useEffect and move it outside. So that it runs when ever there is a change in the state.
function filterProducts (productName = '', productList = []) {
return productName.trim().length > 0 ? productList.filter((prod)=> {
return prod.title === productName;
}); : productList
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [queryString, setQueryString] = useState('');
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
}
}, [queryString]);
// query products is the derived state
const queryProducts = filterProducts(queryString, productList);
// Now instead of using productList to render something use the queryProducts
return (
{queryProducts.map(() => {
.....
})}
)
If you want the filterProducts to run only on change in queryString or productList then you can wrap it in useMemo
const queryProducts = React.useMemo(() => filterProducts(queryString, productList), [queryString, productList]);
When you use a setState function in a useEffect hook while having the state for that setState function as one of the useEffect hook's dependencies, you'll get this recursive effect where you end up infinitely re-rendering your component.
So, first of all we have to remove productList from the useEffect. Then, we can use a function to update your state instead of a stale update (like what you're doing in your example).
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
setProductList(prevProductList => {
return filterProducts(queryString, prevProductList)
});
}, [queryString]);
Now, you still get access to productList for your filter, but you won't have to include it in your dependencies, which should take care of the infinite re-rendering.
I recommend several code changes.
I would separate the state that immediately reflects the user input at all times from the state that represents the query that is send to the backend. And I would add a debounce between the two states. Something like this:
const [query, setQuery] = useState('');
const [userInput, setUserInput] = useState('');
useDebounce(userInput, setQuery, 750);
I would split up the raw data that was returned from the backend and the filtered data which is just derived from it
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
I would split up the useEffect and not mix different concerns all into one (there is no rule that you cannot have multiple useEffect)
useEffect(() => {
if (query.trim() === '') {
Axios
.get("http://localhost:3001/api/product/get-all")
.then((data) => { setProducts(data.data) });
}
}, [query]);
useEffect(
() => setFilteredProducts(filterProducts(userInput, products)),
[userInput, products]
);
I'm trying to display modal when no products have been selected by user before. I ended up having an infinite loop of useEffect() dependency. I'm not sure how to do it correctly in React.
import React, { useState, useEffect, useCallback } from 'react';
const MyComponent = ({ products }) => {
const [modals, setModals] = useState({});
const [currentModalName, setCurrentModalName] = useState('');
const setCurrentModal = useCallback(
(modalName, data = {}) => {
if (modalName) {
setModals({
...modals,
[modalName]: {
...modals[modalName],
...data
}
});
}
setCurrentModalName(modalName);
},
[modals]
);
useEffect(
() => {
if (!products.length) {
setCurrentModal('chooseProduct')
}
},
[products, setCurrentModal] // setCurrentModal causes infinite loop
);
return (
<div>...</div>
);
}
export default MyComponent;
I can just remove setCurrentModal from the dependencies, but I'm warned about it. If I add it, my React app freezes.
How can I organize my code to avoid freezing?
Why it loops?
The callback is always changing since it depends on the modals, which is always a different object even though it has the exact same properties as before, which always triggers the useEffect since it depends on the setCurrentModal callback value, which is always different since (() => {}) !== (() => {}).
Solution
Always use the functional update when the current state is needed to set the next state.
It'll prevent the need for the modals state as a dependency, which will limit the times when the callback is updated, fixing the infinite loop at the same time.
In addition to solving today's problem, functional update of the state is less prone to race-conditions, where multiple updates batched by React would overwrite each others.
const setCurrentModal = useCallback(
(modalName, data = {}) => {
if (!modalName) return; // precondition fail? early return.
// Happy path here!
// Note that I've used a different name to highlight that
// it's a different variable and to avoid shadowing the
// `modals` var from the outer scope.
setModals((currentModals) => ({ // use functional update.
...currentModals,
[modalName]: {
...currentModals[modalName],
...data
}
}));
setCurrentModalName(modalName);
},
// remove `modals` from the dependencies.
// setter functions are stable anyway, so it should remove any warning.
[setModals, setCurrentModalName]
);
useEffect(() => {
if (!products.length) {
setCurrentModal('chooseProduct')
}
},
[products, setCurrentModal]
);
Since the setCurrentModal callback is now stable (never ever changing), the useEffect will only be called when products value changes.
Missing dependencies warnings
The missing dependencies warnings come from the eslint-plugin-react-hooks, specifically, the react-hooks/exhaustive-deps rule. It's totally optional, but it helps keep the code clean and safe.
You could also choose to disable the warning just for this line:
const setCurrentModal = useCallback(
(modalName, data = {}) => {
// ...
setModals(/* ... */);
setCurrentModalName(modalName);
},
[] // eslint-disable-line react-hooks/exhaustive-deps
);
I think you can simplify it, without using useCallback.
(tested with Next.js and had no warnings, but if you still have some, you should use the answer of #Emile Bergeron)
import React, { useState, useEffect } from 'react'
const MyComponent = ({ products }) => {
const [modals, setModals] = useState({})
const [currentModalName, setCurrentModalName] = useState('')
const setCurrentModal = (name, data) => {
if (name) {
setModals(prev => {
return { ...prev, [name]: { ...prev[name], ...data }}
})
setCurrentModalName(name)
}
}
useEffect(() => {
if (!products || !products.length) {
const modalName = 'chooseProduct'
const data = { data: 'data' }
setCurrentModal(modalName, data)
}
}, [products])
const modalsJsx = modals ? Object.keys(modals).map((x, i) => {
return <li key={`modal-${i}`}>{x}</li>
}) : ''
const addModal = () => {
const name = 'test' + Math.floor(Math.random() * Math.floor(300))
setCurrentModal(name, { data: 'Hey' })
}
return (
<div>
<p>Current Modal : {currentModalName}</p>
<p>Modals : </p>
<ul>
{modalsJsx}
</ul>
<button onClick={addModal}>Test</button>
</div>
)
}
export default MyComponent
The function with useCallback to avoid warnings :
const setCurrentModal = useCallback((name, data = {}) => {
if (name) {
setModals(prev => {
return { ...prev, [name]: { ...prev[name], ...data }}
})
setCurrentModalName(name)
}
}, [setModals, setCurrentModalName])