Const empty object causes infinite re-render - reactjs

Consider this dummified example:
export function myHook<TVariables = Record<string,string>> (variables?: TVariables) {
const [result, setResult] = useState<number>();
const fooFunction(variables?: TVariables) {
//In real life : do something with the variables...
setResult(666);
}
useEffect(() =>
{
fooFunction(variables);
}, [variables])
return result;
}
Later, in one of my components I use the hook like this :
export interface IMyVariables {
//Empty. Just because.
}
const MyComponent: React.FunctionComponent = () => {
const variables = {};
const [result] = myHook<IMyVariables>(variables );
return (
<></>
);
}
PROBLEM :
const variables = {}; is not enough to guarantee that React sees that the variables didn't change. It actually thinks that they HAVE changed every time, causing an infinite re-render.
BRUTE FORCE SOLUTION:
This useMemo fixes the problem. As in : React understands that the value didn't change. So the component is rendered only once. And in other places where some other type IMyVariables2 does have fields, then it gets re-rendered if one of them changes.
const params = useMemo(() => { return {}; }, []);
QUESTION:
Is there a smarter way of achieving this? I.e. a smarter way of making React understand that {} === {} ?
EDIT
I thought that the issue was exactly the same when doing const variables = undefined as with const variables = {} .
But it seems that undefined prevents the infinite loop (which makes sense : React correctly determines that undefined === undefined).
I'm not sure why I had convinced myself otherwise. I think it might have been because of a colleague of mine who uses weak typing -- he passed the parameters in the wrong order, passing an actual value where it should have been undefined, and I didn't spot the mistake, wondering about the infinite loop.

Why do you want to reinitate the variables each render? You should keep them inside a state or something else:
const MyComponent: React.FunctionComponent = () => {
const [variables, setVariables] = useState({});
const [result] = myHook<IMyVariables>(variables);
return (
<></>
);
}
you can also use a useRef, or put it outside of the component, but it depends on what the variable should be and when it will change.
As i think you will change the variables somehow the setState would be the best solution, and it will also update the hook after calling setVariables(...).
If they never change anyway use:
const variables = {};
const MyComponent: React.FunctionComponent = () => {
const [result] = myHook<IMyVariables>(variables);
return (
<></>
);
}

Related

react query returning empty object for data

I am trying to abstract away my react/tanstack query.
I have a custom hook like the following:
const useGamesApi = () => {
const upcomingGamesQuery = useQuery(
["upcoming", date],
async () => {
const ret = await apiGetUpcomingGames(date);
return ret;
},
{
onSuccess: (data) => {
setGames(data);
},
}
);
return {
games: upcomingGamesQuery,
};
};
export default useGamesApi;
I am trying to consume my API as follows:
const [games, setGames] = useState<Game[]>([]);
const gamesApi = useGamesApi();
useEffect(() => {
setGames(gamesApi.games.data);
}, []);
This leads to compilation errors and also the value of my games state variable remains an empty array, as if the useEffect never ran.
Basically I am trying to abstract away my react query to provide a simplified way of interacting with it for my components, whilst also giving it a chance to modify the parameter of the date, so that I can be able to set until which date I would like to query.
What would be the correct (compilation vise) and idiomatic way of doing this with react?
(note I am using this in a react native project, not sure if it counts.)
As per rules , You need to add all the variables used inside useEffect as dependency so that it reacts once the value is changed.
You don't really need useEffect for you scenario. It is used to cause side effects. simply do it like :
const games: Game[] = gamesApi?.games?.data;
const games: Game[] = gamesApi?.games?.data || []; // incase you need default value

react-hooks/exhaustive-deps and empty dependency lists for "on mount" [duplicate]

This is a React style question.
TL;DR Take the set function from React's useState. If that function "changed" every render, what's the best way to use it in a useEffect, with the Effect running only one time?
Explanation We have a useEffect that needs to run once (it fetches Firebase data) and then set that data in application state.
Here is a simplified example. We're using little-state-machine, and updateProfileData is an action to update the "profile" section of our JSON state.
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
get_data_async();
}, []);
return (
<p>Hello, world!</p>
);
}
However, ESLint doesn't like this:
React Hook useEffect has a missing dependency: 'actions'. Either include it or remove the dependency array
Which makes sense. The issue is this: actions changes every render -- and updating state causes a rerender. An infinite loop.
Dereferencing updateProfileData doesn't work either.
Is it good practice to use something like this: a single-run useEffect?
Concept code that may / may not work:
const useSingleEffect = (fxn, dependencies) => {
const [ hasRun, setHasRun ] = useState(false);
useEffect(() => {
if(!hasRun) {
fxn();
setHasRun(true);
}
}, [...dependencies, hasRun]);
};
// then, in a component:
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
useSingleEffect(async () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
}, [actions]);
return (
<p>Hello, world!</p>
);
}
But at that point, why even care about the dependency array? The initial code shown works and makes sense (closures guarantee the correct variables / functions), ESLint just recommends not to do it.
It's like if the second return value of React useState changed every render:
const [ foo, setFoo ] = useState(null);
// ^ this one
If that changed every render, how do we run an Effect with it once?
Ignore the eslint rule for line
If you truly want the effect to run only once exactly when the component mounts then you are correct to use an empty dependency array. You can disable the eslint rule for that line to ignore it.
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
get_data_async();
// NOTE: Run effect once on component mount, please
// recheck dependencies if effect is updated.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Note: If you later update the effect and it needs to run after other dependencies then this disabled comment can potentially mask future bugs, so I suggest leaving a rather overt comment as for the reason to override the established linting rule.
Custom hook logic
Alternatively you can use a react ref to signify the initial render. This is preferable to using some state to hold the value as updating it would trigger unnecessary render.
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
const initialRenderRef = useRef(true);
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
if (initialRenderRef.current) {
initialRenderRef.current = false;
get_data_async();
}
}, [actions]); // <-- and any other dependencies the linter complains about
return (
<p>Hello, world!</p>
);
}
And yes, absolutely you can factor this "single-run logic" into a custom hook if it is a pattern you find used over and over in your codebase.

React custom hook doesn't update state

Here i have a example hooks
const useGPS = () => {
const [gps, setGps] = useState({})
useEffect(() => {
setGps({ a: 1 })
}, [])
return [gps]
}
it's pretty simple, but when i use it inside another Component
const Foo = () => {
const location = useGPS()
useEffect(() => {
console.log(location);
}, [])
return (
<div>1</div>
)
}
console always log empty object for first time. Can someone explain what happened and how can i fix it? Sorry about my bad english. Thanks a lot!
To add to Tushar's answer, if you want to fix the behaviour without leaving useEffect running on every update (which can cause bugs in some more complex examples), you can add location to useEffect's dependencies:
const Foo = () => {
const location = useGPS();
useEffect(() => {
console.log(location);
}, [location]);
return <div>1</div>;
};
That way the effect will run only when a new value for location has been generated. You'll still see an empty object the very first time you call console.log, but it will be immediately updated to the generated value.
The value of GPS is only set after the first useEffect is run (inside the custom hook). It is initially empty and when the useEffect(foo component) runs, that empty value is shown.
The value is set successfully, and you can check this if you remove the [] array from the Foo component's useEffect. [] means that it will only run once after mounting, acting as componentDidMount.
export default function App() {
const location = useGPS()
useEffect(() => {
console.log(location);
});
return (
<div>1</div>
)
}
const [location] = useGPS();
you need to destructor location state array

useEffect re-renders too many times

I have this component, that needs to fetch data, set it to state and then pass it to the children.
Some of the data also needs to be set in context.
My problem is that using useEffect, once called the API, it will re-render for each setvalue() function I need to execute.
I have tried passing to useEffect an empty [] array, still getting the same number of re-renders, due to the fact that the state is changing.
At the moment the array is containg the set...functions to prevent eslint to throw warnings.
Is there a better way to avoid this many re-renders ?
const Home = (props) => {
console.log("TCL: Home -> props", props);
const classes = useStyles();
const [value, setValue] = React.useState(0);
//CONTEXT
const { listSavedJobs, setListSavedJobs, setIsFullView} = useContext(HomeContext);
const {
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
} = useContext(UserContext);
// STATE
const [searchSettings, setSearchSettings] = useState([]);
const [oppData, setOppData] = useState([]);
const handleChange = (event, newValue) => {
setValue(newValue);
};
const handleChangeIndex = index => {
setValue(index);
};
//API CALLS
useEffect(() => {
const triggerAPI = async () => {
setIsFullView(false);
const oppResponse = await API.getOpportunity();
if(oppResponse){
setOppData(oppResponse.response);
}
const profileResponse = await API.getUserProfile();
if(profileResponse){
setUserName(profileResponse.response.first_name);
setUserLastName(profileResponse.response.last_name);
setUserEmail(profileResponse.response.emailId);
}
const profileExtData = await API.getUserProfileExt();
if(profileExtData){
setAvatarProfile(profileExtData.response.avatar);
setListSavedJobs(profileExtData.response.savedJobs);
setSearchSettings(profileExtData.response.preferredIndustry);
}
};
triggerAPI();
}, [
setOppData,
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
setListSavedJobs,
setIsFullView,
]);
...```
Pass just an empty array to second parameter of useEffect.
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
Source
Edit: Try this to avoid rerenders. Use with caution
Only Run on Mount and Unmount
You can pass the special value of empty array [] as a way of saying “only run on mount and unmount”. So if we changed our component above to call useEffect like this:
useEffect(() => {
console.log('mounted');
return () => console.log('unmounting...');
}, [])
Then it will print “mounted” after the initial render, remain silent throughout its life, and print “unmounting…” on its way out.
Prevent useEffect From Running Every Render
If you want your effects to run less often, you can provide a second argument – an array of values. Think of them as the dependencies for that effect. If one of the dependencies has changed since the last time, the effect will run again. (It will also still run after the initial render)
const [value, setValue] = useState('initial');
useEffect(() => {
// This effect uses the `value` variable,
// so it "depends on" `value`.
console.log(value);
}, [value])
For more clarification useEffect
If you are using React 18, this won't be a problem anymore as the new auto batching feature: https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching
If you are using an old version, can refer to this solution: https://statics.teams.cdn.office.net/evergreen-assets/safelinks/1/atp-safelinks.html

How to work around expensive custom hooks?

As we know, the rule is:
Only Call Hooks at the Top Level. Don’t call Hooks inside loops, conditions, or nested functions.
So my questions is how to use and design a custom hook that is expensive?
Given this hook:
const useExpensiveHook = () => {
// some code that uses other built-in hooks...
const value = computeExpensiveValue();
// some more code....
return value;
}
If that rule did not exist my client code would be:
const myComponent = ({isSuperFeatureEnabled}) => {
let value;
if (isSuperFeatureEnabled){
value = useExpensiveHook();
}
return <div>{value}</div>;
}
The solution I came up with is to let the hook know that it should bail out, like this, using a flag:
const useExpensiveHook = ({enabled}) => {
// some code that uses other built-in hooks...
let value;
if(enabled) {
value = computeExpensiveValue();
}
else {
value = undefined;
}
// some more code....
return value;
};
and the client code:
const myComponent = ({isSuperFeatureEnabled}) => {
const value = useExpensiveHook({enabled : isSuperFeatureEnabled});
return <div>{value}</div>;
}
It is passing a flag to expensive hooks the right way to handle conditional hooks? What are the other options?
In original example it is hook initial value that is expensive, not a hook itself, computeExpensiveValue can be conditionally called:
const [value, setValue] = useState(enabled ? computeExpensiveValue() : undefined);
In currently listed example useExpensiveHook is not a hook but some function; it doesn't use React hooks.
The purpose of quoted rule is to make built-in hooks called unconditionally because the state of hooks is determined by the order in which they are called:
if (flipCoin())
var [foo] = useState('foo');
var [bar] = useState('bar');
In case useState('foo') isn't called on next component render, useState('bar') becomes the first useState hook to be called and considered foo state, while second useState is missing, this inconsistency triggers an error in a renderer.
If it were guaranteed that the order of hook calls is preserved, it would be acceptable to use conditions but this is rarely feasible in practice. Even if there's seemingly constant condition like if (process.env.NODE_ENV === 'development'), it could change under some circumstances at runtime and result in said problems that are hard to debug.
Correct:
useEffect(() => {
if (varyingCondition)
computeExpensiveValue();
});
Incorrect:
if (varyingCondition)
useEffect(() => {
computeExpensiveValue();
});
This rule applies only to built-in hooks and functions that call them directly or indirectly (so-called custom hooks). As long as computeExpensiveValue doesn't use built-in hooks internally, it can be conditionally called, as 'correct' example shows.
In case a component needs to conditionally apply third-party hook depending on prop flag, it should be guaranteed that the condition won't change with time by restricting it to be initial prop value:
const Component = ({ expensive, optionalValue }) => {
const isExpensive = useMemo(() => expensive, []);
if (isExpensive)
optionalValue = useExpensiveHook();
return ...
}
This way <Component expensive={flipCoin()} /> won't break the rule but just misuse the component.
Since it should be known if expensive hook is needed at the time when <Component expensive/> is used, a cleaner way is to compose this functionality in higher-order component and use different components depending on which one is needed:
const withExpensive = Comp => props => {
const optionalValue = useExpensiveHook();
return <Comp optionalValue={optionalValue} ...props />;
}
const Component = ({ optionalValue }) => {
return ...
}
const ExpensiveComponent = withExpensive(Component);
The argument to useState is being used only once and hence if you initially pass enabled as false to it, it will not execute the computeExpensiveValue ever. Hence you would need to add a useEffect call too. You could instead design your hook like
const useExpensiveHook = ({enabled}) => {
const [value, setValue] = useState(enabled ? computeExpensiveValue : undefined);
useEffect(()=> {
if(enabled) {
const value = computeExpensiveValue();
setValue(value);
}
}, [enabled]);
// some more code....
return value;
};

Resources