I need a refetch on an Apollo query to trigger useEffect. I'm implementing a filter bar that filters by a date period and a text search. My search submit button triggers the refetch on the query, and the data returned is a dependency of my useEffect. When the data is returned, if the search fields are populated, the filter should run. This works the first time I run a search, but when I reset with my reset function, the reset is triggered and the query gets 200 in Chrome and shows the data in Chrome dev tools, but useEffect is not triggered. Why is this?
//React
import React, {useState, useContext, useEffect, Fragment } from 'react'
//extra React libraries
//external libraries
import { useQuery, useMutation } from '#apollo/react-hooks'
//components
import AccountsGrid from '../../containers/AccountsGrid/AccountsGrid'
import SpinnerLoader from '../../components/SpinnerLoader/SpinnerLoader'
//styles
import styles from "./accounts.module.scss";
//contexts
import UserContext from '../../containers/contexts/UserContext/UserContext'
import AccountContext from '../../containers/contexts/AccountContext/AccountContext'
//constants
import {
GET_PARENT_ACCOUNT,
SET_ACCOUNTS,
SET_IS_ACTIVE,
SET_ACCOUNTS_LIMIT,
SET_ACCOUNTS_OFFSET,
SET_ACCOUNTS_TOTAL,
SET_ACCOUNTS_START_DATE,
SET_ACCOUNTS_END_DATE,
} from '../../containers/contexts/AccountContext/accountActionTypes'
import {
GET_ACCOUNT_USERS,
GET_ACCOUNTS,
TOTAL_ACCOUNTS,
} from '../../utils/constants/queries/accountQueries'
import {
UPDATE_ACCOUNT
} from '../../utils/constants/mutations/accountMutations'
import moment from 'moment';
function Accounts(props) {
const userContext = useContext(UserContext)
const accountContext = useContext(AccountContext)
const parentAccountID = userContext.userState.accountId
const [parentAccountIDs, setParentAccountIDs] = useState(null)
const [sortByField, setSortByField] = React.useState(null);
const [sortByType, setSortByType] = React.useState(null);
const [startDate, setStartDate] = React.useState(null);
const [endDate, setEndDate] = React.useState(null);
const [datesValid, setDatesValid] = React.useState(true);
const [searchText, setSearchText] = React.useState("");
const {
loading: loadingAccountUsers,
error: errorAccountUsers,
data: dataAccountUsers,
refetch: refetchAccountUsers,
} = useQuery(GET_ACCOUNT_USERS, {
variables: {
accountId: parentAccountID
}
})
const {
loading: loadingAccounts,
error: errorAccounts,
data: dataAccounts,
refetch: refetchAccounts
} = useQuery(GET_ACCOUNTS, {
variables: {
parentIds: parentAccountIDs,
offset: accountContext.accountState.data.accountsOffset,
limit: accountContext.accountState.data.accountsLimit
}
})
const {
loading: loadingAccountsTotal,
error: errorAccountsTotal,
data: dataAccountsTotal,
refetch: refetchAccountsTotal
} = useQuery(TOTAL_ACCOUNTS)
const [
updateAccount,
{ loading, error, data: updateAccountData }
] = useMutation(UPDATE_ACCOUNT);
const setParentIDsHandler = (id) => {
setParentAccountIDs(String(id))
}
const setOffset = (offset, limit) => {
accountContext.accountDispatch({
type: SET_ACCOUNTS_OFFSET,
payload: {
offset: offset
}
})
accountContext.accountDispatch({
type: SET_ACCOUNTS_LIMIT,
payload: {
limit: limit
}
})
}
const deactivateUser = row => {
updateAccount({
variables: {
account: {
id: row.id,
isActive: !row.isActive
}
}
})
accountContext.accountDispatch({type: SET_IS_ACTIVE, payload: row.id})
}
const handleRequestSort = (sortType, sortBy) => {
sortRow(sortBy)
setSortByType(sortType)
}
const sortRow = (sortBy) => {
switch(sortBy) {
case 'Contact':
setSortByField('email')
break
case 'First Name':
setSortByField('firstName')
break
case 'Last Name':
setSortByField('lastName')
break
case 'Join Date':
setSortByField('dateJoined')
break
}
}
const setDates = (dates) => {
console.log("SETTING DATES", dates)
if ("startDate" === Object.keys(dates)[0]) {
setStartDate(dates.startDate)
} else {
setEndDate(dates.endDate)
}
}
const checkDatesValid = () => {
if (startDate && endDate) {
if (startDate <= endDate) {
return setDatesValid(true)
}
}
return setDatesValid(false)
}
const clearDates = () => {
console.log("CLEARING")
setDatesValid(true)
setStartDate(null)
setEndDate(null)
setSearchText(null)
accountContext.accountDispatch({type: SET_ACCOUNTS_START_DATE, payload: {startDate: null}})
accountContext.accountDispatch({type: SET_ACCOUNTS_END_DATE, payload: {endDate: null}})
return setDatesValid(true)
}
const searchByChars = (text) => {
console.log("SEARCH TEXT", text)
setSearchText(text)
resetAccounts()
}
const resetAccounts = () => {
console.log("RESET ACCOUNTS TRIGGERED")
refetchAccounts({
variables: {
parentIds: parentAccountIDs,
offset: accountContext.accountState.data.accountsOffset,
limit: accountContext.accountState.data.accountsLimit
}
})
console.log("LOADING", loadingAccounts)
console.log("ERROR", errorAccounts)
}
const filterText = (textRows) => {
let newTextRows = []
for (let row of textRows) {
Object.entries(row['users'][0]).forEach(([key, val]) => {
if (String(val).includes(String(searchText))) {
if (!(newTextRows.includes(row))) {
newTextRows.push(row)
}
}
});
}
accountContext.accountDispatch({type: SET_ACCOUNTS, payload: {accounts: newTextRows}})
}
const filterDates = (dateRows) => {
let newDateRows = []
for (let row of dateRows) {
if (datesValid) {
const _date = moment(row.users[0]['dateJoined'])
const sdate = moment(startDate)
const edate = moment(endDate)
if (_date.isBetween(sdate, edate, null, '[]')) {
if (!(newDateRows.includes(row))) {
newDateRows.push(row)
}
}
}
}
accountContext.accountDispatch({type: SET_ACCOUNTS, payload: {accounts: newDateRows}})
}
useEffect(() => {
if (sortByField && sortByType) {
const newRows = accountContext.accountState.data.accounts.sort((a, b) => {
const compareA = a.users[0][sortByField]
const compareB = b.users[0][sortByField]
if (compareA || compareB) {
if ("DESC" === sortByType) {
let comparison = 0;
if (compareA > compareB) {
comparison = 1;
} else if (compareA < compareB) {
comparison = -1;
}
return comparison;
} else {
let comparison = 0;
if (compareA < compareB) {
comparison = 1;
} else if (compareA > compareB) {
comparison = -1;
}
return comparison;
}
}
})
accountContext.accountDispatch({type: SET_ACCOUNTS, payload: newRows})
}
if (dataAccountsTotal) {
accountContext.accountDispatch({type: SET_ACCOUNTS_TOTAL, payload: dataAccountsTotal})
}
if (dataAccountUsers) {
setParentIDsHandler(dataAccountUsers.accountUsers[0].account.id)
accountContext.accountDispatch({type: GET_PARENT_ACCOUNT, payload: dataAccountUsers})
}
if (dataAccounts) {
console.log("NEW DATA", searchText, startDate, endDate, datesValid)
console.log("SEARCH TEXT", searchText)
console.log("START DATE", startDate)
console.log("END DATE", endDate)
accountContext.accountDispatch({type: SET_ACCOUNTS, payload: dataAccounts})
console.log("DATES VALID", datesValid)
if (startDate
&& endDate && datesValid) {
console.log("FILTER BY DATES")
filterDates(accountContext.accountState.data.accounts)
}
if (searchText) {
filterText(dataAccounts)
}
}
}, [
loadingAccounts,
dataAccounts,
dataAccountsTotal,
dataAccountUsers,
parentAccountIDs,
updateAccountData,
sortByField,
sortByType,
searchText
])
return (
<Fragment>
{
accountContext.accountState.data.accounts &&
!loadingAccountUsers &&
!errorAccountUsers &&
!loadingAccounts &&
!errorAccounts &&
parentAccountIDs &&
accountContext.accountState.data.accountUsers
? <AccountsGrid
setOffset={setOffset}
currentStartDate={startDate}
currentEndDate={endDate}
searchText={searchByChars}
datesValid={datesValid}
resetAccounts={resetAccounts}
setDates={setDates}
clearDates={clearDates}
deactivateUser={deactivateUser}
handleRequestSort={handleRequestSort}
accountRows={accountContext.accountState.data.accounts}
/> : <SpinnerLoader />}
</Fragment>
)
}
export default Accounts
I realize this is an old question, but I had a similar issue and arrived here to an unanswered question. I would like to share what I have learned about useEffect hook with Apollo Query or in any other use case, in the event it helps someone else.
useEffect hook does not actively "watch" and only triggers under select circumstances. Data loading from a graphql query is not one of the triggers. useEffect will run :
After re-rendering of the layout when the component mounts
If the state changes of the component
If the props of the component change
When the component unmounts
Please check out this amazing article for more details.
Also if the variable is placed in the dependency array at the end of the useEffect hook, by design, it will NOT run if there are no changes to the variable.
So in order to trigger useEffect after data is loaded from an Apollo query you would have to make one of those conditions happen when the data loads.
It was much easier in my case to simply use the data after loading because trying to sync setState which is async with data loading which is async created some varied and unpredictable results.
It did help to use the parent class and pass in a variable as a prop which would trigger useEffect to setState when it changed. The variable was used in the actual Apollo query so it made sense to the overall design.
Here is an example:
export const ResultsGrid = ( queryTerm ) => {
const [searchTerm, setSearchTerm] = useState('')
const { data, error, loading } = useQuery(GET_RESULTS_QUERY, {variables: { searchTerm }, })
useEffect(() => {setSearchTerm(queryTerm)}
,[queryTerm]) //useEffect will run only when queryTerm changes
if (loading) return <p>Loading data from query...</p>
if (error)
return (
<React.Fragment>
<p>Sorry, unable to fetch results. </p>
<button onClick={() => refetch}>Please try again!</button>
{console.log("error", error)}
</React.Fragment>
)
console.log('data:', data)
console.log('loading:', loading)
const { results } = data
return (
//display data results here
)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.production.min.js"></script>
I hope this is helpful and answers the question.
Related
I have this component:
const updateUrl = (url: string) => history.replaceState(null, '', url);
// TODO: Rename this one to account transactions ATT: #dmuneras
const AccountStatement: FC = () => {
const location = useLocation();
const navigate = useNavigate();
const { virtual_account_number: accountNumber, '*': transactionPath } =
useParams();
const [pagination, setPagination] = useState<PaginatorProps>();
const [goingToInvidualTransaction, setGoingToInvidualTransaction] =
useState<boolean>(false);
const SINGLE_TRANSACTION_PATH_PREFIX = 'transactions/';
// TODO: This one feels fragile, just respecting what I found, but, we could
// investigate if we can jsut rely on the normal routing. ATT. #dmuneras
const transactionId = transactionPath?.replace(
SINGLE_TRANSACTION_PATH_PREFIX,
''
);
const isFirst = useIsFirstRender();
useEffect(() => {
setGoingToInvidualTransaction(!!transactionId);
}, [isFirst]);
const {
state,
queryParams,
dispatch,
reset,
setCursorAfter,
setCursorBefore
} = useLocalState({
cursorAfter: transactionId,
includeCursor: !!transactionId
});
const {
filters,
queryParams: globalQueryParams,
setDateRange
} = useGlobalFilters();
useUpdateEffect(() => {
updateUrl(
`${location.pathname}?${prepareSearchParams(location.search, {
...queryParams,
...globalQueryParams
}).toString()}`
);
}, [transactionId, queryParams]);
useUpdateEffect(() => dispatch(reset()), [globalQueryParams]);
const account_number = accountNumber;
const requestParams = accountsStateToParams({
account_number,
...state,
...filters
});
const { data, isFetching, error, isSuccess } =
useFetchAccountStatementQuery(requestParams);
const virtualAccountTransactions = data && data.data ? data.data : [];
const nextPage = () => {
dispatch(setCursorAfter(data.meta.cursor_next));
};
const prevPage = () => {
dispatch(setCursorBefore(data.meta.cursor_prev));
};
const onRowClick = (_event: React.MouseEvent<HTMLElement>, rowData: any) => {
if (rowData.reference) {
if (rowData.id == transactionId) {
navigate('.');
} else {
const queryParams = prepareSearchParams('', {
reference: rowData.reference,
type: rowData.entry_type,
...globalQueryParams
});
navigate(
`${SINGLE_TRANSACTION_PATH_PREFIX}${rowData.id}?${queryParams}`
);
}
}
};
const checkIfDisabled = (rowData: TransactionData): boolean => {
return !rowData.reference;
};
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
const showTransactionsTable: boolean =
Array.isArray(virtualAccountTransactions) && isSuccess && data?.data;
const onTransactionSourceLoaded = (
transactionSourceData: PayoutDetailData
) => {
const isIncludedInPage: boolean = virtualAccountTransactions.some(
(transaction: TransactionData) => {
if (transactionId) {
return transaction.id === parseInt(transactionId, 10);
}
return false;
}
);
if (!goingToInvidualTransaction || isIncludedInPage) {
return;
}
const fromDate = dayjs(transactionSourceData.timestamp);
const toDate = fromDate.clone().add(30, 'day');
setDateRange({
type: 'custom',
to: toDate.format(dateFormat),
from: fromDate.format(dateFormat)
});
setGoingToInvidualTransaction(false);
};
const fromDate = requestParams.created_after || dayjs().format('YYYY-MM-DD');
const toDate = requestParams.created_before || dayjs().format('YYYY-MM-DD');
const routes = [
{
index: true,
element: (
<BalanceWidget
virtualAccountNumber={account_number}
fromDate={fromDate}
toDate={toDate}
/>
)
},
{
path: `${SINGLE_TRANSACTION_PATH_PREFIX}:transaction_id`,
element: (
<TransactionDetails
onTransactionSourceLoaded={onTransactionSourceLoaded}
/>
)
}
];
return (........
I get this error: 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.
The useEffect where the issue is, it is this one:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
Considering previous answers, would the solution be to make sure I return a new object each time? But I am not sure what would be the best approach. Any clues ?
did you want the useEffect to start every changes of 'data?.meta' ?
Without reading all the code, I believe the data.meta object changes on every render. There is a way to change the useEffect to narrow done its execution conditions:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [!data?.meta, data?.meta?.has_previous_page, data?.meta?.has_next_page]);
Please note the ! before data.?.meta which makes the hook test only for presence or absence of the object, since your code doesn't need more than that information.
I save product ids in a localstorage to be used in recently viewed component.
My Recently viewed component is below
import { useQuery } from "#apollo/client";
import { getRecentlyViewedProductArr } from "#gb-utils/product/product"
import { RECENTLY_VIEWED } from "#gb-utils/queries/product";
import { useEffect, useState } from "react";
export default function RecentlyViewed() {
const [recentIds, setRecentIds] = useState([])
const { loading, error, data } = useQuery(RECENTLY_VIEWED, {
variables: { ids: recentIds }
})
useEffect(() => {
setRecentIds(getRecentlyViewedProductArr())
}, []);
if (loading) {
return 'Loading';
}
if (error) {
return error.message
}
return (
<div>{JSON.stringify(data)}</div>
)
}
My question is about how I use get the product from wp-graphql using userQuery with the local storage.
Currently I am defining a state to store product ids and on mount of the RecentlyViewed component I update the recentIds by getting the data from localstorage. Is there a better way of doing the code above because I feel like it fetching data from wp without or empty recentIds is a problem.
helper.js
export const addRecentlyViewedProduct = (productId) => {
let recent = getRecentlyViewedProduct()
if (isEmpty(recent)) {
// If recently viewed is empty then we just save the product id
saveRecentlyViewedProduct(productId)
return getRecentlyViewedProduct()
}
// if not empty then we check if product id exist and remove
if (recent.includes(productId)) {
recent = recent.replace(productId, '')
}
// Since we remove the product then we Add the product id again
recent = "".concat(productId, '|', recent)
recent = recent.replace('||', '|');
recent = recent.startsWith('|') ? recent.substring(1) : recent
recent = recent.endsWith('|') ? recent.substring(0, recent.length - 1) : recent;
const limit = 5;
let recentProductIds = recent.split('|')
if (recentProductIds.length > limit) {
recentProductIds = recentProductIds.slice(0, limit)
}
saveRecentlyViewedProduct(recentProductIds.join('|'))
return getRecentlyViewedProduct()
}
export const saveRecentlyViewedProduct = (value) => {
return localStorage.setItem('woo-next-recent-product', value);
}
export const getRecentlyViewedProduct = () => {
const recentProductsStr = localStorage.getItem('woo-next-recent-product')
return recentProductsStr
}
export const getRecentlyViewedProductArr = () => {
const recentProductsStr = getRecentlyViewedProduct()
if (isEmpty(recentProductsStr)) {
return ''
}
let recentProductIds = recentProductsStr.split('|')
return Array.from(recentProductIds.slice(1), Number); // get the 2nd to the last element
}
You can use the skip option from the useQuery API: https://www.apollographql.com/docs/react/data/queries/#skip
const [recentIds, setRecentIds] = useState([])
const { loading, error, data } = useQuery(RECENTLY_VIEWED, {
variables: { ids: recentIds },
skip: recentIds.length === 0
})
useEffect(() => {
setRecentIds(getRecentlyViewedProductArr())
}, []);
I would like to set a 24 hours cache once a useQuery request has succeeded.
But as soon as I refresh the page, the cache is gone. I see it because I console.log a message each time the route is hit on my server.
How to prevent this behaviour and implement a real cache?
Here is the code:
import { useQuery } from "react-query";
import { api } from "./config";
const _getUser = async () => {
try {
const res = api.get("/get-user");
return res;
} catch (err) {
return err;
}
};
export const getUser = () => {
const { data } = useQuery("contact", () => _getUser(), {
cacheTime: 1000 * 60 * 60 * 24,
});
return { user: data && data.data };
};
// then in the component:
const { user } = getUser();
return (
<div >
hello {user?.name}
</div>
I've also tried to replace cacheTime by staleTime.
if you reload the browser, the cache is gone because the cache lives in-memory. If you want a persistent cache, you can try out the (experimental) persistQueryClient plugin: https://react-query.tanstack.com/plugins/persistQueryClient
React query has now an experimental feature for persisting stuff on localStorage.
Nonetheless, I preferred using a custom hook, to make useQuery more robust and to persist stuff in localSTorage.
Here is my custom hook:
import { isSameDay } from "date-fns";
import { useEffect, useRef } from "react";
import { useBeforeunload } from "react-beforeunload";
import { useQuery, useQueryClient } from "react-query";
import { store as reduxStore } from "../../redux/store/store";
const LOCAL_STORAGE_CACHE_EXPIRY_TIME = 1000 * 60 * 60 * 23; // 23h
const divider = "---$---";
const defaultOptions = {
persist: true, // set to false not to cache stuff in localStorage
useLocation: true, // this will add the current location pathname to the component, to make the query keys more specific. disable if the same component is used on different pages and needs the same data
persistFor: LOCAL_STORAGE_CACHE_EXPIRY_TIME,
invalidateAfterMidnight: false, // probably you want this to be true for charts where the dates are visible. will overwrite persistFor, setting expiry time to today midnight
defaultTo: {},
};
const getLocalStorageCache = (dataId, invalidateAfterMidnight) => {
const data = localStorage.getItem(dataId);
if (!data) {
return;
}
try {
const parsedData = JSON.parse(data);
const today = new Date();
const expiryDate = new Date(Number(parsedData.expiryTime));
const expired =
today.getTime() - LOCAL_STORAGE_CACHE_EXPIRY_TIME >= expiryDate.getTime() ||
(invalidateAfterMidnight && !isSameDay(today, expiryDate));
if (expired || !parsedData?.data) {
// don't bother removing the item from localStorage, since it will be saved again with the new expiry time and date when the component is unmounted or the user leaves the page
return;
}
return parsedData.data;
} catch (e) {
console.log(`unable to parse local storage cache for ${dataId}`);
return undefined;
}
};
const saveToLocalStorage = (data, dataId) => {
try {
const wrapper = JSON.stringify({
expiryTime: new Date().getTime() + LOCAL_STORAGE_CACHE_EXPIRY_TIME,
data,
});
localStorage.setItem(dataId, wrapper);
} catch (e) {
console.log(
`Unable to save data in localStorage for ${dataId}. Most probably there is a function in the payload, and JSON.stringify failed`,
data,
e
);
}
};
const clearOtherCustomersData = globalCustomerId => {
// if we have data for other customers, delete it
Object.keys(localStorage).forEach(key => {
if (!key.includes(`preferences${divider}`)) {
const customerIdFromCacheKey = key.split(divider)[1];
if (customerIdFromCacheKey && customerIdFromCacheKey !== String(globalCustomerId)) {
localStorage.removeItem(key);
}
}
});
};
const customUseQuery = (queryKeys, getData, queryOptions) => {
const options = { ...defaultOptions, ...queryOptions };
const store = reduxStore.getState();
const globalCustomerId = options.withRealCustomerId
? store.userDetails?.userDetails?.customerId
: store.globalCustomerId.id;
const queryClient = useQueryClient();
const queryKey = Array.isArray(queryKeys)
? [...queryKeys, globalCustomerId]
: [queryKeys, globalCustomerId];
if (options.useLocation) {
if (typeof queryKey[0] === "string") {
queryKey[0] = `${queryKey[0]}--path--${window.location.pathname}`;
} else {
try {
queryKey[0] = `${JSON.stringify(queryKey[0])}${window.location.pathname}`;
} catch (e) {
console.error(
"Unable to make query. Make sure you provide a string or array with first item string to useQuery",
e,
);
}
}
}
const queryId = `${queryKey.slice(0, queryKey.length - 1).join()}${divider}${globalCustomerId}`;
const placeholderData = useRef(
options.persist
? getLocalStorageCache(queryId, options.invalidateAfterMidnight) ||
options.placeholderData
: options.placeholderData,
);
const useCallback = useRef(false);
const afterInvalidationCallback = useRef(null);
const showRefetch = useRef(false);
const onSuccess = freshData => {
placeholderData.current = undefined;
showRefetch.current = false;
if (options.onSuccess) {
options.onSuccess(freshData);
}
if (useCallback.current && afterInvalidationCallback.current) {
afterInvalidationCallback.current(freshData);
useCallback.current = false;
afterInvalidationCallback.current = null;
}
if (options.persist) {
if(globalCustomerId){
saveToLocalStorage(freshData, queryId);
}
}
};
const data = useQuery(queryKey, getData, {
...options,
placeholderData: placeholderData.current,
onSuccess,
});
const save = () => {
if (options.persist && data?.data) {
saveToLocalStorage(data.data, queryId);
}
};
// if there are other items in localStorage with the same name and a different customerId, delete them
// to keep the localStorage clear
useBeforeunload(() => clearOtherCustomersData(globalCustomerId));
useEffect(() => {
return save;
}, []);
const invalidateQuery = callBack => {
if (callBack && typeof callBack === "function") {
useCallback.current = true;
afterInvalidationCallback.current = callBack;
} else if (callBack) {
console.error(
"Non function provided to invalidateQuery. Make sure you provide a function or a falsy value, such as undefined, null, false or 0",
);
}
showRefetch.current = true;
queryClient.invalidateQueries(queryKey);
};
const updateQuery = callBackOrNewValue => {
queryClient.setQueryData(queryKey, prev => {
const updatedData =
typeof callBackOrNewValue === "function"
? callBackOrNewValue(prev)
: callBackOrNewValue;
return updatedData;
});
};
return {
...data,
queryKey,
invalidateQuery,
data: data.data || options.defaultTo,
updateQuery,
isFetchingAfterCacheDataWasReturned:
data.isFetching &&
!placeholderData.current &&
!data.isLoading &&
showRefetch.current === true,
};
};
export default customUseQuery;
Some things are specific to my project, like the customerId.
I'm using onBeforeUnload to delete data not belonging to the current customer, but this project specific.
You don't need to copy paste all this, but I believe it's very handy to have a custom hook around useQuery, so you can increase its potential and do things like running a callback with fresh data after the previous data has been invalidated or returning the invalidateQuery/updateQuery functions, so you don't need to use useQueryClient when you want to invalidate/update a query.
useEffect doesn't trigger on second change, but triggers twice on launch (React hooks and apollo-graphql hooks).
In console.logs I described when the changes are triggered and when not.
I don't have any more clue to add.
Here's my page (Next.js pages)
import React, { useEffect, useState } from 'react'
import Calendar from '../components/Calendar';
import { useHallsQuery } from '../generated/graphql';
const schedule = () => {
const { data, loading, error, refetch:refetchSessions } = useHallsQuery();
const [sessions, setSessions] = useState([] as any);
const [owners, setOwners] = useState([] as any);
useEffect(() => {
console.log('On launch trigger twice, on first change trigger once, on second doesn't trigger and later trigger correctly, but it's one change delay');
if(loading === false && data){
const colors: any = [
'#FF3333',
'#3333FF',
'#FFFF33',
'#33FF33',
'#33FFFF',
'#9933FF',
'#FF9933',
'#FF33FF',
'#FF3399',
'#A0A0A0'
];
let pushSessions:any = [];
let owners:any = [];
data?.halls?.map(({sessions, ...o}, index) =>{
owners.push({id:o.id,
text:o.name,
color: colors[index%10]});
sessions.map((session:any) => {
pushSessions.push({...session,
ownerId: o.id});
})
})
setSessions(pushSessions);
setOwners(owners);
}
}, [loading, data])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<Calendar sessions={sessions} owners={owners} refetchSessions={refetchSessions} />
</div>
)
}
export default schedule
and my component part where I get props and trigger refetchSessions.
const Calendar = (props: any) => {
let { sessions, owners, refetchSessions } = props;
const [moveSession] = useMoveSessionMutation();
...
const commitChanges = ({ added, changed, deleted }: any) => {
if (added) {
//
}
if (changed) {
console.log('trigger on all changes! Correct')
const id = Object.keys(changed)[0];
moveSession({variables:{
input: {
id: parseInt(id),
...changed[id]
}
}, refetchQueries: refetchSessions
})
}
if (deleted !== undefined) {
//
}
};
return (
// Some material-ui components and #devexpress/dx-react-scheduler-material-ui components in which commitChanges function is handled
)
export default Calendar;
Hook was generated with graphql-codegen(generated/graphql.tsx):
export function useHallsQuery(baseOptions?: Apollo.QueryHookOptions<HallsQuery, HallsQueryVariables>) {
return Apollo.useQuery<HallsQuery, HallsQueryVariables>(HallsDocument, baseOptions);
}
export function useHallsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HallsQuery, HallsQueryVariables>) {
return Apollo.useLazyQuery<HallsQuery, HallsQueryVariables>(HallsDocument, baseOptions);
}
export type HallsQueryHookResult = ReturnType<typeof useHallsQuery>;
and here's the schema(graphql/hall.graphql):
query Halls {
halls {
id
name
sessions{
id
title
startDate
endDate
}
}
}
Recently I picked up a project that has d3-flame-graph on it and the graph is displayed according to the Filters defined on another component.
My issue is that when searching with new parameters I can't seem to clean the previous chart and I was wondering if someone could help me. Basically what I'm having right now is, when I first enter the page, the loading component, then I have my graph and when I search for a new date I have the loading component but on top of that I still have the previous graph
I figured I could use flamegraph().destroy() on const updateGraph but nothing is happening
import React, { FC, useEffect, useRef, useState, useCallback } from 'react'
import { useParams } from 'react-router-dom'
import moment from 'moment'
import * as d3 from 'd3'
import { flamegraph } from 'd3-flame-graph'
import Filters, { Filter } from '../../../../../../components/Filters'
import { getFlamegraph } from '../../../../../../services/flamegraph'
import { useQueryFilter } from '../../../../../../hooks/filters'
import FlamegraphPlaceholder from '../../../../../../components/Placeholders/Flamegraph'
import css from './flamegraph.module.css'
import ToastContainer, {
useToastContainerMessage,
} from '../../../../../../components/ToastContainer'
const defaultFilters = {
startDate: moment().subtract(1, 'month'),
endDate: moment(),
text: '',
limit: 10,
}
const getOffSet = (divElement: HTMLDivElement | null) => {
if (divElement !== null) {
const padding = 100
const minGraphHeight = 450
// ensure that the graph has a min height
return Math.max(
window.innerHeight - divElement.offsetTop - padding,
minGraphHeight
)
} else {
const fallBackNavigationHeight = 300
return window.innerHeight - fallBackNavigationHeight
}
}
const Flamegraph: FC = () => {
const [queryFilters, setQueryFilters] = useQueryFilter(defaultFilters)
const [fetching, setFetching] = useState(false)
const [graphData, setGraphData] = useState()
const {
messages: toastMessages,
addMessage: addMessageToContainer,
removeMessage: removeMessageFromContainer,
} = useToastContainerMessage()
const flameContainerRef = useRef<HTMLDivElement | null>(null)
const flameRef = useRef<HTMLDivElement | null>(null)
const graphRef = useRef<any>()
const graphDataRef = useRef<any>()
const timerRef = useRef<any>()
const { projectId, functionId } = useParams()
let [sourceId, sourceLine] = ['', '']
if (functionId) {
;[sourceId, sourceLine] = functionId.split(':')
}
const createGraph = () => {
if (flameContainerRef.current && flameRef.current) {
graphRef.current = flamegraph()
.width(flameContainerRef.current.offsetWidth)
.height(getOffSet(flameRef.current))
.cellHeight(30)
.tooltip(false)
.setColorMapper(function(d, originalColor) {
// Scale green component proportionally to box width (=> the wider the redder)
let greenHex = (192 - Math.round((d.x1 - d.x0) * 128)).toString(16)
return '#FF' + ('0' + greenHex).slice(-2) + '00'
})
}
}
const updateGraph = (newData: any) => {
setGraphData(newData)
graphDataRef.current = newData
if (graphRef.current) {
if (newData === null) {
graphRef.current.destroy()
graphRef.current = null
} else {
d3.select(flameRef.current)
.datum(newData)
.call(graphRef.current)
}
}
}
const fetchGraph = (filters: Filter) => {
setFetching(true)
getFlamegraph(
Number(projectId),
filters.startDate ? filters.startDate.unix() : 0,
filters.endDate ? filters.endDate.unix() : 0,
sourceId,
sourceLine
)
.then(graphData => {
if (!graphRef.current) {
createGraph()
}
updateGraph(graphData)
})
.catch(({ response }) => {
updateGraph(null)
if (response.data) {
addMessageToContainer(response.data.message, true)
}
})
.finally(() => {
setFetching(false)
})
}
const onResize = useCallback(() => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => {
if (graphRef.current && flameContainerRef.current) {
graphRef.current.width(flameContainerRef.current.offsetWidth)
d3.select(flameRef.current)
.datum(graphDataRef.current)
.call(graphRef.current)
}
}, 500)
}, [])
useEffect(() => {
fetchGraph(queryFilters)
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onChangeFilters = (filters: Filter) => {
setQueryFilters(filters)
fetchGraph(filters)
}
return (
<div className={css.host}>
<Filters
defaultValues={queryFilters}
searching={fetching}
onSearch={onChangeFilters}
/>
<div className={css.flameBox}>
<div className={css.flameContainer} ref={flameContainerRef}>
<div ref={flameRef} />
</div>
{fetching || !graphData ? (
<FlamegraphPlaceholder loading={fetching} />
) : null}
</div>
<ToastContainer
messages={toastMessages}
toastDismissed={removeMessageFromContainer}
/>
</div>
)
}
export default Flamegraph
Firstly, flamegraph() creates a new instance of flamegraph, you'd need to use graphref.current.destroy(). Secondly, you'd want to destroy this not when the data has already been loaded, but just as it starts to load, right? Because that's the operation that takes time.
Consider the following:
const cleanGraph = () => {
if (graphref.current !== undefined) {
graphref.current.destroy()
}
}
const fetchGraph = (filters: Filter) => {
setFetching(true)
cleanGraph()
getFlamegraph(
Number(projectId),
filters.startDate ? filters.startDate.unix() : 0,
filters.endDate ? filters.endDate.unix() : 0,
sourceId,
sourceLine
)
...
}