React useMemo or useEffect called too many times on redux state change - reactjs

I am trying different patterns on a new React application, In this case I have a redux store which contains a projects array of objects.
When the component loads I dispatch a new action which gets all the projects and set the store with the new values, in this case the list is rendered correctly.
When I dispatch the same actions from a children of the same component, the redux state is updated, the main component is rerendered but the list remain the same.
export const ProjectList: React.FC = () => {
//When projects change the component should rerender with the new store
const { projects, isLoading, errors } = useSelector((store: IAppState) => store.projectState);
const dispatch = useDispatch();
const { setActiveProject } = useActiveProjectValue();
const { setIsNewTask } = useIsNewTaskValue();
useEffect(() => {
console.log('called');
dispatch(getProjectsAction());
}, []);
return (
<div className="ProjectList">
{!isLoading ? (
projects.map((project: IProject) => (
<ProjectListItem
key={project.docId ? project.projectId : uuid()}
handleClick={() => handleClick(project)}
project={project}
>
{project.name}
</ProjectListItem>
))
) : (
<p>Loading projects</p>
)}
{errors && <p>{errors}</p>}
</div>
);
};
I have also tried to change useEffect with useMemo and add a project dependency to this function, in this case it works but it seems to trigger the action too many times:
useEffect(() => {
dispatch(getProjectsAction());
}, [projects]);
Any advice/solution?
Thanks!

Related

Prevent context.consumer from re-rendering component

I have created the following context provider. In sort it's a toast generator. It can have multiple toasts visible at the same time.
It all worked great and such until I realized that the <Component/> further down the tree that called the const context = useContext(ToastContext) aka the consumer of this context and the creator of the toast notifications, was also re-rendering when the providerValue was changing.
I tried to prevent that, changing the useMemo to a useState hook for the providerValue, which did stop my re-rendering problem , but now I could only have 1 toast active at a time (because toasts was never updated inside the add function).
Is there a way to have both my scenarios?
export const withToastProvider = (Component) => {
const WithToastProvider = (props) => {
const [toasts, setToasts] = useState([])
const add = (toastSettings) => {
const id = generateUEID()
setToasts([...toasts, { id, toastSettings }])
}
const remove = (id) => setToasts(toasts.filter((t) => t.id !== id))
// const [providerValue] = useState({ add, remove })
const providerValue = React.useMemo(() => {
return { add, remove }
}, [toasts])
const renderToasts = toasts.map((t, index) => (
<ToastNote key={t.id} remove={() => remove(t.id)} {...t.toastSettings} />
))
return (
<ToastContext.Provider value={providerValue}>
<Component {...props} />
<ToastWrapper>{renderToasts}</ToastWrapper>
</ToastContext.Provider>
)
}
return WithToastProvider
}
Thank you #cbdeveloper, I figured it out.
The problem was not on my Context but on the caller. I needed to use a useMemo() there to have memoized the part of the component that didnt need to update.

What's the difference between useObserver and observer in this application?

I have a react functional component that accesses the MobX store with useContext. I have found two ways to observe an array that is an observable from the store. First, the useObserver hook and wrapping the component with observer.
I thought that these are the same but that the useObserver only observes specific properties (such as the array that is passed) but I am experiencing a problem when the array reaches size 2 and then the component does not re-render. That's the case when using useObserver. When wrapping with observer, this is fixed.
Can anyone explain why this is happening and what's the difference?
const ApplesContainer = observer(() => {
const stores = useStores();
const applesArray = stores.fruits.apples;
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
// OR with useObserver()
function useGlobalState() {
const stores = useStores();
return useObserver(() => ({
applesArray: stores.fruits.apples
}));
}
const ApplesContainer = observer(() => {
const { applesArray } = useGlobalState();
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
useObserver must return JSX with an observable value.
This hook takes care of tracking changes and re-rendering them.
If no observable value exists in JSX, then it won't be re-rendered.
e.g.:
const SomeContainer =() => {
const { someStores } = useStores();
return useObserver(()=>(
{someStore.data.map(val => (
<Apple key={val.id} val={val} />
))}
));
};

Incorrect use of useEffect() when filtering an array

I have this React app that's is getting data from a file showing in cards. I have an input to filter the cards to show. The problem I have is that after I filter once, then it doesn't go back to all the cards. I guess that I'm using useEffect wrong. How can I fix this?
import { data } from './data';
const SearchBox = ({ onSearchChange }) => {
return (
<div>
<input
type='search'
placeholder='search'
onChange={(e) => {
onSearchChange(e.target.value);
}}
/>
</div>
);
};
function App() {
const [cards, setCards] = useState(data);
const [searchField, setSearchField] = useState('');
useEffect(() => {
const filteredCards = cards.filter((card) => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
});
setCards(filteredCards);
}, [searchField]);
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={cards} />
</div>
);
}
you should Include both of your state "Card", "searchedField" as dependincies to useEffect method.once any change happens of anyone of them, your component will re-render to keep your data up to date,
useEffect(() => { // your code }, [searchField, cards]);
cards original state will be forever lost unless you filter over original data like const filteredCards = data.filter().
though, in a real project it's not interesting to modify your cards state based on your filter. instead you can remove useEffect and create a filter function wrapped at useCallback:
const filteredCards = useCallback(() => cards.filter(card => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
}), [JSON.stringify(cards), searchField])
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={filteredCards()} />
</div>
);
working example
about array as dependency (cards)
adding an object, or array as dependency at useEffect may crash your app (it will throw Maximum update depth exceeded). it will rerun useEffect forever since its object reference will change everytime. one approach to avoid that is to pass your dependency stringified [JSON.stringify(cards)]

How to update useRef after redux update

I'm using useRef to hold the reference of a function that is invoked from a child component (TaskItem) when it is clicked on:
UserTask -> TaskList -> TaskItem
// UserTask.js
const [detailsVisible, setDetailsVisible] = useState(false)
const [selectedTask, setSelectedTask] = useState({})
const openTask = (id) =>{
const task = props.tasks.find(task => task.id === id);
setDetailsVisible(true);
setSelectedTask(task);
}
const openTaskRef = useRef(openTask);
const openTaskHandler = useCallback(id =>{
openTaskRef.current(id);
}, []);
return (
<React.Fragment>
<TaskList
tasks={props.tasks}
onClick={openTaskHandler}
/>
<Modal visible={detailsVisible} onClose={()=> setDetailsVisible(false)}>
<Comments items={selectedTask.comments || []} />
</Modal>
</React.Fragment>
)
The list of tasks arrives via redux->props a few seconds after they are fetched from an API:
// Still UserTask.js
useEffect(()=>{
fetchTasks()
}, [fetchTasks])
const mapDispatchToProps = dispatch =>{
return {
fetchTasks: ()=> dispatch(fetchTasks())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UserTasks)
Now, my problem is that when the component (UserTask) is rendered the first time, the 'openTaskRef' gets created with a context in which props.tasks is empty (since the API request hasn't been issued yet). I need to use the useRef/useCallback combination to avoid re-rendering of TaskItems when the props.tasks list is updated after an item is removed (a scenario not described in the code above).
How can I control this scenario? How can I update the reference to openTask to include the most recent list of tasks?
Thanks.

useLoopCallback -- useCallback hook for components created inside a loop

I'd like to start a discussion on the recommended approach for creating callbacks that take in a parameter from a component created inside a loop.
For example, if I'm populating a list of items that will have a "Delete" button, I want the "onDeleteItem" callback to know the index of the item to delete. So something like this:
const onDeleteItem = useCallback(index => () => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<div>
<span>{item}</span>
<button type="button" onClick={onDeleteItem(index)}>Delete</button>
</div>
)}
</div>
);
But the problem with this is that onDeleteItem will always return a new function to the onClick handler, causing the button to be re-rendered, even when the list hasn't changed. So it defeats the purpose of useCallback.
I came up with my own hook, which I called useLoopCallback, that solves the problem by memoizing the main callback along with a Map of loop params to their own callback:
import React, {useCallback, useMemo} from "react";
export function useLoopCallback(code, dependencies) {
const callback = useCallback(code, dependencies);
const loopCallbacks = useMemo(() => ({map: new Map(), callback}), [callback]);
return useCallback(loopParam => {
let loopCallback = loopCallbacks.map.get(loopParam);
if (!loopCallback) {
loopCallback = (...otherParams) => loopCallbacks.callback(loopParam, ...otherParams);
loopCallbacks.map.set(loopParam, loopCallback);
}
return loopCallback;
}, [callback]);
}
So now the above handler looks like this:
const onDeleteItem = useLoopCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
This works fine but now I'm wondering if this extra logic is really making things faster or just adding unnecessary overhead. Can anyone please provide some insight?
EDIT:
An alternative to the above is to wrap the list items inside their own component. So something like this:
function ListItem({key, item, onDeleteItem}) {
const onDelete = useCallback(() => {
onDeleteItem(key);
}, [onDeleteItem, key]);
return (
<div>
<span>{item}</span>
<button type="button" onClick={onDelete}>Delete</button>
</div>
);
}
export default function List(...) {
...
const onDeleteItem = useCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
)}
</div>
);
}
Performance optimizations always come with a cost. Sometimes this cost is lower than the operation to be optimized, sometimes is higher. useCallback it's a hook very similar to useMemo, actually you can think of it as a specialization of useMemo that can only be used in functions. For example, the bellow statements are equivalents
const callback = value => value * 2
const memoizedCb = useCallback(callback, [])
const memoizedWithUseMemo = useMemo(() => callback, [])
So for now on every assertion about useCallback can be applied to useMemo.
The gist of memoization is to keep copies of old values to return in the event we get the same dependencies, this can be great when you have something that is expensive to compute. Take a look at the following code
const Component = ({ items }) =>{
const array = items.map(x => x*2)
}
Uppon every render the const array will be created as a result of a map performed in items. So you can feel tempted to do the following
const Component = ({ items }) =>{
const array = useMemo(() => items.map(x => x*2), [items])
}
Now items.map(x => x*2) will only be executed when items change, but is it worth? The short answer is no. The performance gained by doing this is trivial and sometimes will be more expensive to use memoization than just execute the function each render. Both hooks(useCallback and useMemo) are useful in two distinct use cases:
Referencial equality
When you need to ensure that a reference type will not trigger a re render just for failing a shallow comparison
Computationally expensive operations(only useMemo)
Something like this
const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}
Now you have a reason to memoized the operation and lazily retrieve the serializedValue everytime props.item changes:
const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])
Any other use case is almost always worth to just re compute all values again, React it's pretty efficient and aditional renders almost never cause performance issues. Keep in mind that sometimes your efforts to optimize your code can go the other way and generate a lot of extra/unecessary code, that won't generate so much benefits (sometimes will only cause more problems).
The List component manages it's own state (list) the delete functions depends on this list being available in it's closure. So when the list changes the delete function must change.
With redux this would not be a problem because deleting items would be accomplished by dispatching an action and will be changed by a reducer that is always the same function.
React happens to have a useReducer hook that you can use:
import React, { useMemo, useReducer, memo } from 'react';
const Item = props => {
//calling remove will dispatch {type:'REMOVE', payload:{id}}
//no arguments are needed
const { remove } = props;
console.log('component render', props);
return (
<div>
<div>{JSON.stringify(props)}</div>
<div>
<button onClick={remove}>REMOVE</button>
</div>
</div>
);
};
//wrap in React.memo so when props don't change
// the ItemContainer will not re render (pure component)
const ItemContainer = memo(props => {
console.log('in the item container');
//dispatch passed by parent use it to dispatch an action
const { dispatch, id } = props;
const remove = () =>
dispatch({
type: 'REMOVE',
payload: { id },
});
return <Item {...props} remove={remove} />;
});
const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }];
//Reducer is static it doesn't need list to be in it's
// scope through closure
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
//remove the id from the list
return state.filter(
item => item.id !== action.payload.id
);
}
return state;
};
export default () => {
//initialize state and reducer
const [list, dispatch] = useReducer(
reducer,
initialState
);
console.log('parent render', list);
return (
<div>
{list.map(({ id }) => (
<ItemContainer
key={id}
id={id}
dispatch={dispatch}
/>
))}
</div>
);
};

Resources