How to memoize custom hooks to improve performance - reactjs

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.

Related

How to re-render a component when a non state object is updated

I have an object which value updates and i would like to know if there is a way to re-render the component when my object value is updated.
I can't create a state object because the state won't be updated whenever the object is.
Using a ref is not a good idea(i think) since it does not cause a re-render when updated.
The said object is an instance of https://docs.kuzzle.io/sdk/js/7/core-classes/observer/introduction/
The observer class doesn't seem to play well with your use case since it's just sugar syntax to manage the updates with mutable objects. The documentation already has a section for React, and I suggest following that approach instead and using the SDK directly to retrieve the document by observing it.
You can implement this hook-observer pattern
import React, { useCallback, useEffect, useState } from "react";
import kuzzle from "./services/kuzzle";
const YourComponent = () => {
const [doc, setDoc] = useState({});
const initialize = useCallback(async () => {
await kuzzle.connect();
await kuzzle.realtime.subscribe(
"index",
"collection",
{ ids: ["document-id"] },
(notification) => {
if (notification.type !== "document" && notification.event !== "write")
return;
// getDocFromNotification will have logic to retrieve the doc from response
setDoc(getDocFromNotification(notification));
}
);
}, []);
useEffect(() => {
initialize();
return () => {
// clean up
if (kuzzle.connected) kuzzle.disconnect();
};
}, []);
return <div>{JSON.stringify(doc)}</div>;
};
useSyncExternalStore, a new React library hook, is what I believe to be the best choice.
StackBlitz TypeScript example
In your case, a simple store for "non state object" is made:
function createStore(initialState) {
const callbacks = new Set();
let state = initialState;
// subscribe
const subscribe = (cb) => {
callbacks.add(cb);
return () => callbacks.delete(cb);
};
// getSnapshot
const getSnapshot = () => state;
// setState
const setState = (fn) => {
state = fn(state);
callbacks.forEach((cb) => cb());
};
return { subscribe, getSnapshot, setState };
}
const store = createStore(initialPostData);
useSyncExternalStore handles the job when the update of "non state object" is performed:
const title = React.useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().title
);
In the example updatePostDataStore function get fake json data from JSONPlaceholder:
async function updatePostDataStore(store) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${Math.floor(Math.random()*100)+1}`)
const postData = await response.json()
store.setState((prev)=>({...prev,...postData}));
};
My answer assumes that the object cannot for some reason be in React as state (too big, too slow, too whatever). In most cases that's probably a wrong assumption, but it can happen.
I can't create a state object because the state won't be updated whenever the object is
I assume you mean you can't put that object in a React state. We could however put something else in state whenever we want an update. It's the easiest way to trigger a render in React.
Write a function instead of accessing the object directly. That way you can intercept every call that modifies the object. If you can reliably run an observer function when the object changes, that would work too.
Whatever you do, you can't get around calling a function that does something like useState to trigger a render. And you'll have to call it in some way every time you're modifying the object.
const myObject = {};
let i = 0;
let updater = null;
function setMyObject(key, value) {
myObject[key] = value;
i++;
if (updater !== null) {
updater(i);
}
};
Change your code to access the object only with setMyObject(key, value).
You could then put that in a hook. For simplicity I'll assume there's just 1 such object ever on the page.
function useCustomUpdater() {
const [, setState] = useState(0);
useEffect(()=>{
updater = setState;
return () => {
updater = null;
}
}, [setState]);
}
function MyComponent() {
useCustomUpdater();
return <div>I re-render when that object changes</div>;
}
Similarly, as long as you have control over the code that interacts with this object, you could wrap every such call with a function that also schedules an update.
Then, as long as your code properly calls the function, your component will get re-rendered. The only additional state is a single integer.
The question currently lacks too much detail to give a good assessment whether my suggested approach makes sense. But it seems like a very simple way to achieve what you describe.
It would be interesting to get more information about what kind of object it is, how frequently it's updated, and in which scope it lives.

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.

When is useCallback called when involving redux state and empty dependency array?

Currently running react and redux. In the simple code below, I have [] as dependency,
Does it mean useCallback only called once when the page loaded?
or when state.test['data'] the redux state gets updated, useCallback will be called?
const mapState = useCallback(state => {
return {
data: state.test['data'],
};
}, []);
const {
data
} = useMappedState(mapState);
Follow up question 1
If I want to listen to state.test['data']'s change, how do I do it? [state.test['data']] this will complain that state is not defined.
Follow up question 2
What about I need to listen to multiple states change?
const mapState = useCallback(state => {
return {
data1: state.test['data1'],
data2: state.test['data2'],
dataN: state.test['dataN'],
};
}, []);
const {
data
} = useMappedState(mapState);
How do I make sure data1, data2, dataN update to date?
As per the docs
useCallback return memoized version of the callback that only changes if one of the dependencies has changed
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
So, whenever there is changes in either of dependant value a or b,useCallback called again.
Does it mean useCallback only called once when the page loaded?
Its not guaranteed.Its totally depend on dependancy passed in array.If its value changes then useCallback is called.
EDIT:
your use case don't need useCallback.useSelector should be used for this use case.
const mapState = useSelector(function (state) {
return {
data: state.test['data']
};
});
const {
data
} = useMappedState(mapState);
This means mapState function will not be re-created in any of the subsequent renders. As it doesn't have any dependency so it will be only be created once.
eg
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
//Returns a memoized callback.
Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

Why does useCallback with an empty dependency array not return the same function?

I'm trying to write a custom React hook that returns functions which maintain reference equality with every invocation of the hook. Meaning that exactly the same function is returned from the hook every single time, such that comparing them with === returns true.
I was under the impression that the useCallback hook was the way to accomplish that:
function useCorrectCallback() {
const [count, setCount] = useState(0)
let increment = () => setCount(c => c + 1)
let memoized = useCallback(increment, [])
return {
count,
increment: memoized
}
}
So when this hook is called, I believe that the increment property of the return value should be the exact same function every time. It should be the value of the inline function the first time the hook was run, and not change on subsequent executions of the hook because I have specified no dependencies to useCallback with [].
But that's not what seems to happen! I've written a test project to demonstrate this problem. Here's the crux of that test:
function validateFunctionEquality(hookResult) {
// Store the last result of running the hook
let stored = { ...hookResult }
// Force a re-render to run the hook again
expect(hookResult.count).toEqual(0)
act(hookResult.increment)
expect(hookResult.count).toEqual(1)
// Compare the previous results to the current results
expect(hookResult.constant).toEqual(stored.constant)
expect(hookResult.increment).toEqual(stored.increment)
}
Just to validate that my tests are accurate, I also wrote a hook which just uses a globally scoped object to maintain "memory" instead of trying to ask React to do it with useCallback. This hook passes the tests:
let memoryHack = {}
function useMemoryHack() {
const [count, setCount] = useState(0)
let increment = () => setCount(c => c + 1)
memoryHack.increment = memoryHack.increment || increment
return {
count,
increment: memoryHack.increment
}
}
Am I using useCallback incorrectly? Why does useCorrectCallback return a different function in its increment property on subsequent executions?
I suspect is an issue with enzyme's shallow. Probably related to https://github.com/airbnb/enzyme/issues/2086
By using mount, your test pass as it is:
act(() => {
componentMount = mount( // used mount instead of shallow
<Component>
{hookValues => {
copyHookValues(hookResult, hookValues)
return null
}}
</Component>
)
})

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