SWR: How do you use the compare option in the SWR hook? - javascript

According to the documentation of SWR: React Hooks for Data Fetching, there is an options object which can be passed in for more control of the hook.
The documentation mentions a compare option:
compare(a, b): comparison function used to detect when returned data
has changed, to avoid spurious rerenders. By default, dequal is used.
Is it something like this?
export function useUser(shouldAPIBeCalled = false) {
const { data: user, error, mutate } = useSWR(() => shouldAPIBeCalled ? '/api/user' : null, fetcher, {
compare: (_old, _new) => {
return dequal(_old, _new);
}
})
Essentially I don't want the fetcher to be called if we haven't changed the original object in my case:
If the user is still...
user: null // don't make that call again
Don't make another call because it hasn't changed!
What’s happening to me is the hook keeps firing.
I only have the hook in a Layout component to show hide navigation elements if we have a user or not,
In the submit function of the login to get the mutate function to update the user,
And in the Auth component (which wraps certain pages) to check if a user is authorized.
Thanks in advance!

Related

Filtering Table data with useEffect Hook causing component to re-render infinitely

I'm trying to use a search bar component to dynamically filter the content of a table that's being populated by API requests, however when I use this implementation the component re-renders infinitely and repeatedly sends the same API requests.
The useEffect() Hook:
React.useEffect(() => {
const filteredRows = rows.filter((row) => {
return row.name.toLowerCase().includes(search.toLowerCase());
});
if (filteredRows !== rows){
setRows(filteredRows);
}
}, [rows, search]);
Is there something I've missed in this implementation that would cause this to re-render infinitely?
Edit 1:
For further context, adding in relevant segments of code from that reference this component which might cause the same behaviour.
Function inside the parent component that renders the table which calls my API through a webHelpers library I wrote to ease API request use.
function fetchUsers() {
webHelpers.get('/api/workers', environment, "api", token, (data: any) => {
if (data == undefined || data == null || data.status != undefined) {
console.log('bad fetch call');
}
else {
setLoaded(true);
setUsers(data);
console.log(users);
}
});
}
fetchUsers();
Edit 2:
Steps taken so far to attempt to fix this issue, edited the hook according to comments:
React.useEffect(() => {
setRows((oldRows) => oldRows.filter((row) => {
return row.name.toLowerCase().includes(search.toLowerCase());
}));
}, [search]);
Edit 3:
Solution found, I've marked the answer by #Dharmik pointing out how Effect calls are managed as this caused me to investigate the parent components and find out what was causing the component to re-render repeatedly. As it turns out, there was a useEffect hook running repeatedly by a parent element which re-rendered the page and caused a loop of renders and API calls. My solution was to remove this hook and the sub-components continued rendering as they should without loops.
It is happening because you've added rows to useEffect dependency array and when someone enters something into search bar, The rows get filtered and rows are constantly updating.
And because of that useEffect is getting called again and again. Remove rows from the useEffect dependency array and it should work fine.
I would like to complement Dharmik answer. Dependencies should stay exhaustive (React team recomendation). I think a mistake is that filteredRows !== rows uses reference equality. But rows.filter(...) returns a new reference. So you can use some kind of deep equality check or in my opinion better somethink like:
React.useEffect(() => {
setRows((oldRows) => oldRows.filter((row) => {
return row.name.toLowerCase().includes(search.toLowerCase());
}));
}, [search]);

Returning a value from an Async Function. AWS/React

I'm trying to build a component that retrieves a full list of users from Amazon AWS/Amplify, and displays said results in a table via a map function. All good so far.
However, for the 4th column, I need to call a second function to check if the user is part of any groups. I've tested the function as a button/onClick event - and it works (console.logging the output). But calling it directly when rendering the table data doesn't return anything.
Here is what I've included in my return statement (within the map function)
<td>={getUserGroups(user.email)}</td>
Which then calls this function:
const getUserGroups = async (user) => {
const userGroup = await cognitoIdentityServiceProvider.adminListGroupsForUser(
{
UserPoolId: '**Removed**',
Username: user,
},
(err, data) => {
if (!data.Groups.length) {
return 'No';
} else {
return 'Yes';
}
}
);
};
Can anyone advise? Many thanks in advance if so!
Because you should never do that! Check this React doc for better understanding of how and where you should make AJAX calls.
There are multiple ways, how you can solve your issue. For instance, add user groups (or whatever you need to get from the backend) as a state, and then call the backend and then update that state with a response and then React will re-render your component accordingly.
Example with hooks, but it's just to explain the idea:
const [groups, setGroups] = useState(null); // here you will keep what "await cognitoIdentityServiceProvider.adminListGroupsForUser()" returns
useEffect(() => {}, [
// here you will call the backend and when you have the response
// you set it as a state for this component
setGroups(/* data from response */);
]);
And your component (column, whatever) should use groups:
<td>{/* here you will do whatever you need to do with groups */}</td>
For class components you will use lifecycle methods to achieve this (it's all in the documentation - link above).

How to use useEffect deps when page reload?

I got simple blog with arficles, and when user click edit button he got form filled with articles data - title, description, body and tags. I use useEffect to get data and fill form, when I got "id". If there is no "id" form should be blank. here is my useEffect:
useEffect(() => {
if (id) {
isLoading = true;
return props.onLoad(userService.articles.get(id));
}
props.onLoad(null);
}, [id]
);
but when I reload page id not changed, and func userService.articles.get(id) not run, and all datas gone. I need advice how to fix it? may be I need to use other deps for useEffect, but now I have no idea what deps i can use exept id.
upd:
thank you all for help. all i want is:
when the edit page load/reload and "id" exist -> fills form fields
when "id" not exist -> blank form fields
now when I reload edit page i got id - but all datas gone, and i got blank form :(
Here is the full code: codesandbox
p.s. i use free API - so you can create user in a sec with any imagined email, username and password. you don't need mail confirmation.
You should use this.props.match.params to access your id that comes from the url.
useEffect(() => {
if (props.match.params.id) {
setIsloading(true);
userService.articles.get(props.match.params.id)
.then((resp) => {
setIsloading(false);
props.onLoad(resp)
})
} else {
props.onLoad(null);
}
}, [props.match.params.id]);
Also you should rely on useState to manage your isLoading variable.
const [isLoading, setIsloading] = useState(false);
I did a bit more digging into the code you have provided.
The initialValues will be first empty because the data coming from the props is not there yet. And once the initialValues have been set you can't change them dynamically, you have to resort to the antd Form api.
You cannot set value for each form control via value or defaultValue
prop, you should set default value with initialValues of Form. Note that initialValues cannot be updated by setState dynamically, you
should use setFieldsValue in that situation.
The key here is to use another useEffect with dependencies to your form values comming from the props and use those to reset the form values via setFieldsValue.
try to useEffect without options and it will run just when the page loads for the first time
useEffect(() => {
if (id) {
isLoading = true;
return props.onLoad(userService.articles.get(id));
}
props.onLoad(null);
}, []
);
Based on the assumption that you want props.onLoad to run whenever there is a defined "id" or the defined "id" changes:
Returning a function from a useEffect hook (as you do with return props.onLoad(...)) is specifically for "cleaning up" things like side effects or subscriptions. A function returned inside a useEffect hook will only run when the component unmounts. See docs here. Also it doesn't seem like you are even passing a function to run on cleanup. You're passing the result of props.onLoad to run on cleanup, which based on the function name doesn't seem like it is intended to return another function.
So, if you want props.onLoad() to run if the "id" is defined, remove the return before props.onLoad. That return is telling React to hold (what it thinks is a function) for cleanup on unmount. If it's still not working, I think we'll need more information on what exactly props.onLoad is doing.

How to reduce the number of times useEffect is called?

Google's lighthouse tool gave my app an appalling performance score so I've been doing some investigating. I have a component called Home
inside Home I have useEffect (only one) that looks like this
useEffect(() => {
console.log('rendering in here?') // called 14 times...what?!
console.log(user.data, 'uvv') // called 13 times...again, What the heck?
}, [user.data])
I know that you put the second argument of , [] to make sure useEffect is only called once the data changes but this is the main part I don't get. when I console log user.data the first 4 console logs are empty arrays. the next 9 are arrays of length 9. so in my head, it should only have called it twice? once for [] and once for [].length(9) so what on earth is going on?
I seriously need to reduce it as it must be killing my performance. let me know if there's anything else I can do to dramatically reduce these calls
this is how I get user.data
const Home = ({ ui, user }) => { // I pass it in here as a prop
const mapState = ({ user }) => ({
user,
})
and then my component is connected so I just pass it in here
To overcome this scenario, React Hooks also provides functionality called useMemo.
You can use useMemo instead useEffect because useMemo cache the instance it renders and whenever it hit for render, it first check into cache to whether any related instance has been available for given deps.. If so, then rather than run entire function it will simply return it from cache.
This is not an answer but there is too much code to fit in a comment. First you can log all actions that change user.data by replacing original root reducer temporarlily:
let lastData = {};
const logRootReducer = (state, action) => {
const newState = rootReducer(state, action);
if (newState.user.data !== lastData) {
console.log(
'action changed data:',
action,
newState.user.data,
lastData
);
lastData = newState.user.data;
}
return newState;
};
Another thing causing user.data to keep changing is when you do something like this in the reducer:
if (action.type === SOME_TYPE) {
return {
...state,
user: {
...state.user,
//here data is set to a new array every time
data: [],
},
};
}
Instead you can do something like this:
const EMPTY_DATA = [];
//... other code
data: EMPTY_DATA,
Your selector is getting user out of state and creating a new object that would cause the component to re render but the dependency of the effect is user.data so the effect will only run if data actually changed.
Redux devtools also show differences in the wrong way, if you mutate something in state the devtools will show them as changes but React won't see them as changes. When you assign a new object to something data:[] then redux won't show them as changes but React will see it as a change.

React Hooks: Referencing context and managing dependencies in useEffect hook

I am trying to use react hooks to make a Table component that displays rows of data from an API based on a set of filters that the user can choose. I want to make a new call to fetch data whenever the user clicks an 'Apply Filters' button, not when the user makes changes to the filters.
I am using context to manage the 'filters' state and a 'lastFetched' state which tracks when the user last clicked the 'Apply Filters' button (as well as other states on the page). Updates to the context are made via the useReducer hook and its dispatch method (see here).
The data fetching occurs in a useEffect hook that reruns whenever the 'lastFetched' state changes. This appears to be working correctly; however, the effect references other values from the context (i.e. the filters) that are not included in the dependencies. I am aware of the exhaustive-deps eslint rule, and I am concerned that I am not handling the hook's dependencies correctly.
const Table = () => {
const [context, dispatch] = useTableContext(); // implemented with createContext and useReducer
const { filters, lastFetched } = context;
useEffect(() => {
if (!filters.run) {
return;
}
dispatch({ type: 'FETCH_DATA_BEGIN' });
const params = convertContextToParams(context); // this is lazy, but essentially just uses the the filters and some other state from the context
API.fetchData(params)
.then((data) => {
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data.results });
})
.catch((e) => {
dispatch({ type: 'FETCH_DATA_FAILURE', payload: e.response.data.message });
});
return () => { ... some cleanup... };
}, [lastFetched]); // <== This is the part in question
return <...some jsx.../>
};
Again, this appears to be working, but according to the react docs, it seems I should be including all the values from the context used in the hook in the hook's dependencies in order to prevent stale references. This would cause the logic to break, since I don't want to fetch data whenever the filters change.
My question is: when the user clicks 'Apply Filters', updates context.lastFetched, and triggers the useEffect hook, will the hook be referencing stale filter state from the context? If so, why? Since the effect is rerun whenever the button is clicked, and all the state updates are done via a reducer, does the usual danger of referencing stale variables in a closure still apply?
Any guidance appreciated!
Note: I have thought about using useRef to prevent this issue, or perhaps devising some custom async middleware to fetch data on certain dispatches, but this is the solution I currently have.
I am not an expert but I would like to provide my takes. According to my understanding of how Context works, you will not get stale filter data with the current implementation. useReducer updates the state with a new object which will trigger Table to be re-render.
Also, Table component doesn't really care about filter data unless lastFetched is changed by a click event. If lastFetched is changed, all the Consumer of TableContext will be re-render again. You should not get stale filter data either.

Categories