I am trying to extract my API calls using react-query into a reusable hook. The parameters I need to send to this hook are moduleName and value. For some reason, I get an error that I need to follow hooks rules.
Please advice.
This is my code:
export const useAutoSave = () => {
const fetcher = useCallback(
(
moduleName: ISourceLoaderEditTabs,
value: Partial<ISourceConfigurationEdit[ISourceLoaderEditTabs]>,
saveUrl = '',
) => {
const handleSaveSourceDetailsMutation = useMutation(
(data: ISourceConfigurationEdit) =>
saveUrl
? postSaveStageRaw(`${POST_SAVE_STAGE_RAW}?${saveUrl}`, data)
: saveSourceDetails(data),
);
const sourceId = sessionStorage.getItem('sourceId');
const sourceDetail = queryClient.getQueryData([
'getSourcesDetail',
Number(sourceId),
]);
handleSaveSourceDetailsMutation.mutate(
{
...(sourceDetail as ISourceConfigurationEdit),
[moduleName]: {
...(sourceDetail as ISourceConfigurationEdit)[moduleName],
...value,
},
},
{
onSuccess: async (data) => {
queryClient.setQueryData(
['getSourcesDetail', Number(sourceId)],
data,
);
},
},
);
},
[],
);
return [fetcher];
};
Then in my component I use it as
const [fetch] = useAutoSave();
fetch('abc', {
name:'a2441918'
})
Code snippet : Stackblitz: https://stackblitz.com/edit/react-q8uvse?file=src%2Fhooks.js
you cannot call useMutation inside useCallback. Also, you don't need to. useMutation returns one object with two functions - mutate and mutateAsync, that you can invoke when you want to call invoke your mutation. So your custom hook very likely should only return whatever useMutation returns. The fist argument to useMutation is the mutateFn - the function that is called when you invoke mutate or mutateAsync, and you can also pass one parameters object there:
const useAutoSave = () => {
return useMutation(
({ moduleName, value, saveUrl }) => saveUrl
? postSaveStageRaw(`${POST_SAVE_STAGE_RAW}?${saveUrl}`, data)
: saveSourceDetails(data),
)
}
you can then invoke it via:
const { mutate } = useAutoSave()
<button onClick={() => {
mutate({ moduleName: 'something, value: 'somethingElse' })
}}>Save</button>
The issue as it states in the error log - usage of useMutation
const handleSaveSourceDetailsMutation = useMutation(
(data: ISourceConfigurationEdit) =>
saveUrl
? postSaveStageRaw(`${POST_SAVE_STAGE_RAW}?${saveUrl}`, data)
: saveSourceDetails(data),
);
This useMutation hook needs to be outside of the useCallback. This also means that the saveUrl and other params need to be refactored.
export const useAutoSave = () => {
const i_dont_know = useMutation(x,x,x,x); // hooks can't be called in regular functions
}
Rules of hook for reference: https://reactjs.org/docs/hooks-rules.html
Related
When I click the Link in PublicMyTeam.js
it passes query(=el.group_name) using router.
// PublicMyTeam.js
<Link href={`/public/${el.group_name}`}>
<a><h1 className={styles.team_name}>{el.group_name}</h1></a>
</Link>
But because of async problem of useRouter(), an error occured in useEffect() in [group].js.
It can't read dbService.collection(group).
// [group].js
const router = useRouter()
const { group } = router.query
async function getGroupPlayers() {
const querySnapshot = await dbService.collection(group).doc('group_data').collection('players').get()
querySnapshot.forEach(doc => {
const singlePlayerObject = {
name: doc.data().name,
photoURL: doc.data().photoURL,
joined_date: doc.data().joined_date,
rating: doc.data().rating,
game_all: doc.data().game_all,
game_win: doc.data().game_win,
game_lose: doc.data().game_lose,
status: doc.data().status,
introduce: doc.data().introduce
}
setGroupPlayers(groupPlayers => [...groupPlayers, singlePlayerObject])
})
groupPlayers.sort((a, b) => b.rating - a.rating)
}
useEffect(() => {
getGroupPlayers()
}, [])
What should I do?
Try adding router to the useEffect's dependencies array, and check for router.query.group before calling getGroupPlayers().
useEffect(() => {
if (router.query.group) {
getGroupPlayers()
}
}, [router])
you could try putting the contents of the entire async function inside of useEffect and make the function self-invoking
useEffect(() => {
(async function Update() {
return (await page.current) === reviews_page
? true
: set_reviews_page((page.current = reviews_page));
})();
}, [page.current, reviews_page]);
this works quite well for custom pagination I have configured with SWR. It's my workaround for the same problem
If you want to read up on the topic, it's referred to as IIFEs (Immediately Invoked Function Expression)
So, something like this should do the trick:
useEffect(() => {
(async function getGroupPlayers() {
const querySnapshot = await dbService.collection(group).doc('group_data').collection('players').get()
querySnapshot.forEach(doc => {
const singlePlayerObject = {
name: doc.data().name,
photoURL: doc.data().photoURL,
joined_date: doc.data().joined_date,
rating: doc.data().rating,
game_all: doc.data().game_all,
game_win: doc.data().game_win,
game_lose: doc.data().game_lose,
status: doc.data().status,
introduce: doc.data().introduce
}
setGroupPlayers(groupPlayers => [...groupPlayers, singlePlayerObject])
})
groupPlayers.sort((a, b) => b.rating - a.rating)
})();
}, [])
Although, you may want to add some conditional logic for this to execute as intended (and a lifecycle dependency or two). make a useRef to reference group or groupPlayers and only trigger the lifecycle hook to execute on componentDidUpdate (similar to how I have the state updating when the page.current and reviews_page values differ). page.current is the useRef const for the reviews_page state as follows:
const [reviews_page, set_reviews_page] = useState<number>(1);
const page = useRef<number>(reviews_page);
In my React project using React Query, I have a functional component MoveKeywordModal such that:
when it first loads, it fetches from API endpoint api/keyword_lists to fetch a bunch of keywordLists data. For each of these keywordLists, call it list, I create a clickable element.
When the clickable element (wrapped in a HoverWrapper) gets clicked, I want to send a POST API request to api/keyword_lists/:list_id/keyword_list_items/import with some data.
where :list_id is the id of the list just clicked.
export const MoveKeywordModal = ({
setShowMoveKeywordModal,
keywordsToMove
}) => {
const { data: keywordLists } = useQuery('api/keyword_lists', {})
const [newKeywordList, setNewKeywordList] = useState({})
const { mutate: moveKeywordsToList } = useMutation(
`api/keyword_lists/${newKeywordList.id}/keyword_list_items/import`,
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
const availableKeywordLists = keywordLists
.filter(l => l.id !== activeKeywordList.id)
.map(list => (
<HoverWrapper
id={list.id}
onClick={() => {
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList, data: keywordsToMove }
})
}}>
<p>{list.name}</p>
</HoverWrapper>
))
return (
<>
<StyledModal
isVisible
handleBackdropClick={() => setShowMoveKeywordModal(false)}>
<div>{availableKeywordLists}</div>
</StyledModal>
</>
)
}
Despite calling setNewKeywordList(list) in the onClick of the HoverWrapper, it seems the newKeywordList.id is still not defined, not even newKeywordList is defined.
What should I do to fix it?
Thanks!
react doesn’t perform state updates immediately when you call the setter of useState - an update is merely 'scheduled'. So even though you call setNewKeywordList, the newKeywordList will not have the new value in the next line of code - only in the next render cycle.
So while you are in your event handler, you’ll have to use the list variable:
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList: list, data: keywordsToMove }
})
/edit: I just realized that your call to useMutation is not correct. It doesn’t have a key like useQuery, it has to provide a function as the first argument that takes variables, known as the mutation function:
const { mutate: moveKeywordsToList } = useMutation(
(variables) => axios.post(`api/keyword_lists/${variables.newKeywordList.id}/keyword_list_items/import`),
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
see also: https://react-query.tanstack.com/guides/mutations
I'm creating a custom hook to detect clicks inside/outside a given HTMLElement.
Since the hook accepts a function as an argument, it seems like either the input needs to be wrapped in a useCallback or stored inside the hook with useRef to prevent useEffect from triggering repeatedly.
Are both of the following approaches functionally the same?
Approach One (preferred)
// CALLER
useClickInsideOutside({
htmlElement: htmlRef.current,
onClickOutside: () => {
// Do something via anonymous function
},
});
// HOOK
const useClickInsideOutside = ({
htmlElement,
onClickOutside,
}) => {
const onClickOutsideRef = useRef(onClickOutside);
onClickOutsideRef.current = onClickOutside;
useEffect(() => {
function handleClick(event) {
if (htmlElement && !htmlElement.contains(event.target)) {
onClickOutsideRef.current && onClickOutsideRef.current();
}
}
document.addEventListener(MOUSE_DOWN, handleClick);
return () => { document.removeEventListener(MOUSE_DOWN, handleClick); };
}, [htmlElement]);
}
Approach Two
// CALLER
const onClickOutside = useCallback(() => {
// Do something via memoized callback
}, []);
useClickInsideOutside({
htmlElement: htmlRef.current,
onClickOutside,
});
// HOOK
const useClickInsideOutside = ({
htmlElement,
onClickOutside,
}) => {
useEffect(() => {
function handleClick(event) {
if (htmlElement && !htmlElement.contains(event.target)) {
onClickOutside();
}
}
document.addEventListener(MOUSE_DOWN, handleClick);
return () => { document.removeEventListener(MOUSE_DOWN, handleClick); };
}, [htmlElement, onClickOutside]);
}
Does the first one (which I prefer, because it seems to make the hook easier to use/rely on fewer assumptions) work as I imagine? Or might useEffect suffer from enclosing stale function references inside handleClick?
useCallback is the right approach for this. However, I think I'd design it in a way where I could abstract the memo-ing away from the consumer:
/**
* #param {MutableRefObject<HTMLElement>} ref
* #param {Function} onClickOutside
*/
const useClickInsideOutside = (ref, onClickOutside) => {
const { current: htmlElement } = ref;
const onClick = useCallback((e) => {
if (htmlElement && !htmlElement.contains(e.target)) {
onClickOutside();
}
}, [htmlElement, onClickOutside])
useEffect(() => {
document.addEventListener(MOUSE_DOWN, onClick);
return () => document.removeEventListener(MOUSE_DOWN, onClick);;
}, [onClick]);
}
And since I already touched on design, I'd try to make the design resemble similar API functions where 2 arguments are [in]directly related. I'd end up looking like this:
// Consumer component
const ref = useRef()
useClickOutside(ref, () => {
// do stuff in here
})
I have a mystery. Consider the following custom React hook that fetches data by time period and stores the results in a Map:
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(new Map(data.set(period, asyncState)));
}
},
[setData, data, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(periodData => {
updateData(dateRange, makeAsyncData(periodData));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRanges /*, updateData - for some reason when included this triggers infinite renders */]);
return data;
}
The useEffect is being repeatedly triggered when updateData is added as a dependency. If I exclude it as a dependency then everything works / behaves as expected but eslint complains I'm violating react-hooks/exhaustive-deps.
Given updateData has been useCallback-ed I'm at a loss to understand why it should repeatedly trigger renders. Can anyone shed any light please?
The problem lies in the useCallback/useEffect used in combination. One has to be careful with dependency arrays in both useCallback and useEffect, as the change in the useCallback dependency array will trigger the useEffect to run.
The “data” variable is used inside useCallback dependency array, and when the setData is called react will rerun function component with new value for data variable and that triggers a chain of calls.
Call stack would look something like this:
useEffect run
updateData called
setState called
component re-renders with new state data
new value for data triggers useCallback
updateData changed
triggers useEffect again
To solve the problem you would need to remove the “data” variable from the useCallback dependency array. I find it to be a good practice to not include a component state in the dependency arrays whenever possible.
If you need to change component state from the useEffect or useCallback and the new state is a function of the previous state, you can pass the function that receives a current state as parameter and returns a new state.
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
In your example you need the current state only to calculate next state so that should work.
This is what I now have based on #jure's comment above:
I think the problem is that the "data" variable is included in the dependency array of useCallback. Every time you setData, the data variable is changed that triggers useCallback to provide new updateData and that triggers useEffect. Try to implement updateData without a dependecy on the data variable. you can do something like setData(d=>new Map(d.set(period, asyncState)) to avoid passing "data" variable to useCallback
I adjusted my code in the manners suggested and it worked. Thanks!
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(traffic => {
updateData(dateRange, makeAsyncData(traffic));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
}, [dateRanges , updateData]);
return data;
}
let's I have a graphql mutation component, that I reuse in many places
const MarkAsViewed =({ type = 1, children }) => {
const markAsViewed = (commitMutation) => (type) => {
return commitMutation({
variables: { type }
});
};
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{
(commitMutation, { error, loading }) => children({
markAsViewed: markAsViewed(commitMutation)
})
}
</MarkAsViewedMutation>
);
};
however since markAsViewed is a closure function, it will always return different function with different ref which means different for react.
this makes the child component to have to do a useCallback like:
const alwaysSameRefFunc = useCallback(()=>{ markAsViewed(), []}
above works but creates 2 problems:
I get linter warning saying I should add markAsViewed as dependency blah blah. which I cannot, because it triggers infinite loop (since it's different ref every time)
everyone that uses <MarkAsViewed /> component will need to manually memoirzation
Ideally this is what I want, but it's an invalid code, because "markAsViewed" is not a react component and cannot have useCallback
const markAsViewed = (commitMutation) => useCallback((type) => {
return commitMutation({
variables: { type }
});
}, []);
any idea how can I solve the issue?
note: we are not ready to update Apollo version to have hoook yet
Does the following work?
const markAsViewed = commitMutation => type => {
return commitMutation({
variables: { type },
});
};
const MarkAsViewed = ({ type = 1, children }) => {
const fn = useCallback(
(commitMutation, { error, loading }) =>
children({
markAsViewed: markAsViewed(commitMutation),
}),
[children]
);
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{fn}
</MarkAsViewedMutation>
);
};
I'm not sure if that will work because it still depends on children, if that causes unnesesary renders then maybe post how MarkAsViewed component is rendered.