React Using Correct Memo for Table Column Mapping - reactjs

I have a React component that uses HandsOnTable to render a table (not necessarily important to the issue but figured I'd give context).
Every time the component renders the table, it runs a renderer function on every column header to handle formatting. I want to memoize the this function pumping out the columns because tables can have 100+ columns and the renderer/formatter it getting very expensive on the big tables.
const getSettings = () => {
// getSettings only runs once or twice, but getTableColumns will run once for every column
return {
columns: getTableColumns
}
}
const getTableColumns = (index) => {
const column = data[index]
const renderer = () => {
// expensive formatting using the column data
}
return {
renderer
}
}
I'm unsure which to use, memo, useMemo, useCallback; I'm new to the memo situation in React. Also, I've read the React versions of memo aren't like actual memo like in lodash? Here is what I've tried:
columns: useMemo(() => getHotColumns, [props.selectedNode])
This unfortunately throws an error: Invalid hook call. Hooks can only be called inside of the body of a function component.
Every time the user selects a new node on my graph UI I want to rerender my table to handle the new data, aka when props.selectedNode changes.

The quick version:
It sounds to me like what you actually want here is to memoize the output of each renderer invocation. That could looks something like this:
// This is the important bit, we are caching the rendered output for each column
const renderedColumns = useMemo(() => {
const renderer = (columnData) => {
// expensive formatting using the column data
}
return your_column_data.map(renderer);
}, [your_column_data]);
const getTableColumns = (index) => {
// Return the cached value instead of re-calculating
return renderedColumns[index];
};
// This memoization sounds like it is less important, but it doesn't hurt.
const columnSettings = useMemo(() => ({
columns: getTableColumns
}), [getTableColumns]);
I'm unsure which to use, memo, useMemo, useCallback
Quick overview, as I understand it:
memo - Memoizes a React component. This means that it won't rerender unless one of its props change. (normally, if the parent rerenders, so will the child)
useMemo computes and caches a value until one of the values in the specified dependencies array changes, in which case it will recalculate using the provided function.
useCallback afaik this is identical to useMemo, just has a cleaner interface for memoizing functions.

Related

React make an async call on data after retrieving it from the state

I need to make an async call after I get some data from a custom hook. My problem is that when I do it causes an infinite loop.
export function useFarmInfo(): {
[chainId in ChainId]: StakingBasic[];
} {
return {
[ChainId.MATIC]: Object.values(useDefaultFarmList()[ChainId.MATIC]),
[ChainId.MUMBAI]: [],
};
}
// hook to grab state from the state
const lpFarms = useFarmInfo();
const dualFarms = useDualFarmInfo();
//Memoize the pairs
const pairLists = useMemo(() => {
const stakingPairLists = lpFarms[chainIdOrDefault].map((item) => item.pair);
const dualPairLists = dualFarms[chainIdOrDefault].map((item) => item.pair);
return stakingPairLists.concat(dualPairLists);
}, [chainIdOrDefault, lpFarms, dualFarms]);
//Grab the bulk data results from the web
useEffect(() => {
getBulkPairData(pairLists).then((data) => setBulkPairs(data));
}, [pairLists]);
I think whats happening is that when I set the state it re-renders which causes hook to grab the farms from the state to be reset, and it creates an infinite loop.
I tried to move the getBulkPairData into the memoized function, but that's not meant to handle promises.
How do I properly make an async call after retrieving data from my hooks?
I am not sure if I can give you a solution to your problem, but I can give you some hints on how to find out the cause:
First you can find out if the useEffect hook gets triggered too often because its dependency changes too often, or if the components that contains your code gets re-mounted over and over again:
Remove the dependency of your useEffect hook and see if it still gets triggered too often. If so, your problem lies outside of your component.
If not, find out if the dependencies of your useMemo hook change unexpectedly:
useEffect(()=>console.log("chainIdOrDefault changed"), [chainIdOrDefault]);
useEffect(()=>console.log("lpFarms changed"), [lpFarms]);
useEffect(()=>console.log("dualFarms changed"), [dualFarms]);
I assume, this is the most likely reason - maybe useFarmInfo or useDualFarmInfo create new objects on each render (even if these objects contain the same data on each render, they might not be identical). If so, either change these hooks and add some memoization (if you have access to your code) or narrow down the dependencies of your pairLists:
const pairLists = useMemo(() => {
const stakingPairLists = lpFarms[chainIdOrDefault].map((item) => item.pair);
const dualPairLists = dualFarms[chainIdOrDefault].map((item) => item.pair);
return stakingPairLists.concat(dualPairLists);
}, [lpFarms[chainIdOrDefault], dualFarms[chainIdOrDefault]]);

Does this "custom react hook" breaks the hooks law?

I am using many useCallbacks in useEffects in the same format syntax and feel like shortening them. So, I map them in this hook. effects is an array of useCallback functions.
import React from 'react';
const useEffects = (effects: Function[]) =>
effects.map((effect) =>
React.useEffect(() => {
effect();
}, [effect])
);
export default useEffects;
Hooks can't be defined inside an array.
I wrote an article recently about the rendering order of hooks. https://windmaomao.medium.com/understanding-hooks-part-4-hook-c7a8c7185f4e
It really goes down to below snippet how Hooks is defined.
function hook(Hook, ...args) {
let id = notify()
let hook = hooks.get(id)
if(!hook) {
hook = new Hook(id, element, ...args)
hooks.set(id, hook)
}
return hook.update(...args)
}
When a hook is registered, it requires an unique id, as in line notify(). Which is just a plain i++ placed inside the Component where the hook is written inside.
So if you have a fixed physical location of the hook, you have a fixed id. Otherwise the id can be random, and since the render Component function is called every render cycle, a random id is not going to find the right hook in the next render cycle.
That is why if can not be written before the hook statement either. Just try
const Component = () => {
const b = true
if (!b) return null
const [a] = useState(1)
}
You should get similar error.

does `useCallback` have something like a `useRef.current` inside it that will ensure that calls always use the latest version of the callback?

I have a situation where I am loading several datasets; the user can choose how many to load. All the datasets are rendered on the same chart. The datasets load individually and asynchronously.
The code is something along the lines of
export const DatasetGraph = ({userSelections}) => {
const [datasets, setDatasets] = useState({});
// when a selection changes, update the data
useEffect(() => {
// while loading a dataset, it is visible but has no data
setDatasets(userSelections.map(selec => ({ dsname: [] })));
// trigger asynchronous loads for each new dataset
userSelections.forEach((dsname) => fetchDataset(dsname));
}, [userSelections]);
const fetchDataset = async (dsname) => {
response = await fetch(url + dsname);
// QUESTION: after the await, will this call the most recent version of
// the callback? Will it use the most recent datasets object or an old
// one saved in the useCallback closure?
updateDataset(dsname, response);
};
// when a fetch returns, update the empty placeholder with the real data
const updateDataset = useCallback(
// For this toy example, we could use a setState function to always
// retrieve the latest `datasets`. However, this callback may use other
// state which is really what this question is about.
(dsname, response) => setDatasets({ ...datasets, dsname: response }),
[datasets]
);
return <Graph data={datasets}></Graph>;
};
I have not yet tried just having each dataset be a React component that doesn't render anything to the DOM, which can then manage its own loading state. That might actually be easier.
useCallback uses the dependencies array to detect changes
The useCallback method uses the dependencies array you pass to it in order to memoize the value of your function. Your function will be recreated every time but not assigned to updateDataset unless one of the dependencies has changed.
You should be wary of using useCallback unless a component below your function is expensive to rerender, otherwise, useCallback won't have much of a positive effect on your application's performance if any positive effect at all.
It works the same way that useMemo does to ensure that your data, in the case of useCallback it is a function, is only updated on your variable when something it depends on has changed.

Call Hooks on component change in React Native

I'm working on an react native app.
This app use a database, the main component use 2 differents hook.
The first hook retrieves the results of a SQL query and store them in a variable.
The second hook creates a list from the first variable
Like this:
const [people, setPeople ] = useState([]);
useEffect (() => {
db.getAllPeople().then(row => setPeople(row))
},[])
const [listData, setListData] = useState([]);
useEffect(()=> {
setListData(
Array(people.length)
.fill('')
.map((_, i) => ({ key: `${i}`, name: `${people[i].name}`}))
)
}, [people]);
After that, my main component displays a SwipeList from the results.
Here is the problem. I am using another component to add an element to my database. When I return to my main component I would like this new element to be displayed. But the problem is that the 2 hooks are not called on the component change and the list therefore remains unchanged.
I've tried to use the useFocusEffect but it doesn't work in my case.
Any suggestions ?
I think the useState hook manages the state of the component itself, unless you are passing this state among your parent and child or using callbacks to set the state on the component that you want to render, you could use a single source of truth to handle the changes in data, react itself will notice this changes and therefore, render the changed screens, considering that you have asynchronous operations when querying the database, a combination of redux and redux saga may help you.
https://github.com/redux-saga/redux-saga
There're one issues with your current code, or potential issues
Your second useEffect might get called when people becomes an empty list, this will reset your list data. The cure is to put a if statement inside, ex.
useEffect(()=> {
if (!people) return;
setListData(...)
}, [people]);
To be honest, if these two lists are connected, you shouldn't use two hook. The best way is to define listData
const listData = (a function that takes people as input), ex.
const listData = people.map(v => v)
Of course, there might be a reason why you'd like to introduce more hook in complex situation, ex. useRef, useMemo.

useState vs useReducer

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
(quote from https://reactjs.org/docs/hooks-reference.html#usereducer)
I'm interested in the bold part, which states that useReducer should be used instead of useState when being used in contexts.
I tried both variants, but they don't appear to differ.
The way I compared both approaches was as follows:
const [state, updateState] = useState();
const [reducerState, dispatch] = useReducer(myReducerFunction);
I passed each of them once to a context object, which was being consumed in a deeper child (I just ran separate tests, replacing the value by the function that I wanted to test).
<ContextObject.Provider value={updateState // dispatch}>
The child contained these functions
const updateFunction = useContext(ContextObject);
useEffect(
() => {
console.log('effect triggered');
console.log(updateFunction);
},
[updateFunction]
);
In both cases, when the parent rerendered (because of another local state change), the effect never ran, indicating that the update function isn't changed between renders.
Am I reading the bold sentence in the quote wrong? Or is there something I'm overlooking?
useReducer also lets you optimize performance for components that
trigger deep updates because you can pass dispatch down instead of
callbacks.
The above statement is not trying to indicate that the setter returned by useState is being created newly on each update or render. What it means is that when you have a complex logic to update state you simply won't use the setter directly to update state, instead you will write a complex function which in turn would call the setter with updated state something like
const handleStateChange = () => {
// lots of logic to derive updated state
updateState(newState);
}
ContextObject.Provider value={{state, handleStateChange}}>
Now in the above case everytime the parent is re-rendered a new instance of handleStateChange is created causing the Context Consumer to also re-render.
A solution to the above case is to use useCallback and memoize the state updater method and use it. However for this you would need to take care of closure issues associated with using the values within the method.
Hence it is recommended to use useReducer which returns a dispatch method that doesn't change between re-renders and you can have the manipulation logic in the reducers.
Practical observation on useReducer and useState -
UseState:
In my React Native project I've 1 screen containing 25+ different states created using useState.
I'm calling an api in useEffect (componentDidMount) and on getting the response based on some conditions, I'm setting up these 25 states, calling 25 state setter function for each function.
I've put a re-rendering counter and checked my screen is re-rendered 14 times.
re-rendering count likewise :
let count = 0;
export default function Home(props) {
count++;
console.log({count});
//...
// Rest of the code
}
UseReducer :
Then I've moved these 25 states in useReducer states, And used only single action to update these states on API response.
I've observed there is only 2 re-render.
//API calling method:
fetchData()
{
const response = await AuthAxios.getHomeData();
dispatch({type: 'SET_HOME_DATA', data: response.data});
}
//useReducer Code:
const initialStaes = {
state1: null,
state2: null,
//.....More States
state27: null,
state28: null
}
const HomeReducer = (state, action) => {
switch (action.type) {
case 'SET_HOME_DATA': {
return {
...state,
state1: (Data based on conditions),
state2: !(some Conditions ),
//....More states
state27: false
}
}
}
}
Advantage of useReducer in this case :
Using useReducer I've reduced number of re-renders on the screen, hence better performance and smoothness of the App.
Number of lines is reduced in my screen itself. It improved code readablity.
When you need to care about it
If you create a callback on render and pass it to a child component, the props of that child will change. However, when the parent renders, a regular component will rerender (to the virtual dom), even props remain the same. The exception is a classComponent that implements shouldComponentUpdate, and compares props (such as a PureComponent).
This is an optimization, and you should only care about it if rerendering the child component requires significant computation (If you render it to the same screen multiple times, or if it will require a deep or significant rerender).
If this is the case, you should make sure:
Your child is a class component that extends PureComponent
Avoid passing a newly created function as a prop. Instead, pass
dispatch, the setter returned from React.useState or a memoized
customized setter.
Using a memoized customized setter
While I would not recommend building a unique memoized setter for a specific component (there are a few things you need to look out for), you could use a general hook that takes care of implementation for you.
Here is an example of a useObjState hook, which provides an easy API, and which will not cause additional rerenders.
const useObjState = initialObj => {
const [obj, setObj] = React.useState(initialObj);
const memoizedSetObj = React.useMemo(() => {
const helper = {};
Object.keys(initialObj).forEach(key => {
helper[key] = newVal =>
setObj(prevObj => ({ ...prevObj, [key]: newVal }));
});
return helper;
}, []);
return [obj, memoizedSetObj];
};
function App() {
const [user, memoizedSetUser] = useObjState({
id: 1,
name: "ed",
age: null,
});
return (
<NameComp
setter={memoizedSetUser.name}
name={user.name}
/>
);
}
const NameComp = ({name, setter}) => (
<div>
<h1>{name}</h1>
<input
value={name}
onChange={e => setter(e.target.value)}
/>
</div>
)
Demo

Resources