I used the useState of react to deal with "hooks" (is the name of it?)
const [users, setUsers] = useState(null);
In the next piece of code I use the setUsers but dosnt do it...
getUsersApi(queryString.stringify(params)).then(response => {
console.log(params.page)
// eslint-disable-next-line eqeqeq
if(params.page == "1"){
console.log(response)//line33
if(isEmpty(response)){
setUsers([]);
}else{
setUsers(response);
console.log(users);//line38
}
}else{
if(!response){
setBtnLoading(0);
}else{
setUsers({...users, ...response});
setBtnLoading(false);
}
}
})
I inserted a console.log inside of it and apparently pass through there, but dosnt set users...
Here is the function getUsersApi() in case you need it.
export function getUsersApi(paramsUrl){
console.log(paramsUrl)
const url = `${API_HOST}/users?${paramsUrl}`
const params = {
headers:{
Authorization: `Bearer${getTokenApi()}`,
},
};
return fetch(url, params).then(response => {
return response.json();
}).then(result => {
return result;
}).catch(err => {
return err;
});
}
Here is the function isEmpty() in case you need it.
function isEmpty(value) {
if (value == null) {
return true;
}
if (isArrayLike(value) &&
(isArray(value) || typeof value == 'string' || typeof value.splice == 'function' ||
isBuffer(value) || isTypedArray(value) || isArguments(value))) {
return !value.length;
}
var tag = getTag(value);
if (tag == mapTag || tag == setTag) {
return !value.size;
}
if (isPrototype(value)) {
return !baseKeys(value).length;
}
for (var key in value) {
if (hasOwnProperty.call(value, key)) {
return false;
}
}
return true;
}
Thanks a lot guys!!
If you want to know if the state has been updated, be sure to set a useEffect hook with the state inside the dependency array.
import React, { useEffect, useState } from 'react';
useEffect(() => {
console.log("Users: ", users)
}, [users])
If this gets logged, users have been sucessfully updated.
If this does NOT gets logged, you're not entering the else which calls setUsers
Related
here is what the static method looks like, I want to change the class component to a functional component but functional components don't support it. I am still learning, any advice will be appreciated
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.security.validToken) {
setTimeout(() => {
const product_type = localStorage.getItem("types");
if (product_type === "imp") {
nextProps.history.push("/imported");
} else if (product_type === "branded") {
nextProps.history.push("/brands");
} else if (product_type === "imp,branded" || product_type === "branded,imp") {
nextProps.history.push("/imported-brands");
} else if (product_type === "local") {
nextProps.history.push("/local");
}
}, 1000);
}
Would be easier if you could share the whole component, or at least how/where you plan to manage your functional component's state. As said on this anwser:
This has a number of ways that it can be done, but the best way is situational.
Let's see how we could create a ProductsRouter functional component.
import React from 'react'
import { useHistory } from 'react-router'
export const ProductsRouter = ({ security }) => {
// our page state
const [productType, setProductType] = React.useState('')
// history object instance
const history = useHistory()
// our routing effect
React.useEffect(() => {
// if not authenticated, do nothing
if (!security.validToken) return
// update our state
setProductType(localStorage.getItem('types'))
// timeout function
const timer = setTimeout(() => {
if (productType === 'imp') {
history.push('/imported')
} else if (productType === 'branded') {
history.push('/brands')
} else if (productType === 'imp,branded' || productType === 'branded,imp') {
history.push('/imported-brands')
} else if (productType === 'local') {
history.push('/local')
}
}, 1000)
return () => clearTimeout(timer)
}, [])
return <pre>{JSON.stringify({ history }, null, 2)}</pre>
}
We could use a map to keep our code tidy and flexible. If there's no particular reason to have a 1000ms timeout, we could also call push instantly, as long as we have a validToken.
const typesMap = new Map([
['imp', '/imported'],
['branded', '/brands'],
['imp,branded', '/imported-brands'],
['branded,imp', '/imported-brands'],
['local', '/local'],
])
export const ProductsRouter = ({ security }) => {
const history = useHistory()
React.useEffect(() => {
if (!security.validToken) return
history.push(typesMap.get(localStorage.getItem('types')))
return () => {}
}, [security.validToken])
return <pre>{JSON.stringify({ history }, null, 2)}</pre>
}
Hope that helps! Cheers
I'm building an tic-tac-toe app in React JS, but useEffect in react behaving pretty weired for me.
this is full project url: https://github.com/vyshnav4u/react-tic-tac-toe
Snippet Having problem
You can see that i'm calling isWinner function inside useEffect, in winning condition is meet, isWinner should be print "finish" on console and return true. Now it's printing "finish" on console correctly but not return or printing true inside useEffect.
useEffect(() => {
if (currentSquare >= 0) {
let winFlag = isWinner(currentSquare);
// this should print true when winning condition is meet,
// but it's always printing false
console.log(winFlag);
if (winFlag) {
alert("won");
}
}
}, [currentSquare, value]);
//isWinner function
const isWinner = (currentIndex) => {
pattern.forEach((singlePattern, index) => {
if (singlePattern.includes(currentIndex)) {
let tempIndex_0 = singlePattern[0];
let tempIndex_1 = singlePattern[1];
let tempIndex_2 = singlePattern[2];
if (
value[tempIndex_0] == value[tempIndex_1] &&
value[tempIndex_1] == value[tempIndex_2]
) {
console.log("finish");
return true;
}
}
});
return false;
};
Edit:
The issue was sorted, it was not a problem caused by useEffect but with the incorrect use of forEach in isWinner function please see the verified answer.
Array.prototype.forEach is a void return. The return true isn't a return for the isWinner callback, it's only a return for the .forEach callback.
If I'm understanding the purpose of the isWinner function you want to return true|false if the wining condition is met or not.
You could keep an isWon variable initialized false and only set true upon the condition.
const isWinner = (currentIndex) => {
let isWon = false;
pattern.forEach((singlePattern, index) => {
if (singlePattern.includes(currentIndex)) {
const tempIndex_0 = singlePattern[0];
const tempIndex_1 = singlePattern[1];
const tempIndex_2 = singlePattern[2];
if (
value[tempIndex_0] == value[tempIndex_1] &&
value[tempIndex_1] == value[tempIndex_2]
) {
console.log("finish");
isWon = true;
}
}
});
return isWon;
};
Or refactor the code to be a little more functional.
const isWinner = (currentIndex) => {
return pattern.some((singlePattern, index) => {
if (singlePattern.includes(currentIndex)) {
const [tempIndex_0, tempIndex_1, tempIndex_2] = singlePattern;
if (
value[tempIndex_0] == value[tempIndex_1] &&
value[tempIndex_1] == value[tempIndex_2]
) {
console.log("finish");
return true;
}
}
return false;
});
};
It looks like you're returning within the callback of the forEach. The loop will break. However, you will still go to the next line in the isWinner function and return false.
Putting a console.log right before return false; will show you this.
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.
I've been seen many developers(including me) doing some conditional rendering checking the variables value like this:
import React, { useState, useMemo } from 'react'
const Page = () => {
const [data, setData] = useState<any[]>()
if (!data) return 'Loading...'
if (data.length === 0) return 'Empty'
if (data && data.length > 0) {
return data.map(item => item)
}
return null
}
But, I don't think it is explicit enough about the page status and about what it should render. So I've been thinking if there is a way of doing something like this:
import React, { useState, useMemo } from 'react'
enum PageStatus {
loading,
empty,
withResults
}
const Page = () => {
const [data, setData] = useState<any[]>()
const status = useMemo(() => {
if (!data) {
return PageStatus.loading
}
if (data.length > 0) {
return PageStatus.withResults
}
return PageStatus.empty
}, [data])
if (status === PageStatus.empty) {
return 'Empty'
}
if (status === PageStatus.withResults) {
return data.map(item => item)
}
return null
}
Unfortunately, Typescript does not recognise the right type for data in the last expression. You can play around and see the error here in the TS Playground. Maybe, there is a way to cast/bind a variable during the run time but I didn't find anything related to this.
So, do you know if that is possible to have conditional renderings oriented to states/status instead of individual variable values? Or even a better way? I appreciate your help, thanks.
The issue here is that TypeScript has no way of relating the correct status to what the results of the function may be. What we need is a union type that equates the status type to what data is actually returned.
import React, { useState, useMemo } from 'react'
enum PageStatus {
loading,
empty,
withResults
}
type StatusData<T> = {
status: PageStatus.loading,
data: undefined
} | {
status: PageStatus.empty,
data: []
} | {
status: PageStatus.withResults,
data: T[];
}
const Page = () => {
const [data, setData] = useState<string[]>(); // just assuming string here
const statusData = useMemo((): StatusData<string> => {
if (!data) {
return {
status: PageStatus.loading,
data
}
}
if (data.length > 0) {
return {
status: PageStatus.withResults,
data
}
}
return {
status: PageStatus.empty,
data: [],
}
}, [data]);
if (statusData.status === PageStatus.empty) {
return 'Empty'
}
if (statusData.status === PageStatus.withResults) {
return statusData.data.map(item => item)
}
return null
}
Playground
You are not initializing with any data, so the type inferred will be any[] | undefined. Rather than using type any, it would be better to use a type and also initialize with some data. (possibly and empty array)
import React, { useState, useMemo } from 'react'
enum PageStatus {
loading,
empty,
withResults
}
interface dummy {
name: string
}
const Page = () => {
const [data, setData] = useState<dummy[]>([])
const status = useMemo(() => {
if (!data) {
return PageStatus.loading
}
if (data.length > 0) {
return PageStatus.withResults
}
return PageStatus.empty
}, [data])
if (status === PageStatus.empty) {
return 'Empty'
}
if (status === PageStatus.withResults) {
return data.map(item => item)
}
return null
}
Link to playground
I think your problem is you are relying on Typescript to recognize that data is defined off of a function which accepts a function as a parameter that defines data (useMemo).
And that function in the first parameter which defines data is not run in a non react environment - it does not have () after it. We know it is run by the framework because we trust useMemo, but Typescript doesn't. Typescript has no awareness on useMemo and the dependencies, so there is no way for it to know it is defined... unless it did some deep code diving which I think is beyond the scope.
This is one of the few times I'll use data!.map(. I try to avoid it as much as possible, but framework magic is a place I'll use it.
Component will preserve state even after unmounting it
I'm building a feedback form with Formik and want to move from class components to hooks but face mentioned difficulties.
function Feedback(props) {
const [fileInfo, setFileInfo] = useState("");
const [feedbackStatus, setFeedbackStatus] = useState("");
let timer = null;
useEffect(() => {
const status = props.feedbackResponse.status;
if (status) {
if (status >= 200 && status < 300) {
setFeedbackStatus("success");
timer = setTimeout(() => {
props.history.goBack();
}, 2500);
} else if (status === "pending") {
setFeedbackStatus("pending");
} else {
setFeedbackStatus("error");
}
}
return () => {
clearInterval(timer);
}
}, [props.feedbackResponse.status]);
// ...code ommited for brevity
}
This effect runs succesfully after my form submission while waiting for a server response. Feedback component is a react-router modal component, if it matters. However, if I re-open that modal, I see a success message instead of a new form. In my return I am conditionally rendering a success message if feedbackStatus === "success" or a form that, depending on a server response, might display an error message otherwise. My class component works fine with this code:
componentDidUpdate(prevProps) {
const status = this.props.feedbackResponse.status;
if (prevProps.feedbackResponse.status !== status) {
if (status >= 200 && status < 300) {
this.setState({feedbackStatus: "success"});
this.timer = setTimeout(() => {
this.props.history.goBack();
}, 2500);
} else if (status === "pending") {
this.setState({feedbackStatus: "pending"});
} else {
this.setState({feedbackStatus: "error"});
};
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
Expected output: reopening this modal component after a successful form submit should render a new form but it renders a previous' submit status. This leads me to think that I'm not unmounting my Feedback component at all but where's my mistake then?
You can use <Feedback key={someKey} />.
This will ensure that a new instance of Feedback component is made when you re-open it, thus your old success/failure messages will be erased from the state.
The above behaviour happens because the effect is run on initial render as well and in that case props.feedbackStatus might be preserved from the previous instances.
Since you only wish to execute the effect when the component updates, you would need to stop execution of useEffect on initial render which happens even when you pass values to the dependency array. You can do that using useRef
function Feedback(props) {
const [fileInfo, setFileInfo] = useState("");
const [feedbackStatus, setFeedbackStatus] = useState("");
const isInitialRender = useRef(true);
let timer = null;
useEffect(() => {
if(isInitialRender.current === true) {
isInitialRender.current = false;
} else {
const status = props.feedbackResponse.status;
if (status) {
if (status >= 200 && status < 300) {
setFeedbackStatus("success");
timer = setTimeout(() => {
props.history.goBack();
}, 2500);
} else if (status === "pending") {
setFeedbackStatus("pending");
} else {
setFeedbackStatus("error");
}
}
}
return () => {
clearInterval(timer);
}
}, [props.feedbackResponse.status]);
}