Nextjs/React : Skip useEffect function on particular state change - reactjs

I built chart view page which display sales data.
This chart will updated through REST API when user change dimension, filter value and period.
useEffect(async () => {
let [startDate, endDate] = getReportPeriod({target: { innerText: period}}) // get formatted date string
getReportData(startDate, endDate) // function that call REST API to get chart data
}, [filteredDimensions, dimensions, dimensionTime])
Also, user can save and load chart setting.
const loadCustomReport = async(reportName) => {
let res = await axios.post('/api/customReport/loadReport', {
report_name: reportName
}) // get report setting
let setting = JSON.parse(res.data[0].report_setting) // parse report setting as json
setRevenue(setting.metrics.revenue)
setImpression(setting.metrics.impression)
setClicks(setting.metrics.clicks)
setCPM(setting.metrics.CPM)
setCTR(setting.metrics.CTR)
setDimensions(setting.dimensions)
setFilteredDimensions(setting.filteredDimensions)
}
The problem is the useEffect function run twice(both of setDimensions and setFilteredDimensions) and response times vary, so sometimes I got report data that a setting is not fully applied.
Is there any way to skip useEffect function running on setDimensions?
I think put both dimension setting and filter setting on one state might be works but I just curious that there are other solutions.
Please advise me any suggestion to solve the problem.
#Gabriele Petrioli give me the solution actually I wanted.
#man.in.the.jukebox and #Brandon Pelton-Cox's advice give me a great point.
Currently my code is as below(I changed before I saw #Gabriele Petrioli's answer)
useEffect(async () => {
let [startDate, endDate] = getReportPeriod({target: { innerText: period}})
getReportData(startDate, endDate)
}, [filteredDimensions, dimensionTime])
// button handler that add dimension to current dimensions state
const addDimension = (dimension) => {
setDimensions([
...dimensions,
dimension
])
let [startDate, endDate] = getReportPeriod({target: { innerText: period}})
getReportData(startDate, endDate)
}
// button handler that remove dimension from current dimensions state
const removeDimension = (idx) => {
let tempDimensions = [...dimensions]
tempDimensions.splice(idx, 1)
setDimensions(tempDimensions)
let [startDate, endDate] = getReportPeriod({target: { innerText: period}})
getReportData(startDate, endDate)
}
// load report setting
const loadCustomReport = async(reportName) => {
let res = await axios.post('/api/customReport/loadReport', {
report_name: reportName
})
let setting = JSON.parse(res.data[0].report_setting)
setRevenue(setting.metrics.revenue)
setImpression(setting.metrics.impression)
setClicks(setting.metrics.clicks)
setCPM(setting.metrics.CPM)
setCTR(setting.metrics.CTR)
setDimensions(setting.dimensions)
setFilteredDimensions(setting.filteredDimensions)
}
Thank you all for give me great advices.

Since your state updates are fired from an async function, react does not batch them by default
(applies to React versions before 18, as in 18 all updates are batched regardless of where they originate).
You need to opt-in to unstable_batchedUpdates, so
const loadCustomReport = async(reportName) => {
let res = await axios.post('/api/customReport/loadReport', {
report_name: reportName
}) // get report setting
let setting = JSON.parse(res.data[0].report_setting) // parse report setting as json
React.unstable_batchedUpdates(() => {
setRevenue(setting.metrics.revenue)
setImpression(setting.metrics.impression)
setClicks(setting.metrics.clicks)
setCPM(setting.metrics.CPM)
setCTR(setting.metrics.CTR)
setDimensions(setting.dimensions)
setFilteredDimensions(setting.filteredDimensions)
});
}

By including dimensions in the dependency array for the useEffect function, you are telling React to re-render the page and execute the function in that useEffect block whenever setDimensions is called. Therefore, if you want to prevent that function from being executed when dimensions is updated, you need to remove from the dependencies array, like so
useEffect(async () => {
...
}, [filteredDimensions, dimensionTime])

Related

React useState without undefined values

I'm novice with React and the state concept. So I fetch data from my database with axios. I put everything in a state (accomodation) and then I need to catch a field (country) into datas and use it into other function.
I succeed to catch everything but when I try to test it with a 'console.log' it appears that the two first result returns empty/undefined before having a result. Because the fact the two first attempts are empty/undefined, the other function doesn't work.
Anyone could help me please :)
Here are my code :
const { id } = useParams()
const [accomodation, setAccomodation] = useState('')
const getAcc = async () => {
const data = await axios.get(
`${process.env.REACT_APP_API_URL}api/v1/accomodations/${id}`
)
setAccomodation(data.data.data.accomodation)
}
useEffect(() => {
getAcc()
}, [])
const country = accomodation.country
console.log(country)
To go over what is happening here:
const [accomodation, setAccomodation] = useState('') the accomodation object is set to empty string.
useEffect(() => { getAcc() }, []) getAcc() is called upon render.
const country = accomodation.country; console.log(country) we are still waiting for getAcc() to finish, meanwhile, country is set to the 'country' property of an empty string which is underfined. undefined is printed.
setAccomodation(data.data.data.accomodation) getAcc() finally finishes, and accommodation is hopefully set to the object you intended.
What happens now?.. Nothing. Because you have not done anything to act on a state update. So one of the answers already posted here is on the right track. You need to act on the update of accommodation state.
This is how you trigger an effect on state update:
useEffect(() => {
if (accommodation.country) {
console.log(accomodation.country);
}
}, [accomodation])
Note that this useEffect() will be invoked whenever accommodation changes from its previous state. If the desired effect is not achieved it may be that accommodation is not the object you intended.

React state is empty inside useEffect

Currently I'm building a pusher chat app with react. I'm trying to keep a list of online users. I'm using this code below:
const [users, setUsers] = useState([]);
useEffect(() => { // UseEffect so only called at first time render
window.Echo.join("server.0")
.here((allUsers) => {
let addUsers = [];
allUsers.map((u) => {
addUsers.push(u.name)
})
setUsers(addUsers);
})
.joining((user) => {
console.log(`User ${user.name} joined`);
setUsers([users, user]);
})
.leaving((user) => {
console.log(`User ${user.name} left`);
let addUsers = users;
addUsers.filter((u) => {
return u !== user.name;
})
setUsers(addUsers);
})}, []);
Whenever I subscribe to the pusher channel, I receive the users that are currently subscribed and the state is set correctly. All subscribed users are showing. However when a new user joins/leaves, the .joining/.leaving method is called and the users state is empty when I console log it. This way the users state is being set to only the newly added user and all other users are being ignored. I'm new to react so there is probably a simple explanation for this. I was not able to find the answer myself tough. I would really appreciate your help.
I saw the problem in joining. You need to update setState like this: setUsers([...users, user.name]);
And leaving also need to update:
const addUsers = users.filter((u) => {
return u !== user.name;
});
setUsers(addUsers);
here should also rewrite:
let addUsers = allUsers.map((u) => u.name);
setUsers(addUsers);
I found the issue. The problem is that when accessing state from within a callback funtion, it always returns the initial value. In my case an empty array. It does work when using a reference variable. I added the following lines:
const [users, _setUsers] = useState([]);
const usersRef = React.useRef(users);
const setUsers = data => {
usersRef.current = data;
_setUsers(data);
}
Each time I update the users state, I use the setUsers function. Now when I acces the state from inside my callback function with usersRef.current I get the latest state.
Also I used the code from the answer of #Viet to update the values correctly.

React Hooks: Referencing data that is stored inside context from inside useEffect()

I have a large JSON blob stored inside my Context that I can then make references to using jsonpath (https://www.npmjs.com/package/jsonpath)
How would I go about being able to access the context from inside useEffect() without having to add my context variable as a dependency (the context is updated at other places in the application)?
export default function JsonRpc({ task, dispatch }) {
const { data } = useContext(DataContext);
const [fetchData, setFetchData] = useState(null);
useEffect(() => {
task.keys.forEach(key => {
let val = jp.query(data, key.key)[0];
jp.value(task.payload, key.result_key, val);
});
let newPayload = {
jsonrpc: "2.0",
method: "call",
params: task.payload,
id: "1"
};
const domain = process.env.REACT_APP_WF_SERVER;
let params = {};
if (task.method === "GET") {
params = newPayload;
}
const domain_params =
JSON.parse(localStorage.getItem("domain_params")) || [];
domain_params.forEach(e => {
if (e.domain === domain) {
params[e.param] = e.value;
}
});
setFetchData({ ...task, payload: newPayload, params: params });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [task]);
}
I'm gonna need to post an answer because of code, but I'm not 100% sure about what you need, so I'll build a correct answer with your feedback :)
So, my first idea is: can't you split your effects in two React.useEffect? Something like this:
export default function JsonRpc({ task, dispatch }) {
...
useEffect(() => {
...
setFetchData(...);
}, [task]);
useEffect(() => {
...
}, [data]);
..
}
Now, if my understanding are correct, this is an example of events timeline:
Due to the update on task you will trigger the first useEffect, which can setFetchData();
Due to the update on fetchData, and AXIOS call is made, which updates data (property in the context);
At this, you enter the second useEffect, where you have the updated data, but NO call to setFetchData(), thus no loop;
Then, if you wanted (but couldn't) put data in the dependencies array of your useEffect, I can imagine the two useEffect I wrote have some shared code: you can write a common method called by both useEffects, BUT it's important that the setFetchData() call is outside this common method.
Let me know if you need more elaboration.
thanks for your reply #Jolly! I found a work around:
I moved the data lookup to a state initial calculation:
const [fetchData] = useState(processFetchData(task, data));
then im just making sure i clear the component after the axios call has been made by executing a complete function passed to the component from its parent.
This works for now, but if you have any other suggestions id love to hear them!

React hook missing dependency

I'm hoping someone can explain to me the correct usage of React hook in this instance, as I can't seem to find away around it.
The following is my code
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// This is a trick so that the debounce doesn't run on initial page load
// we use a ref, and set it to true, then set it to false after
const firstUpdate = React.useRef(true);
const UserSearchTimer = React.useRef()
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
function _debounceSearch() {
clearTimeout(UserSearchTimer.current);
UserSearchTimer.current = setTimeout( async () => {
_getUsers();
}, DEBOUNCE_TIMER);
}
async function _getUsers(query = {}) {
if(type) query.type = type;
if(search) query.search = search;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
So essentially I have a table in which i am displaying users, when the page changes, or the perPage, or the order, or the type changes, i want to requery my user list so i have a useEffect for that case.
Now generally I would put the _getUsers() function into that useEffect, but the only problem is that i have another useEffect which is used for when my user starts searching in the searchbox.
I don't want to requery my user list with each and every single letter my user types into the box, but instead I want to use a debouncer that will fire after the user has stopped typing.
So naturally i would create a useEffect, that would watch the value search, everytime search changes, i would call my _debounceSearch function.
Now my problem is that i can't seem to get rid of the React dependency warning because i'm missing _getUsers function in my first useEffect dependencies, which is being used by my _debounceSearch fn, and in my second useEffect i'm missing _debounceSearch in my second useEffect dependencies.
How could i rewrite this the "correct" way, so that I won't end up with React warning about missing dependencies?
Thanks in advance!
I would setup a state variable to hold debounced search string, and use it in effect for fetching users.
Assuming your component gets the query params as props, it would something like this:
function Component({page, perPage, order, type, search}) {
const [debouncedSearch, setDebouncedSearch] = useState(search);
const debounceTimer = useRef(null);
// debounce
useEffect(() => {
if(debounceTime.current) {
clearTimeout(UserSearchTimer.current);
}
debounceTime.current = setTimeout(() => setDebouncedSearch(search), DEBOUNCE_DELAY);
}, [search]);
// fetch
useEffect(() => {
async function _getUsers(query = {}) {
if(type) query.type = type;
if(debouncedSearch) query.search = debouncedSearch;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
_getUsers();
}, [page, perPage, order, type, debouncedSearch]);
}
On initial render, debounce effect will setup a debounce timer... but it is okay.
After debounce delay, it will set deboucedSearch state to same value.
As deboucedSearch has not changed, ferch effect will not run, so no wasted fetch.
Subsequently, on change of any query param except search, fetch effect will run immediately.
On change of search param, fetch effect will run after debouncing.
Ideally though, debouncing should be done at <input /> of search param.
Small issue with doing debouncing in fetching component is that every change in search will go through debouncing, even if it is happening through means other than typing in text box, say e.g. clicking on links of pre-configured searches.
The rule around hook dependencies is pretty simple and straight forward: if the hook function use or refer to any variables from the scope of the component, you should consider to add it into the dependency list (https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies).
With your code, there are couple of things you should be aware of:
1.With the first _getUsers useEffect:
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// Correctly it should be:
useEffect(() => {
_getUsers()
}, [_getUsers])
Also, your _getUsers function is currently recreated every single time the component is rerendered, you can consider to use React.useCallback to memoize it.
2.The second useEffect
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
// Correctly it should be
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [firstUpdate, _debounceSearch])

react hooks (useEffect) infinite loop clarification

This question is related to this previous question of mine. I have written two custom hooks. The first is useFetchAccounts whose job is to fetch some data about user accounts and looks like this:
// a fake fetch for the sake of the example
function my_fetch(f, delay) {
setTimeout(f, delay)
}
function useFetchAccounts() {
const [data, setData] = useState({accounts: [], loaded: false})
useEffect(() => {
my_fetch(() => {setData({accounts: [{id:1},{id:2}], loaded: true})}, 3000)
}, [])
return data
}
Nothing special about this hook, simply fetches the ids of the accounts.
Now I have another hook, useFetchBalancesWithIDs(account_ids, accounts_loaded), which is meant to take the ids from the previous step and fetch the balances of these accounts if the accounts have been loaded. It looks like this:
function useFetchBalanceWithIDs(account_ids, accounts_loaded) {
const [balances, setBalances] = useState(null)
useEffect(() => {
if (!accounts_loaded) return; // only fetch if accounts are loaded
my_fetch(() => {
setBalances(account_ids.map(id => 42+id))
}, 3000)
}, [account_ids, accounts_loaded])
return balances
}
As you can see if the accounts_loaded is false, it will not perform the fetch. Together I am using them as follows:
function App() {
const account_data = useFetchAccounts()
const accounts = account_data.accounts
const account_ids = accounts.map(account => account.id) // extract ids
const balance = useFetchBalanceWithIDs(account_ids, account_data.loaded)
console.log(balance)
return null
}
Unfortunately, this results in an infinite loop. What does work is changing the useFetchBalancesWithIDs to this:
function useFetchBalanceWithAccounts(accounts, accounts_loaded) {
const [balances, setBalances] = useState(null)
useEffect(() => {
if (!accounts_loaded) return; // only fetch if accounts are loaded
my_fetch(() => {
setBalances(accounts.map(account => 42+account.id))
}, 3000)
}, [accounts, accounts_loaded])
return balances
}
which performs the ids extraction within. I'm using it like this:
function App() {
const account_data = useFetchAccounts()
const accounts = account_data.accounts
const balance = useFetchBalanceWithAccounts(accounts, account_data.loaded)
console.log(balance)
return null
}
which runs just fine. So the problem seems to be related to the extraction of the IDs from within the App component, which seems to be triggering the useFetchBalanceWithIDs all the time, even though the account_ids do not change value. Can someone help explain this behaviour please? It's OK for me to use useFetchBalanceWithAccounts but I would like to understand why useFetchBalanceWithIDs doens't work. Thank you!
The issue is that you're using the map function to get an array of your account ids. React uses Object.is to determine if your values in the dependency array has changed in each render cycle. The map function returns a new array, so even though the values in your array are the same, the comparison check evaluates to false, and the effect is run.
Referential equality is used by useEffect, that means that [1,2,3] would not equal [1,2,3]
const data = [{id:1}];
data.map(d=>d.id)===data.map(d=>d.id);//is false
One way to prevent the array from id's from changing reference is combining useState with useEffect in App:
function App() {
//store account id's in app state
const [accountIds, setAccountIds] = useState([]);
const account_data = useFetchAccounts();
const accounts = account_data.accounts;
//only if accounts change re set the id's
useEffect(() => {
setAccountIds(accounts.map(a => a.id));
}, [accounts]);
const balance = useFetchBalanceWithAccounts(
accountIds,
account_data.loaded
);
console.log(balance);
return null;
}

Resources