React usEffect firing when it shouldn't be - reactjs

I have a react component with a prop that is passed by a redux connect method. There is a useEffect linked specifically to that prop that is supposed to perform an async call when it changes. The problem is the useEffect fires any time I change the redux state anywhere else in that app, despite the prop I have the useEffect attached to not changing.
The useEffect method looks like this
useEffect(() => {
if (userPhoneNumber) {
myAsyncFunction()
.then(() => {
showData()
})
}
}, [userPhoneNumber])
And the userPhoneNumber prop is passed via the react-redux connect method like so:
const mapStateToProps = state => {
return {
userPhoneNumber: state.appState.userPhoneNumber
}
}
export default connect(mapStateToProps)(MyComponent)
From what I understand this could be breaking in two potential places. for one, useEffect should not be firing if the userPhoneNumber prop doesn't change. Also, the mapStateToProps method is not returning a new value so it should not be triggering any sort of a rerender.
The redux state changes that are leading to the unexpected useEffect call are coming from sibling components that have a similar react-redux connect setup to this component, with different state to prop mappings.

When a reducer runs you'll have an entirely new state object, thus a new userPhoneNumber prop reference. You should memoize the userPhoneNumber value. I use reselect to create memoized state selectors, but you could probably use the useMemo hook and use that memoized value in the dependency for the effect.
const memoizedUserPhoneNumber = useMemo(() => userPhoneNumber, [userPhoneNumber]);
useEffect(() => {
if (memoizedUserPhoneNumber) {
myAsyncFunction()
.then(() => {
showData()
})
}
}, [memoizedUserPhoneNumber]);
Using Reselect
import { createSelector } from 'reselect';
const appState = state => state.appState || {};
const userPhoneNumber = createSelector(
appState,
appState => appState.userPhoneNumber;
);
...
const mapStateToProps = state => {
return {
userPhoneNumber: userPhoneNumber(state),
}
}
None of this helps keep the entire functional component from re-rendering when the parent re-renders though, so to help with this you'll need to help "memoize" the props being fed into the component with the memo HOC.
export default memo(connect(mapStateToProps)(MyComponent))

Related

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
}

Does useSelector automatically unsubscribe the component from store updates when it is unmounted?

I was looking through the source code for useSelector as part of the react-redux library.
I was wondering if useSelector automatically unsubscribes components from redux store updates when the component unmounts, similar to using the unsubscribe function returned by calling store.subscribe in Redux? Or if another component changes that piece of state, will this still be subscribed to such changes?
Yes, useSelector explicitly unsubscribes from the store when the component unmounts. Per https://github.com/reduxjs/react-redux/blob/v7.2.2/src/hooks/useSelector.js#L81 :
useIsomorphicLayoutEffect(() => {
function checkForUpdates() {
try {
const newSelectedState = latestSelector.current(store.getState())
if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}
latestSelectedState.current = newSelectedState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err
}
forceRender()
}
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
checkForUpdates()
return () => subscription.tryUnsubscribe() // unsubscribes here
}, [store, subscription])

Mixing Redux with useEffect Hook

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

Avoid re render with react hooks

I have some value that comes from Redux through props and I want to NOT render the component again when this value changes.
I found some answers saying that I can use Memo but I don't know if this is the best option for my case?
My "code":
const MyComponent = () => {
return ...;
}
const mapStateToProps = state => ({
myVar: state.myVar
});
export default connect(mapStateToProps)(MyComponent);
myVar changing shouldn't re render the component in this case.
React.memo can do the job, you can pass a custom equality check function to perform a rerender only when it returns a falsy value. I never faced a case where you want to completely ignore a value update from your Redux store, maybe it shouldn't be stored there ?
Memo API
eg: React.memo(Component, [areEqual(prevProps, nextProps)])
UseSelector API
Another way would be to use useSelector with a custom equality check function:
useSelector Redux API Reference
Connect API
If you still want to stick with mapStateToProps, you can also pass a custom equality check function as a parameter of the connect function:
areStatePropsEqual Redux API Reference
Edit: useRef solution
By using useRef, you store a mutable variable that will be kept as it is for the whole lifetime of the component.
Example based on yours:
const StoreMyVar = (WrappedComponent) => ({myVar, ...props}) => {
const myRefVar = useRef(myVar)
return <WrappedComponent myVar={myRefVar} {...props} />
}
const MyComponentWithImmutableVar = StoreMyVar(MyComponent)
The fastest way is React.memo, but you can use it just with functional components. Be careful, it is not tested.
const MyComponent(props) {
return ...;
}
const areEqual(prevProps, nextProps) {
return prevProps.myVar === nextProps.myvar
}
const mapStateToProps = state => ({
myVar: state.myVar
});
export default React.memo(MyComponent, areEqual);

How to fetch data by existing redux action with hooks?

I'm trying to understand React Hooks. What I want is to fetch data inside functional component by call redux action with useEffect hooks.
I know that I can pass props to state like
const [todoList] = useState(props.todoList)
But what is the best practice to fetch data by existing redux actions?
In React class component i call this method to fetch data in componentDidMount() and everythink works.
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { ITodo } from './types'
import { getTodos } from '../actions/todoActions'
interface IProps {
todoList: Array<ITodo>
getTodos: typeof getTodos
}
const Todos = (props: IProps) => {
useEffect(() => {
props.getTodos()
}, [props.todoList])
return (
<div>
{props.todoList.map((_) => (<div key={_.Id}>{_.Name}</div>))}
</div>
)
}
const mapStateToProps = (state) => ({
todoList: state.todo.todoList
})
const mapDispatchToProps = {
getTodos
}
export default connect(mapStateToProps, mapDispatchToProps)(ProdRoute)
I expected to get list of todos with props and props.getTodos() should call once like in componentDidMount() method. But actualy I get data and getTodos() are called over and over again but should be called once on component mount
Take care that if you pass [props.todoList] to the useEffect you are erroneously forcing a constant refresh because:
useEffect does an instance comparison (===) to know if props.todoList is changed
after the the very first render the props.getTodos() dispatcher is called
when the props.todoList will be updated the component is re-rendered
the useEffect call will receive [props.todoList] as a value to check if it needs to re-run or not
props.todoList is changed (it was empty and now it's valorized) and props.getTodos() is so re-called
redux updates the todoList with the same values but mutating the array reference
the component is re-rendered and the useEffect will check if the [props.todoList] param is been updated... but IT IS BEEN UPDATED because the previous props.todoList is different from the actual props.todoList, even if the content is the same
So, if you need to call the props.getTodos() just once can
use [props.todoList.length] instead of [props.todoList] as the second parameter for the useEffect call
use an empty array [] as the second parameter for the useEffect call (see the docs)

Resources