Mixing Redux with useEffect Hook - reactjs

I read that this is theoretically OK. In my small use case, I'm running into an issue, where mixing those technologies leads to double re-rendering.
First, when redux dispatch is executed and some components use a prop via useSelector. Then, after the functional component is already re-rendered, a useEffect hook is being applied which updates the state of some property. This update re-triggers the render again.
E.g. the below console log prints out twice in my case.
Question: should I remove the useEffect and useState and integrate it into redux' store?
import {useSelector} from "react-redux";
import React from "react";
const Dashboard = (props) => {
const selFilters = useSelector((state) => state.filter.selectedDashboardFilters);
const [columns, setColumns] = React.useState([]);
React.useEffect(() => {
let newColumns = getColumns();
setColumns(newColumns)
}, [selFilters]
)
console.log("RENDER")
return (
<h1>{columns.length}</h1>
)
}

If columns needs to be recomputed whenever selFilters changes, you almost certainly shouldn't be recomputing it within your component. If columns is computed from selFilters, then you likely don't need to store it as state at all. Instead, you could use reselect to create a getColumns() selector that derives the columns from the state whenever the relevant state changes. For example:
const getColumns = createSelector(
state => state.filter.selectedDashboardFilters,
selFilters => {
// Compute `columns` here
// ...
return columns
}
)

Related

React Hooks, how to prevent unnecessary rerendering

I have a hook that gets data from another hook
const useIsAdult = () => {
const data = useFormData();
return data.age > 18;
}
This hook returns true or false only, however the useFormData is constantly being updated. Everytime useFormData reruns, it will rerender my component. I only want my component to rerender if the result of useIsAdult changes.
I know this can obviously be solved by implementing some logic via react-redux and using their useSelector to prevent rendering, but I'm looking for a more basic solution if there is any.
Before you say useMemo, please read question carefully. The return value is a boolean, memo here doesnt make any sense.
Even with returned useMemo in useIsAdult parent component will be rerendered. This problem is reason why I rarely create custom hooks with other hooks dependency. It will be rerender hell.
There I tried to show that useMemo doesnt work. And using useMemo for components its wrong way. For components we have React.memo.
const MyComponent = memo(({ isAdult }: { isAdult: boolean }) => {
console.log("renderMy");
return <h1>Hello CodeSandbox</h1>;
});
And memo will help to prevent rendering of MyComponent. But parent component still render.
https://codesandbox.io/s/interesting-lumiere-xbt4gg?file=/src/App.tsx
If you can modify useFormData maybe useRef can help. You can store data in useRef but it doesnt trigger render.
Something like this but in useFormData:
onst useIsAdult = () => {
const data = useFormData();
const prevIsAdultRef = useRef();
const isAdult = useMemo(() => data.age > 18, [data]);
if (prevIsAdultRef.current === isAdult) {
return prevIsAdultRef.current;
}
prevIsAdultRef.current = isAdult;
return isAdult;
};
I dont know how useFormData works, then you can try it self with useRef.
And if useFormData gets data very often and you cant manage this, you can use debounce or throttle to reduce number of updates
Memoize hook
const useIsAdult = () => {
const data = useFormData();
return useMemo(() => data.age > 18, [data.age]);
}
Here, useMemo will let you cache the calculation or multiple renderes, by "remembering" previous computation. So, if the dependency (data.age) doesn't change then it will use simply reuse the last value it returned, the cached one.
Memoize component expression
useMemo can also be used to memoize component expressions like this: -
const renderAge = useMemo(() => <MyAge age={data.age} />, [data.age]);
return {renderAge}
Here, MyAge will only re-render if the value of data.age changes.

Difference between redux nested selector and non-nested selector

I have seen many example codes of redux and found that we use useSelector in mostly 2 ways.
nested
import React from 'react'
import { useSelector } from 'react-redux'
export const UserComponent = () => {
const name = useSelector((state) => state.user.name)
const age = useSelector((state) => state.user.age)
return <div>{name}, {age}</div>
}
non-nested
import React from 'react'
import { useSelector } from 'react-redux'
export const UserComponent = () => {
const user = useSelector((state) => state.user)
return <div>{user.name}, {user.age}</div>
}
As you can see, with approach 2, I will end up saving lots of code repetition and I only need to create 1 selector.
So I wanted to know the difference, any performance benefits, etc.
There is a difference between these two approaches, let's dive deeper into the useSelector hook and see how it works. So let's say you're the creator of this hook and you define it as:
// this is your redux state
const state = { user: { name: 'Haseeb', age: 10 } };
// defining our own selector hook
const useSelector = (selectorFunction) => {
const accessedProperty = selectorFunction(state);
// accessedProperty will contain whatever is returned from the selectorFunction
return accessedProperty;
}
Now, let's use our hook
// nested, subscribe to just the name property in user
const name = useSelector((state) => state.user.name);
// non-nested, subscribe to the complete user object
const user = useSelector((state) => state.user);
Talking about the performance, When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render.
So in your nested approach, useSelector will only do a reference equality comparison of the name value. AND in your non-nested approach, useSelector will do a reference equality comparison of the complete user object. In both cases, the result of the comparison determines whether the component should re-render. Chances are the non-nested approach will cause more re-renders than the nested approach because the non-nested approach is subscribing to more values than the nested approach.
Call useSelector() multiple times, with each call returning a single field value but it is also okay to use one useSelector() if further performance optimizations are not necessary

How to prevent extra hook calls on redux state change?

I have a custom hook: useNavigation(). It returns the target route to redirect the user if it is required.
Inside the hook I use redux store (only to read values):
{ prop1, prop2 } = useTypedSelector(state => state.data);
Where useTypedSelector code is:
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
Where TypedUseSelectorHook and useSelector hooks come from react-redux and RootState is a type of my root reducer.
I have the following actual result:
If redux store changes, then the change triggers unexpected useNavigation hook call.
But my expected result is:
useNavigation hook uses redux store to read values, but they don't triggers useNavigation hook call on change.
How to prevent extra hook calls on redux state change?
You always want to select the minimum amount of data that you need with useSelector. Any changes to the value that is returned from useSelector will trigger a re-render.
You can make it marginally better by selecting prop1 and prop2 separately so that changes to other properties in state.data don't trigger re-rendering.
const prop1 = useTypedSelector(state => state.data.prop1);
const prop2 = useTypedSelector(state => state.data.prop2);
But I suspect that you can make it much better by moving some of the logic that is happening in your hook into a custom selector function instead. You may want to use reselect to memoize your selector.
const selectTargetRoute = (state: RootState) => {
const {prop1, prop2} = state.data;
// do some logic
// return a route or null/undefined
}
const useNavigation = () => {
const targetRoute = useTypedSelector(selectTargetRoute);
// do something with the route
}

React - reusable Redux hook slice issue (Redux Toolkit, Typescript)

I'm trying to create some hooks to fetch slices of the Redux store so that the store can't access directly to return anything.
If I useSelector directly in the component it works great and doesn't unnecessarily re-render:
*** Modal.tsx ***
const { isModalOpen } = useSelector((state: RootState) => state.carsSlice); // Works great. No unnecessary re-renders
If I create a hook to return the carsSlice then it unnecessarily re-renders the whole page. Why?
E.g.,
*** StateHooks.ts ***
const useGlobalState = () => useSelector((state: RootState) => state);
export const useCarsState = () => useGlobalState().carsSlice;
*** Modal.tsx ***
const { isModalOpen } = useCarsState(); // Re-renders underlying page and is significantly slower than above approach
I'm fetching the specific value from the state so I don't understand why it would re-render the whole page it is being used on? Is there a way to do this?
I've also tried the below but it still causes page re-render:
const useCarsState = useSelector((state: RootState) => state.carsSlice); // Same result
The only way it works as expected is the below BUT I want the custom hooks above:
const { isModalOpen } = useSelector((state: RootState) => state.carsSlice); // Works great
Thanks all.
you can use useShallowEqualSelector
It will do only rerender when the value from the store is changed.
Moreover, you can put this into a separate hook like:
import { TypedUseSelectorHook, useSelector, shallowEqual } from "react-redux";
import { RootState } from "store/rootReducer";
const useShallowEqualSelector: TypedUseSelectorHook<RootState> = (selector) =>
useSelector(selector, shallowEqual);
export default useShallowEqualSelector;
in this case, you are able to use this hook everywhere and you have access to the hints from typescript
useSelector works by doing reference comparisons of the value you return. If that value changes, it forces the component to re-render.
Because of that, you should never return the entire root state from `useSelector! That will cause the component to always re-render.
It doesn't matter what additional destructuring you do with the returned value later - what matters is what the selector itself returns.
That's why you should always select the smallest piece of state needed by a given component, to ensure that it only re-renders when it absolutely needs to because its data actually changed.

React Component should mount previous state before fetching new data

DataContext.js
export const DataContext = createContext()
const DataContext = ({ children }) => {
const [userValue, setUserValue] = useState()
const [user, setUser] = useState()
const fetch = useCallback(async () => {
const response = await axios(Url)
setBalance(response.data)
}
}, [])
usePrevious.js
import { useEffect, useRef } from 'react'
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
export default usePrevious
UsersData Component
import { DataContext } from '../context/DataContext'
import usePrevious from './usePrevious'
const UsersValue = () => {
const { userValue} = useContext(DataContext)
const prevData = usePrevious(useValue)
useEffect(() => {
console.log('prevData ', prevData)
console.log('userValue', userValue)
}, [useValue, prevData])
return (....)
Following the React documentation using usePrevious example.
When I render the component for the first time it takes few sec to load the data(async), this is fine, but if I route away to another link and come back, I still get the component loading the data and I want to fetch the previous state if the data has not changed
I was specting the console.log for prevData to show the previous state(when linked back to component) while component is fetching, but is just showing the same for userValue, both have the same state undefined, than data comes is available
How can I render a memorized state for the UsersData Component,to avoid loading all the time when component is mounting, I start looking into react.memo, is this the direction I need to take?
If so how can I implement React.memo in this case, thx
I don't think usePrevious will help you if you are unmounting and re-mounting the component, the ref will be recycled. You could pass down the fetch function from the context, and call it in the child component when it mounts. This fetch would run in the context, and if there is a difference between the new and data stored in the context, update the userValue using setUserValue in the context. This change would then be passed down to the child after the state has been updated. If they are the same, do not call setUserValue and no update will occur.
The problem here is if there is a new value, the child component won't know until the fetch has finished. You may be displaying old data off the bat, but if you don't tell the user you are fetching, the child component may update unexpectedly when the fetch is finished and the context is updated.
Caching is difficult, you may just want to always provide a fresh value.
If you decide to go this route, you will need to check if the data you fetched has changed. If the data is an object and doesn't have functions added to it, a quick way is to use JSON.stringify on both objects and compare the stringified values.

Resources