I am trying to implement shouldComponentUpdate method in a functional component.
Here is the original method (classfull component)
shouldComponentUpdate(nextProps, nextState) {
return (
this.state.isSelected || nextState.isSelected !== this.state.isSelected
);
}
But, with React memo it seems that I can only access prevProps and nextProps, not nextState.
memo(..., (prevProps, nextProps) => true);
How can I access nextState in a functional component to simulate the shouldComponentUpdate?
The purpose of react memo is to only check when props changes not state. If you try to do so, you are breaking the rule - the purpose of pure component functionality. What you want is to use the effect hook:
useState(()=>{
//...
}, [state that you want to check])
So, with this requirement, avoid using memo as it is not pure component. Component will render when state change, so there's no pure functionality.
While performing shouldComponentUpdate, you don't need to pass dependency array, just check the condition and then you make useEffect as being used as shouldComponentUpdate.
useState(()=>{
//check condition here and do logic
})
you can use this :
const Component = {....}
const handleMemoProps = (prevProps, nextProps) => {
return true or false
}
export default React.memo(Component, handleMemoProps)
Related
I have a functional component which has has a connection to an object in my Redux store. To avoid re-renders when the object has changed I added React.memo, however, this didn't avoid the re-renders. My Memo looks something like this:
const MemoizedRadio = React.memo(Radio, (prevProps, nextProps) => {
if (prevProps.selected !== nextProps.selected) {
return false;
}
return true;
});
const mapStateToProps = state => ({
nodes: state.canvas.elements.nodes
});
export default connect(mapStateToProps)(MemoizedRadio);
I would expect this component to not re-render if the nodes object has changed, but it does.
When I rewrite my component to a class component and add a shouldComponentUpdate the re-render will be prevented as expected. The shouldComponentUpdate looks as followed:
shouldComponentUpdate(nextProps) {
if (this.props.selected !== nextProps.selected) {
return true;
}
return false;
}
I thought that React.memo acted the same as shouldComponentUpdate but this does not seem to be the case. The React.memo implementation does always re-render when the reference to the nodes object changes while the shouldComponentUpdate implementation prevents the re-render as expected.
Can anyone explain this behaviour?
You are using React.memo correctly, the problem is that you are using connect an HOC to connect class based components to the store. Instead of using HOC you should use useDispatch and useSelector
I am trying to figure out a way to be able to memoize React components by specifying particular props.
For instance, if you use React.memo — it memoizes the component based on all props.
What I am trying to achieve is being able to pass particular props as a dependency to a util (say, SuperMemo) and the component will be memoized based on those props. The approach is very similar to what recompose — compose the component before export.
Here's an example code
import React from "react";
const isFunction = value =>
value &&
(Object.prototype.toString.call(value) === "[object Function]" ||
"function" === typeof value ||
value instanceof Function);
export const memo = (Comp, resolver) => {
if (isFunction(resolver)) {
const Memoized = props => {
const deps = resolver(props);
if (deps && deps.length) {
// eslint-disable-next-line react-hooks/rules-of-hooks
return React.useCallback(React.createElement(Comp, props), deps);
}
return React.createElement(Comp, props);
};
Memoized.displayName = `memoized(${Comp.name})`;
return Memoized;
}
return React.memo(Comp);
};
export default memo;
Here is how it will be used to compose components
import Todo from "./Todo";
import memo from "../memo";
export default memo(Todo, props => [props.text]);
I have a working codesandbox here — memo-deps
This is what I have observed —
I should not use React.useCallback or any hook inside a conditional statement because React needs to know the order in which hooks are invoked and using it inside a conditional may mess up the order during runtime
But React.useCallback works pretty neat in a conditional for my case as I know the order will remain the same during runtime
I am not using the hook inside the conditional statement during render, instead I am composing the component during export conditionally
I am thinking about React components as plain JavaScript functions and trying to memoize it like how I would memoize a regular JavaScript function
I could easily replace React.useCallback with lodash.memoize and the end result will be pretty much the same
I don't want to use an external library like lodash.memoize or build a custom implementation of memoization while React.useCallback pretty much does the work for me
This is where I am not sure what's happening (these are my questions) —
React components are not really vanilla JavaScript functions and I cannot memoize them with lodash.memoize
lodash.memoize and React.useCallback are not the same when I try to memoize a React component
React executes the function before figuring out the render even when React.memo is used (maybe to check prevProps vs newProps?)
Is my implementation okay even though it breaks the rules of React? (use hook in a conditional statement)
How else can I memoize a React.createElement if not for React.useCallback?
The reason as to why I might want to do this —
I don't want to memoize handlers (closure with a value and event) every time I pass them to a component wrapped in React.memo. I want to be able to declaratively write memoize dependencies for components.
React.memo accepts a function as the second parameter to do a custom props comparison.
By default it will only shallowly compare complex objects in the props
object. If you want control over the comparison, you can also provide
a custom comparison function as the second argument.
You can use that in your util function like this :
export const memoWithSecondParam = (Comp, deps = []) => {
return React.memo(Comp, (prevProps, nextProps) => {
return deps.every(d => prevProps[d] === nextProps[d])
});
};
And call it like this :
export default memoWithSecondParam(Todo, ["text"]);
When sending props to a PureComponent or a functional component, you can optimize performance by using props that don't change for every render, which will prevent the component from re-rendering.
When using class components this is simple:
class Component extends React.Component {
render() {
return <List createRows={this.createRows}/>;
}
createRows = () => this.props.data.map(dataToRow);
}
Given List being either a PureCompoment or a functional component, the createRows prop will never cause a re-render of List.
But if the Component is a functional component, this is no longer possible:
function Component(props) {
return <List createRows={createRows}/>;
function createRows() {
return props.data.map(dataToRow);
}
}
Since createRows is created every time Component renders, the prop will change, causing a re-render of List every time Component is re-rendered. This can cause a big loss in performance. Notice also that the createRows cannot be placed outside the functional component, since it is dependent on the data prop of List.
Now, with the introduction on Hooks, it is possible to hold the createRows in a useState hook:
function Component(props) {
const [ createRows ] = useState(() => () =>
props.data.map(dataToRow);
);
return <List createRows={createRows}/>;
}
Since the createRows is saved in a state hook, it will not change with each render, and no re-render of List will occour, like we want.
However, this seems more like a hack than a solution.
What is best practice for sending a function prop from a functional components to a child component, without causing unnecessary re-renders of the child component?
useCallback hook exists exactly to solve this problem. I advise you to carefully read the official guide to hooks, it pretty much answers all possible questions
function Component(props) {
const createRows = useCallback(() =>
props.data.map(dataToRow);
), []); // provide dependencies here
return <List createRows={createRows}/>;
}
This is the purpose of useCallback. You can find more details in some of my related answers below.
Trouble with simple example of React Hooks useCallback
What is the intension of using React's useCallback hook in place of useEffect?
React Hooks useCallback causes child to re-render
Since I have a component with forms, I need the forms to be connected to the component state. The initial data comes from Redux so try to initialize and update the component by setting the state with the props:
componentWillMount = () => {
this.setState({
language: this.props.language || 'en'
})
}
language is a connected prop and I checked that it is updated in the store.
const mapStateToProps = state => ({
language: state.language
})
I also tried to use componentWillReceiveProps and componentWillUpdate but it doesn't work. I get the initial state, and even though the store and the connected props change, the component's state doesn't update.
{this.props.language} // updates
{this.state.language} // doesn't change
What is the correct way to manage forms from Redux data?
The render part:
render () {
const {classes, theme, web} = this.props
const language = (
<CardContent>
<Typography type="headline">
Language
</Typography>
<Divider/>
<form className={classes.container} autoComplete="off">
<FormControl fullWidth margin="normal">
<InputLabel htmlFor="language">Select Block</InputLabel>
<Select
value={this.state.language} // <==================== language
onChange={this.handleLanguaheChange}
input={<Input id="language"/>}
>
<MenuItem value={'en'}>English</MenuItem>
<MenuItem value={'he'}>עברית</MenuItem>
</Select>
</FormControl>
</form>
</CardContent>
)
...
return (
<Grid
container
spacing={theme.spacing.unit * 3}
justify={'space-between'}
className={classes.gridWrap}
>
<Grid item xs={6}>
<Card className={classes.card}>
{language}
</Card>
...
First, you are using an arrow function for componentWillMount. Rule of thumb is, do not use arrow functions for life-cycle hooks(componentWillMount, shouldComponentUpdate, ...etc). It's usual to setState in componentWillMount hook. But never set state in componentDidMount.
please try to re-write it as,
constructor(props) {
super(props)
this.state = {
language: 'en',
}
}
componentWillMount() {
const { language } = this.props
if (language) this.setState(prevState => ({ language: prevState.language = language }))
}
in some exceptional cases, such as i wrote two classes in a single .js file(like i said, some exceptions) and i couldn't be able to modify it from componentWillMount as expected(later noted, the props are modified by the child class).
in such cases, you can override it in render
render() {
const { language } = this.props
if (language) this.setState(prevState => ({ language: prevState.language = language }))
To accomplish this with React Hooks:
Track previous value with useRef()
Compare with previous value and conditionally update the local component state
The posted example didn't really make sense to me so here is the problem I faced:
I have a form with component state that I needed to clear out when the redux state changed.
To accomplish this my component looks like this:
import { useSelector } from 'react-redux';
import React, { useState, useEffect, useRef } from 'react';
const CreateCase = () => {
//redux state
const accounts = useSelector(state => state.accounts);
//component state
const [productId, setProductId] = useState(undefined);
const prevAccountRef = useRef<string>();
useEffect(() => {
//compare current with previous account and clear productId if changed
if (account.id != prevAccountRef.current) {
setProductId(undefined);
}
//set previous account for next render
prevAccountRef.current = account.id;
});
//... render
}
It's very important that you only run setState inside of useEffect conditionally.
even though the store and the connected props change, the component's state doesn't update
The way you have it written, the state won't update unless you explicitly update it using setState() (most likely in the componentWillReceiveProps() method).
When you use mapStateToProps() with the Redux connect() HOC, you are mapping your Redux state to your component through its props, so in your case this.props.language will update when the Redux stored updates.
componentWillReceiveProps will be called only when your component re-rendered.
Initially when component first time mounting, it not going to triggered.
You can not call setState inside componentwillupdate.
In order to initialise the initial state of component from redux store,you should use constructor.
constructor(props) {
super(props);
this.state = {
language: this.props.language || 'en'
}
}
Not sure if this applies, but I ran into a similar problem recently and my case was due to the fact that calling this.setState does not guarantee an instant update to state; it only says that the state change will be effected eventually.
From the react-component documentation:
Think of setState() as a request rather than an immediate command to
update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
If you need to make some "instant" changes to state or things that depend on state, then there's that callback on setState() that you can use to lock things in place. The callback is what worked for me.
I want my component know if some library is already loaded. To know that from any context i connect it to the "library" reducer of my store to my component.
I also pass it a configuration object this.props.dataObject from the parent where the component has been called. Like this:
class GoogleButton extends Component {
render() {
if (this.props.libraries.google) {
return <a id='sharePost' className='google_icon'></a>
} else {
return null
}
}
componentDidUpdate() {
gapi.interactivepost.render('sharePost', this.props.dataObject)
}
}
function mapStateToProps(state) {
return { libraries: state.libraries }
}
export default connect(mapStateToProps)(GoogleButton)
The reducer that handles the libraries state is like this:
let newState = {...state}
newState[action.libraryName] = action.state
return newState
When I change the library state componentDidUpdate works. The problem is when i change the prop inherited by the parent this.props.dataObject. In that case is where componentDidUpdate wont fire. If i remove the connect from the component it works as espected. I'm missing something here?
Most likely some of your props are mutated outside the component.
For example, you might be rendering your component like this:
class Parent extends Component {
constructor() {
super()
this.state = { libraries: {} }
}
handleClick() {
// MUTATION!
this.state.libraries.google = true
// Normally this forces to update component anyway,
// but React Redux will assume you never mutate
// for performance reasons.
this.setState({ libraries: this.state.libraries })
}
render() {
return (
<div onClick={() => this.handleClick()}>
<GoogleButton libraries={this.state.libraries} />
</div>
)
}
}
Because Redux apps deal with immutable data, connect() uses shallow equality check for its props to avoid unnecessary re-renders. However, this won’t work if you use mutation in your app.
You have two options:
Don’t Mutate Anything
This is the best option. For example, instead of something like
handleClick() {
this.state.libraries.google = true
this.setState({ libraries: this.state.libraries })
}
you can write
handleClick() {
this.setState({
libraries: {
...this.state.libraries,
google: true
}
})
}
This way we are creating a new object so connect() wouldn’t ignore the changed reference. (I’m using the object spread syntax in this snippet.)
Disable Performance Optimizations
A worse alternative is to completely disable performance optimizations made by connect(). Then your props would update even if you mutate them in the parent, but your app will be slower. To do this, replace
export default connect(mapStateToProps)(GoogleButton)
with
export default connect(mapStateToProps, null, null, { pure: false })(GoogleButton)
Don’t do this unless absolutely necessary.
I solved it. I'm not 100% sure that this is accurate, but I will explain. If im wrong with something, please correct me.
I keep thinking about the shallow equality check that Dan said in his answer. The problem was there.
I was passing down an object from the parent and the nested elements of that object were the ones that changed. The object remain the same. So with the shallow equality check that connect brings the component will never update.
My solution was in the parent use Object.assign({}, dataObject) when I pass down the prop so I make another different object. Now shallow equality check could compare it and determinate that the props have changed and there before update the component.
i had same problem and i used object.assign for create new state but i use combineReducer and it cause multi level state ,in my case i pass whole state as props to component so shallow equality check can not detect my state change so componentDidUpdate didnot call,it is important to pass state in level it change when using combine reducer
in my case i pass it like this
const MapStateToProps=(state)=>{
return {
reportConfig:state.ReportReducer
}
};
and my state tree is like this
{
ReportReducer: {
reportConfig: {
reportDateFilter: 'this week',
reportType: null,
reportShopId: null,
updateShop: true
}
}
}
and in my reducer and return it like this as ReportReducer
export default combineReducers({reportConfig});
and my root reducer is like this
const rootReducer =combineReducers({ReportReducer});
const store = createStore(rootReducer ,{},enhancer);
Another option that you can use is to make a deep copy of the inherit prop this.props.dataObject on the child component, this in order for the componentDidUpdate to 'catch' the updated prop, you could use:
dataObject={JSON.parse(JSON.stringify(valueToPass))}
Use this where you are passing the prop from the parent component, this works for me in a similar problem (This applies when you don't have any function inside the prop).
I had this exact same problem with Components I used from an external library.
So I didn't had the option to modify the inherited property.
I only needed a part of the inherited property object (will use dataObject for simplicity). Solved it by adding it to the mapStateToProps function:
function mapStateToProps(state, ownProps) {
return { libraries: state.libraries, neededValue: ownProps.dataObject.value }
}
By which a shallow compare is enough to notice a value change. So use this.props.neededValue iso this.props.dataObject.value in the render() function.