How to work around expensive custom hooks? - reactjs

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;
};

Related

Const empty object causes infinite re-render

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 (
<></>
);
}

How to memoize custom hooks to improve performance

Have below function which is common custom hook and called from multiple places.
How this can be memoized to improve performance. (While debug on browser then observed it called multiple times). It would be also fine if fields.forEach only memoized instead of all code under custom hooks'
I tried to add a function inside the hook but, I need to return result object instead of function.
export function useListObject(fields)
{
const allData = useGetAll();
const result = {};
fields.foreach((field) =>{
...
...
...
result[field]= {key, value , parameters, names}
});
return result;
}
///// component called as below
const listData = useListObject(['state','country','categoryTypes','category']);
//Is it possible to memoize here ? so no need to memoize within custom hooks.
You can use the useMemo hook provided by React to memoize the result of your forEach loop:
const result = useMemo(() => {
const fieldData = {};
fields.foreach((field) =>{
...
...
...
fieldData[field] = { key, value , parameters, names }
})
return fieldData;
}, [fields]);
This way until fields changes, result will be memoized to whatever you return inside of the useMemo call.

What are production use cases for the useRef, useMemo, useCallback hooks?

Outside of the counter example seen in many YouTube tutorial videos, what are practical/real-world use cases for useMemo and useCallback?
Also, I've only seen an input focus example for the useRef hook.
Please share other use cases you've found for these hooks.
useRef:
Syntax: const refObject = useRef(initialValue);
It simply returns a plain JavaScript object. Its value can be accessed and modified (mutability) as many times as you need without worrying about "rerender".
Its value will persist (won't be reset to the initialValue unlike an ordinary* object defined in your function component; it persists because useRef gives you the same object instead of creating a new one on subsequent renders) for the component lifetime.
If you write const refObject = useRef(0) and print refObject on console, you would see the log an object - { current: 0 }.
*ordinary object vs refObject, example:
function App() {
const ordinaryObject = { current: 0 } // It will reset to {current:0} at each render
const refObject = useRef(0) // It will persist (won't reset to the initial value) for the component lifetime
return <>...</>
}
Few common uses, examples:
To access the DOM: <div ref={myRef} />
Store mutable value like instance variable (in class)
A render counter
A value to be used in setTimeout / setInterval without a stale closure issue.
useMemo:
Syntax: const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
It returns a memoized value. The primary purpose of this hook is "performance optimization". Use it sparingly to optimize the performance when needed.
It accepts two arguments - "create" function (which should return a value to be memoized) and "dependency" array. It will recompute the memoized value only when one of the dependencies has changed.
Few common uses, examples:
Optimize expensive calculations (e.g. operations on data like sort, filter, changing format etc.) while rendering
Unmemoized example:
function App() {
const [data, setData] = useState([.....])
function format() {
console.log('formatting ...') // this will print at every render
const formattedData = []
data.forEach(item => {
const newItem = // ... do somthing here, formatting, sorting, filtering (by date, by text,..) etc
if (newItem) {
formattedData.push(newItem)
}
})
return formattedData
}
const formattedData = format()
return <>
{formattedData.map(item => <div key={item.id}>
{item.title}
</div>)}
</>
}
Memoized example:
function App() {
const [data, setData] = useState([.....])
function format() {
console.log('formatting ...') // this will print only when data has changed
const formattedData = []
data.forEach(item => {
const newItem = // ... do somthing here, formatting, sorting, filtering (by date, by text,..) etc
if (newItem) {
formattedData.push(newItem)
}
})
return formattedData
}
const formattedData = useMemo(format, [data])
return <>
{formattedData.map(item => <div key={item.id}>
{item.title}
</div>)}
<>
}
useCallback:
Syntax: const memoizedCallback = useCallback(() => { //.. do something with a & b }, [a, b])
It returns a memoized function (or callback).
It accepts two arguments - "function" and "dependency" array. It will return new i.e. re-created function only when one of the dependencies has changed, or else it will return the old i.e. memoized one.
Few common uses, examples:
Passing memoized functions to child components (that are optimized with React.memo or shouldComponentUpdate using shallow equal - Object.is) to avoid unnecessary rerender of child component due to functions passed as props.
Example 1, without useCallback:
const Child = React.memo(function Child({foo}) {
console.log('child rendering ...') // Child will rerender (because foo will be new) whenever MyApp rerenders
return <>Child<>
})
function MyApp() {
function foo() {
// do something
}
return <Child foo={foo}/>
}
Example 1, with useCallback:
const Child = React.memo(function Child({foo}) {
console.log('child rendering ...') // Child will NOT rerender whenever MyApp rerenders
// But will rerender only when memoizedFoo is new (and that will happen only when useCallback's dependency would change)
return <>Child<>
})
function MyApp() {
function foo() {
// do something
}
const memoizedFoo = useCallback(foo, [])
return <Child foo={memoizedFoo}/>
}
Passing memoized functions to as dependencies in other hooks.
Example 2, without useCallback, Bad (But eslint-plugin-react-hook would give you warning to correct it):
function MyApp() {
function foo() {
// do something with state or props data
}
useEffect(() => {
// do something with foo
// maybe fetch from API and then pass data to foo
foo()
}, [foo])
return <>...<>
}
Example 2, with useCallback, Good:
function MyApp() {
const memoizedFoo = useCallback(function foo() {
// do something with state or props data
}, [ /* related state / props */])
useEffect(() => {
// do something with memoizedFoo
// maybe fetch from API and then pass data to memoizedFoo
memoizedFoo()
}, [memoizedFoo])
return <>...<>
}
These hooks rules or implementations may change in the future. So, please make sure to check hooks reference in docs. Also, it is important to pay attention to eslint-plugin-react-hook warnings about dependencies. It will guide you if omit any dependency of these hooks.
I want to add, for useMemo i usually use it when i want to combine useState and useEffect at the same time. For example:
...
const [data, setData] = useState(...);
const [name, setName] = useState("Mario");
// like the example by ajeet, for complex calculations
const formattedData = useMemo(() => data.map(...), [data])
// or for simple state that you're sure you would never modify it directly
const prefixedName = useMemo(() => NAME_PREFIX + name, [name]);
I do not know if there will be performance issues because the docs stated that useMemo should be used for expensive calculation. But i believe this is way cleaner than using useState
useMemo always use for performance optimization. Be careful to add all the deps need.

useEffect on infinite loop using async fetch function

I am trying to understand why the following useEffect is running in an infinite loop. I made the fetchSchedule helper function to call the getSchedule service (using Axios to query the API endpoint). Reason I did not define this function inside the useEffect hook is because I would like to alternatively also call it whenever the onStatus function is invoked (which toggles a Boolean PUT request on a separate endpoint).
The eslinter is requiring fetchSchedule be added to the array of dependencies, which seems to be triggering the infinite loop.
The way it should work is fetching the data from the database on first render, and then only each time either the value prop is updated or the onStatus button is toggled.
So far my research seems to point that this may have something to do with the way useEffect behaves with async functions and closures. I’m still trying to understand Hooks and evidently there’s something I’m not getting in my code…
import React, { useEffect, useCallback } from 'react';
import useStateRef from 'react-usestateref';
import { NavLink } from 'react-router-dom';
import { getSchedule, updateStatus } from '../../services/scheduleService';
import Status from './status';
// import Pagination from './pagination';
const List = ({ value }) => {
// eslint-disable-next-line
const [schedule, setSchedule, ref] = useStateRef([]);
// const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
}, [value, setSchedule]);
const onStatus = (id) => {
updateStatus(id);
fetchSchedule();
console.log('fetch', ref.current[0].completed);
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
return (...)
Update March 2021
After working with the repo owner for react-usestateref, the package now functions as originally intended and is safe to use as a replacement for useState as of version 1.0.5. The current implementation looks like this:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(state);
var dispatch = React.useCallback(function(val) {
ref.current = typeof val === "function" ?
val(ref.current) : val;
setState(ref.current);
}, []);
return [state, dispatch, ref];
};
You would be fine if it weren't for this react-usestateref import.
The hook returns a plain anonymous function for setting state which means that it will be recreated on every render - you cannot usefully include it in any dependency array as that too will be updated on every render. However, since the function is being returned from an unknown custom hook (and regardless, ESLint would correctly identify that it is not a proper setter function) you'll get warnings when you don't.
The 'problem' which it tries to solve is also going to introduce bad practice into your code - it's a pretty way to avoid properly handling dependencies which are there to make your code safer.
If you go back to a standard state hook I believe this code will work fine. Instead of trying to get a ref of the state in onStatus, make it async as well and return the data from fetchSchedule as well as setting it.
const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
return data;
}, [value]);
const onStatus = async (id) => {
updateStatus(id);
const data = await fetchSchedule();
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
Alternatively, although again I wouldn't really recommend using this, we could actually write a safe version of the useStateRef hook instead:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(defaultValue);
ref.current = state;
return [state, setState, ref];
}
A state setter function is always referentially identical throughout the lifespan of a component so this can be included in a dependency array without causing the effect/callback to be recreated.

Best practice for marking hooks as not to be reused in multiple places

It seems a lot of my custom React Hooks don't work well, or seem to cause a big performance overhead if they are reused in multiple places. For example:
A hook that is only called in the context provider and sets up some context state/setters for the rest of the app to use
A hook that should only be called in a root component of a Route to setup some default state for the page
A hook that checks if a resource is cached and if not, retrieves it from the backend
Is there any way to ensure that a hook is only referenced once in a stack? Eg. I would like to trigger a warning or error when I call this hook in multiple components in the same cycle.
Alternatively, is there a pattern that I should use that simply prevents it being a problem to reuse such hooks?
Example of hook that should not be reused (third example). If I would use this hook in multiple places, I would most likely end up making unnecessary API calls.
export function useFetchIfNotCached({id}) {
const {apiResources} = useContext(AppContext);
useEffect(() => {
if (!apiResources[id]) {
fetchApiResource(id); // sets result into apiResources
}
}, [apiResources]);
return apiResources[id];
}
Example of what I want to prevent (please don't point out that this is a contrived example, I know, it's just to illustrate the problem):
export function Parent({id}) {
const resource = useFetchIfNotCached({id});
return <Child id={id}>{resource.Name}</Child>
}
export function Child({id}) {
const resource = useFetchIfNotCached({id}); // <--- should not be allowed
return <div>Child: {resource.Name}</div>
}
You need to transform your custom hooks into singleton stores, and subscribe to them directly from any component.
See reusable library implementation.
const Comp1 = () => {
const something = useCounter(); // is a singleton
}
const Comp2 = () => {
const something = useCounter(); // same something, no reset
}
To ensure that a hook called only once, you only need to add a state for it.
const useCustomHook = () => {
const [isCalled, setIsCalled] = useState(false);
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
setIsCalled(true);
setState(value);
};
return { state, setState: onSetState, isCalled };
};
Edit:
If you introduce a global variable in your custom hook you will get the expected result. Thats because global variables are not tied to component's lifecycle
let isCalledOnce = false;
const useCustomHook = () => {
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
if (!isCalledOnce) {
isCalledOnce = true;
setState(false);
}
};
return { state, setState: onSetState, isCalled };
};

Resources