I have a custom React hook something like this:
export default function useLocations(locationsToMatch) {
const state = useAnotherHookToGetStateFromStore();
const { allStores } = state.locations;
const allLocations = {};
allStores.forEach((store) => {
const { locationId, locationType } = store;
const isLocationPresent = locationsToMatch.indexOf(locationId) !== -1;
if (isLocationPresent && locationType === 'someValue') {
allLocations[locationId] = true;
} else {
allLocations[locationId] = false;
}
});
return allLocations;
}
When I use above hook inside my React component like this:
const locations = useLocations([908, 203, 678]) // pass location ids
I get a max call depth error due to infinite rendering. This is because I have some code inside my component which uses useEffect hook like this:
useEffect(() => { // some code to re-render component on change of locations
}, [locations])
So I tried to wrap my return value in useLocations hook inside a useMemo like this:
export default function useLocations(locationsToMatch) {
const state = useAnotherHookToGetStateFromStore();
const { allStores } = state.locations;
const allLocations = {};
const getStores = () => {
allStores.forEach((store) => {
const { locationId, locationType } = store;
const isLocationPresent = locationsToMatch.indexOf(locationId) !== -1;
if (isLocationPresent && locationType === 'someValue') {
allLocations[locationId] = true;
} else {
allLocations[locationId] = false;
}
});
return allLocations;
};
return useMemo(() => getStores(), [locationsToMatch, state]);
}
But this still causes infinite re-rendering of the consuming component. So how can I return a memoized value from my custom hook useLocations to prevent infinite re-rendering?
Related
The function "AdicionaItem" not reload the map of component "Items.tsx" why?
<Itens.tsx>
const { transacao } = useContext(TransacaoContext);
return (
transacao.itens.map(({descricao}: TransacaoItens) => (<h1>{descricao}</h1>)
);
<TransacaoContext.tsx>
const [transacao, setTransacao] = useState<Transacao>(transacaoInicial);
function AdicionaItem(item: TransacaoItens) {
let novosValores = transacao;
novosValores.itens = [...novosValores.itens, item];
setTransacao(novosValores);
}
<Consulta.tsx>
const { AdicionaItem } = useContext(TransacaoContext);
function Adiciona(){
AdicionaItem({descricao: "Teste"});
};
Your useState hook is outside of your functional component. Try moving it inside of AdicionaItem:
function AdicionaItem(item: TransacaoItens) {
const [transacao, setTransacao] = useState<Transacao>(transacaoInicial);
let novosValores = transacao;
novosValores.itens = [...novosValores.itens, item];
setTransacao(novosValores);
}
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.
import React, { useState, useEffect, useRef } from 'react';
import styles from './TextAnimation.module.scss';
const TextAnimation = () => {
const [typedText, setTypedText] = useState([
"Welcome to Byc",
"Change your Life"
]);
const [value, setValue] = useState();
const [inType, setInType] = useState(false);
let attachClasses = [styles.Blink];
if(inType) {
attachClasses.push(styles.Typing)
}
const typingDelay = 200;
const erasingDelay = 100;
const newTextDelay = 5000;
let textArrayIndex = 0;
let charIndex = 0;
const type = () => {
if(charIndex < typedText[textArrayIndex].length + 1) {
setValue(typedText[textArrayIndex].substring(0, charIndex));
charIndex ++;
setTime();
} else {
setInType(false);
setTimeout(erase, newTextDelay);
}
};
const setTime = () => {
setTimeout(type, typingDelay);
};
const erase = () => {
if(charIndex > 0) {
setValue(typedText[textArrayIndex].substring(0, charIndex - 1));
charIndex --;
setTimeout(erase, erasingDelay);
} else {
setInType(false);
textArrayIndex ++;
if(textArrayIndex >= typedText.length) {
textArrayIndex = 0;
}
setTimeout(type, newTextDelay - 3100);
}
};
useEffect(() => {
type();
}, [])
return (
<div className={styles.TextAnimation}>
<span className={styles.Text} >{value}</span><span className={attachClasses.join(' ')} > </span>
</div>
);
};
export default TextAnimation;
I'am trying to make text animation, but i got an message just like this...
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
How can i fix it?
You need to clear timeouts when your component unmounts, otherwise maybe a timeout will run after the component is unmounted.
To do that :
store the return value of each timeout in a list in some ref (with React.useRef for example)
return a callback in useEffect that clears the timeouts with clearTimeout(<return value of setTimeout>)
I have a function that I call from a child component callback. I'm trying to access some state variable but variables are undefined. I think the issue is when the child component callback the function context it not bind to the parent component. How to do this.
It is sure that myVariable is set before myFunciton is called.
const MyParentView = props => {
const[myVariable, setMyVariable] = useState(undefined)
const onTextFieldChange = val => {
setMyVariable(val)
}
const myFunction = () => {
// myVariable is set to some value by this time
console.log(myVariable)
// But it logs undefined
}
return (
<Input onChange={e => onTextFieldChange(e.target.value)}
<MyChildComponent getData={()=>myFunction()}/>
)
}
Following is the child component ( The actual one )
// #flow
import React, { useEffect, useRef } from "react"
import { get } from "lodash"
type Props = {
children: any,
getData?: Function,
threshold?: number
}
const InfiniteScroll = ({ children, getData, threshold = 0.9 }: Props) => {
const listRef = useRef()
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
}, [])
useEffect(() => {
if (listRef.current) {
const bottom = listRef.current.getBoundingClientRect().bottom
const height =
window.innerHeight || get(document, "documentElement.clientHeight")
if (bottom <= height) {
getData && getData()
}
}
})
const handleScroll = () => {
const winScroll =
get(document, "body.scrollTop") ||
get(document, "documentElement.scrollTop")
const height =
get(document, "documentElement.scrollHeight") -
get(document, "documentElement.clientHeight")
const scrolled = winScroll / height
if (scrolled >= threshold) {
getData && getData()
}
}
return <div ref={listRef}>{children}</div>
}
export default InfiniteScroll
Try returning a closure in your myFunction like this:
const myFunction = () => {
return function() {
// myVariable is set to some value by this time
console.log(myVariable)
// But it logs undefined
}
}
This is a follow up question to this question:
How to call useDispatch in a callback
I got a React component which needs to receive information from redux in its props. The information is taken using a custom hook.
This is the custom hook:
export function useGetData(selectorFunc)
{
return type =>
{
if(!type)
{
throw new Error("got a wrong type");
}
let myData = selectorFunc(state =>
{
let res = JSON.parse(sessionStorage.getItem(type));
if(!res )
{
res = state.myReducer.myMap.get(type);
}
return res;
});
return myData;
}
}
Based on the answer for the linked question, I tried doing something like this:
function Compo(props)
{
const getDataFunc = useGetData(useSelector);
return <MyComponent dataNeeded = { getDataFunc(NeededType).dataNeeded } />
}
but I get an error because an hook can not be called inside a callback.
How can I fix this issue?
Don't pass the selector, just use it.
Also, according to your logic, you should parse the storage key outside the selector.
export function useDataFunc() {
const myData = useSelector(state => myReducer.myMap);
const getDataFunc = type => {
const resByData = myData.get(type);
try {
// JSON.parse can throw an error!
const res = JSON.parse(sessionStorage.getItem(type));
} catch (e) {
return resByData;
}
return res ? res : resByData;
};
return getDataFunc;
}
function Compo(props) {
const getDataFunc = useDataFunc();
return <MyComponent dataNeeded={getDataFunc(NeededType).dataNeeded} />;
}
I think it should be like,
const myData = useSelector(state => state.myReducer.myMap);