Avoid re render with react hooks - reactjs

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

Related

How to use mapStateToProps to get a value from the state based on a context value?

I've an application when most of the data are stored in the store but the selected item is provided thought the usage of a React.Context.
React-Redux provide the connect api that accept a mapStateToProps function with state and props as a component.
What I would like, if it didn't break the hooks, is something like:
function mapStateToProps(state){
const selectedItemId = useContext(MySelectedItemContext)
return {
item: state.items[selectedItemId]
}
}
but of course it is not possible since I'm outside of the component and cannot invoke the useContext.
So, I tried to use the old API context:
function mapStateToProps(state, props){
return {
item: state.items[props.id]
}
}
export default connect(mapStateToProps)((props) =>
<MySelectedItemContext.Consumer>
{ selectedItemId => <Component id={selectedItemId} {...props}/> }
</MySelectedItemContext.Consumer>)
but this still not works because the connect returns a new component that has the consumer inside instead of outside and id prop is not defined yet in mapStateToProps.
Any idea?
The best way is to remove mapStateToProps and use useSelector hooks and Redux selectors. But if you need mapStateToProps, then you can wrap your component that must be connected to Redux into another component that will get value from context and will pass it to a component that uses Redux.
// Use this component
export function ContextConsumerComponent() {
const selectedItemId = useContext(SelectedItemIdContext);
return <ReduxConsumerComponent id={selectedItemId} />;
}
function mapStateToProps(state, props) {
return {
item: state.items[props.id]
}
}
const ReduxConsumerComponent = connect(mapStateToProps)((props) => {
// props.item will be here
});

How do I use react-redux 'useSelector' with an additional variable?

I have a redux (sub) state that consists of a large number of similar entries.
export type PartnerCalculatorStateShape = {
m16_19:number;
m20_24:number;
m25_34:number;
m35_44:number;
m45_64:number;
m65_plus:number;
f16_19:number;
f20_24:number;
f25_34:number;
f35_44:number;
f45_64:number;
f65_plus:number;
};
I am using the Redux Toolkit so my reducer is of this form (note that Redux Toolkit uses immutable update logic, so I can assign modify the state directly)
type PartnerCalculatorPayload = {
key:string;
value:number;
}
export const partnerCalculatorSlice = createSlice({
name: 'PartnerCalculator',
initialState,
reducers: {
partnerCalculatorValueReceived(state, action: PayloadAction<PartnerCalculatorPayload>) {
state[action.payload.key] = action.payload.value;
}
}
});
I'm a bit stuck on how to use useSelector. What I want to do is have a selector function in my Redux file, something like this
export const selectorFunction = (state,key) => state[key]
where key would be m20_24, for example. Then I would use that selector function in my React component
const myVar = useSelector(selectorFunction)
But how do I pass in the key?
The official hooks documentation recommends using closure to pass additional variables
import React from 'react'
import { useSelector } from 'react-redux'
export const TodoListItem = props => {
const todo = useSelector(state => state.todos[props.id])
return <div>{todo.text}</div>
}
However. useSelector will be in my React component and I want to keep the selector function I pass to useSelector inside my redux file, so I can't see how to use closure.
I suppose I could just pass the entire state out from my selector function
const selectorFunction = state => state
and then treat it as an object in my React component and key into it there
const myState = useSelector(selectorFunction)
const myVar = myState["m20_24"]
but that seems kind of ugly.
If that's the way to go, would myVar update anytime any of the fields in my Redux state changed? I'm a bit unclear as to how the useSelector equality testing mechanism works -- it says it uses 'strict equality', so if any part of my state object changed (that is, if the field 'm20_24' changed) then myVar would be updated?
Thanks for any insights!
Pass an anonymous selector to useSelector, and then call the actual selector inside of there:
const value = useSelector(state => selectValueByKey(state, props.key));

React usEffect firing when it shouldn't be

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

How to compare previous context with current context in React 16.x?

Is there a way to compare previous context value with the current context value in the consumer child component's lifecycle methods?
If there is no way to do that, is there any other workaround or an alternate solution to this?
FYI: I'm using react v16.8.1
Using HOC:
const withContext = Wrapped => props => (
<SomeContext.Consumer>
{value => <Wrapped {...props} contextValue={value}/>}
</SomeContext.Consumer>
);
const MyComponent = withContext(class extends React.Component {
...
componentDidUpdate(prevProps) {
if (prevProps.contextValue !== this.props.contextValue) {
...
}
}
...
});
Using hooks:
function MyComponent(props) {
...
const contextValue = useContext(SomeContext);
const [oldContextValue, saveContextValue] = useState(contextValue);
useEffect(() => {
console.log(oldContextValue, contextValue);
saveContextValue(contextValue);
}, [contextValue]);
...
}
Note that useEffect runs only on the first render and when contextValue changes. So if you don't really need old contextValue for something, you don't need useState.
Usually if you're going to have dynamic context its value will most likely come from a state of a parent component.
So why not detect changes in the state instead of trying to detect changes in the Provider?
Like in the example for dynamic context in the react documentation : https://reactjs.org/docs/context.html#dynamic-context

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