I have a functional React component that uses useState and useContext, and it derives its display properties from both context and props.
I am using useMemo with its second argument, the areEqual function to prevent the component from re-rendering when new props come in. There are dozens of values for the prop, but I only want to re-render the component when the prop changes to a specific value.
Here is a simplified example:
const MyComponent = (props) => {
const {contextThing} = useContext();
const {stateThing} = useState();
const {propsThing} = props;
// Since only foo changes what is rendered, I want to prevent rerendering unless the
// prop changes to 'foo'
const derivedThing =
(propsThing === 'foo' && contextThing === 'bar') ?
'foobar' :
null;
return (
<>
<div>{contextThing}</div>
<div>{stateThing}</div>
<div>{propsThing}</div>
<div>{derivedThing}</div>
<>
}
function preventRerender(prevProps, nextProps) {
if(prevProps.propsThing !== 'foo' }} nextProps.propsThing !== 'foo') {
return true;
}
return false;
}
export default memo(MyComponent, preventRerender);
This successfully prevents the component from re-rendering, because one level up the tree, the MyComponent is wrapped with MyComponentContainer, which uses a custom hook
to drill the props into MyComponent, like so:
const MyComponentContainer = () {
// Note: `useMyCustomHook` uses `useContext`, `useReducer` and `useState`
const { propsThing } = useMyCustomHook();
return (
<>
<Header />
<MyComponent propsThing={propsThing} />
<Footer />
<>
);
}
export default MyComponentContainer;
The Problem
In reference to the areEqual function:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
https://reactjs.org/docs/react-api.html#reactmemo
React docs seem to say this is a bad practice. I'm essentially treating memo/areEqual like a shouldComponentUpdate in a class component.
I don't have control over when propsThing changes. It is driven by both user actions and by pub/sub to a 3rd party API.
Is this an anti-pattern that I need to replace with a different pattern?
Or is React just saying "don't rely on this for features, because we may change how it works?".
Is there are 3rd party solution to better handle this use case?
Related
I want to enumerate children props of a nested component without passing them over.
Let's take a look at this example (pseudo code)
# JSX
<Root>
<NodeWrapper />
</Root>
# NodeWrapper component
function NodeWrapper() {
return <InnerNode myPropName="myPropValue" />
}
# Root component
function Root({children}) {
// children.props > lists all NodeWrapper props
// how to get a hold of InnerNode props, so that Root can detect prop `myPropName`?
}
The only way I found so far is to pass myPropName to NodeWrapper. Is there a way to grab myPropName value from within Root component without passing it down from Root to InnerNode through NodeWrapper?
I understand InnerNode will be available only when NodeWrapper is rendered, that is not the case as Root is being rendered and InnerNode is not rendered yet (i.e., it is a component and not yet an instance).
I think this question hides some React concept I am missing.
EDIT: Please note that my question is not to avoid prop drilling. Prop drilling and contexts are techniques to pass data down the component tree. What I want to do is quite the opposite: read a nested component props from the Root. The usage of Root.children gives me only NodeWrapper props, but I do actually would like to get InnerNode props from within Root component.
I think you are trying to avoid props drilling that is passing props to children where it is not directly needed but it is needed inside some nested component. For this I would recommend to use Context it is great way to avoid prop drilling here how you can configure it
import './App.css';
import { useContext } from 'react';
// In the login Component
const InnerComponent = () => {
const authContext = useContext(MyContext);
const handleLogin = () => {
authContext.onAuthChange(true); // this will make the user login that change the value of auth to true
}
return (
<div>Login JSX</div>
)
}
const MyContext = React.createContext(null);
const NodeWrapper = () => <InnerComponent />
function App() {
const [auth, setAuth] = React.useState(true);
const handleAuthChange = (newAuthState) => {
setAuth(newAuthState);
}
return (
<MyContext.Provider value={{
auth,
onAuthChange: handleAuthChange
}}>
<NodeWrapper />
</MyContext.Provider>
);
}
export default App;
My code has a component that takes both props and has its own internal state.
The component should rerender ONLY when its props change. State changes should NOT trigger a rerender.
This behaviour can be implemented with a class based component and a custom shouldComponentUpdate function.
However, this would be the first class based component in the codebase. Everything is done with functional components and hooks.
Therefore I would like to know whether it is possible to code the desired functionality with functional components.
After a few answers that didn't approach the real problem, I think I have to reformulate my question. Here is a minimal example with two components:
Inner takes a prop and has state. This is the component in question. It must not rerender after state changes. Prop changes should trigger a rerender.
Outer is a wrapper around inner. It has no meaning in the scope of this question and is only there to give props to Inner and to simulate prop changes.
To demonstrate the desired functionality I have implemented Inner with a class based component. A live version of this code can be found on codesandbox. How can I migrate it to a functional component:
Inner.tsx:
import React, { Component } from 'react'
interface InnerProps{outerNum:number}
interface InnerState{innerNum:number}
export default class Inner extends Component<InnerProps, InnerState> {
state = {innerNum:0};
shouldComponentUpdate(nextProps:InnerProps, nextState:InnerState){
return this.props != nextProps;
}
render() {
return (
<button onClick={()=>{
this.setState({innerNum: Math.floor(Math.random()*10)})
}}>
{`${this.props.outerNum}, ${this.state.innerNum}`}
</button>
)
}
}
Outer.tsx:
import React, { useState } from "react";
import Inner from "./Inner";
export default function Outer() {
const [outerState, setOuterState] = useState(1);
return (
<>
<button
onClick={() => {
setOuterState(Math.floor(Math.random() * 10));
}}
>
change outer state
</button>
<Inner outerNum={outerState}></Inner>
</>
);
}
The official docs say to wrap the component in React.memo. But this doesn't seem to work for preventing rerenders on state change. It only applies to prop changes.
I have tried to make React.memo work. You can see a version of the code with both Outer and Inner being functional components here.
Related questions:
How to use shouldComponentUpdate with React Hooks? : This question only deals with prop changes. The accepted answer advises to use React.memo
shouldComponentUpdate in function components : This question predates stateful functional components. The accepted answer explains how functional components don't need shouldComponentUpdate since they are stateless.
React memo do not stop state changes
React.memo only checks for prop changes. If your function component
wrapped in React.memo has a useState or useContext Hook in its
implementation, it will still rerender when state or context change.
Ref:- https://reactjs.org/docs/react-api.html#reactmemo
Your Inner component depends on the property num of the Outer component, you can't prevent it from rendering on property change as React.memo makes properties comparison:
// The default behaviour is shallow comparison between previous and current render properties.
const areEqual = (a, b) => a.num === b.num;
export default React.memo(Inner, areEqual);
By memoizing the Inner component and removing the num dependency, it won't render on Outer rendering, see sandbox attached.
export default function Outer() {
const [outerState, setOuterState] = useState(1);
return (
<>
...
// v Inner is memoized and won't render on `outerState` change.
<Inner />
</>
);
}
If you want to implement shouldComponentUpdate with hooks you can try:
const [currState] = useState();
// shouldUpdateState your's custom function to compare and decide if update state needed
setState(prevState => {
if(shouldUpdateState(prevState,currState)) {
return currState;
}
return prevState;
});
React is by design driven by setState -> re-render loop. Props change is in fact a setState somewhere in parent components. If you don't want the setState to trigger a re-render, then why in the first place use it?
You can pull in a const state = useRef({}).current to store your internal state instead.
function InnerFunc(props) {
const state = useRef({ innerNum: 0 }).current;
return (
<button
onClick={() => {
state.innerNum = Math.floor(Math.random() * 10);
}}
>
{`${props.outerNum}, ${state.innerNum}`}
</button>
);
}
That said, it's still a valid question to ask: "how to implement shouldComponentUpdate in a react hook fashion?" Here's the solution:
function shouldComponentUpdate(elements, predicate, deps) {
const store = useRef({ deps: [], elements }).current
const shouldUpdate = predicate(store.deps)
if (shouldUpdate) {
store.elements = elements
}
store.deps = deps
return store.elements
}
// Usage:
function InnerFunc(props) {
const [state, setState] = useState({ innerNum: 0 })
const elements = (
<button
onClick={() => {
setState({ innerNum: Math.floor(Math.random() * 10) });
}}
>
{`${props.outerNum}, ${state.innerNum}`}
</button>
);
return shouldComponentUpdate(elements, (prevDeps) => {
return prevDeps[0] !== props
}, [props, state])
}
Noted that it's impossible to prevent a re-render cycle when setState is called, the above hook merely makes sure the re-rendered result stays the same as prev rendered result.
you should use the event that provide the browser and capture in the function before setState, like this
function setState = (e) =>{ //the e is the event that give you the browser
//changing the state
e.preventDefault();
}
I have some heavy forms that I'm dealing with. Thus, I'm trying to squeeze performance wherever I can find it. Recently I added the Why-did-you-render addon to get more insight on what might be slowing down my pages. I noticed that, for example, when I click on a checkbox component about all of my other components re-render. The justification is always the same. WDYR says
Re-rendered because of props changes: different functions with the
same name {prev onChangeHandler: ƒ} "!==" {next onChangeHandler: ƒ}
As much as possible, I try to respect best the best practices indications that I find. The callback functions that my component passes follow this pattern
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
export function TopLevelComponent({props}){
const defaultData = {name: '', useMale: false, useFemale: false}
const [data, setData] = useState(defData);
const { t } = useTranslation();
const updateState = (_attr, _val) => {
const update = {};
update[_attr] = _val;
setData({ ...data, ...update });
}
const updateName = (_v) => updateState('name', _v);//Text input
const updateUseMale = (_v) => updateState('useMale', _v);//checkbox
const updateUseFemale = (_v) => updateState('useFemale', _v);//checkbox
...
return <div>
...
<SomeInputComponent value={data.name} text={t('fullName')} onChangeHandler={updateName} />
<SomeCheckboxComponent value={data.useMale} onChangeHandler={updateUseMale} text={t('useMale')}/>
<SomeCheckboxComponent value={data.useFemale} onChangeHandler={updateUseFemale} text={t('useFemale')}/>
...
</div>
}
In an example like this one, altering any of the inputs (eg: Writing text in the text input or clicking one of the checkboxes) would cause the other 2 components to re-render with the justification presented above.
I guess that I could stop using functional components and utilize the shouldComponentUpdate() function, but functional components do present some advantages that I'd rather keep. How should I write my functions in such a way that interacting with one input does not force an update on another input?
The problem stems from the way you define your change handlers:
const updateName = (_v) => updateState('name', _v)
This line is called on each render and thus, every time your component is rendered, the prop has a new (albeit functionality-wise identical) value. The same holds for every other handler as well.
As an easy solution you can either upgrade your functional component to a fully fledged component and cache the handlers outside of the render function, or you can implement shouldComponentUpdate() in your child components.
You need to use memo for your child components to reduce renders
const SomeInputComponent = props => {
};
export default memo(SomeInputComponent);
// if it still causes rerender witout any prop change then you can use callback to allow or block render
e.f.
function arePropsEqual(prevProps, nextProps) {
return prevProps.name === nextProps.name; // use your logic to determine if props are same or not
}
export default memo(SomeInputComponent, arePropsEqual);
/* One reason for re-render is that `onChange` callback passed to child components is new on each parent render which causes child components to re-render even if you use `momo` because function is updated on each render so in order to fix this, you can use React hook `useCallback` to get the same function reference on each render.
So in you parent component, you need to do something like
*/
import { useCallback } from 'react';
const updateName = useCallback((_v) => updateState('name', _v), [])
You have to memoize parent function before pass to children, using useCallback for functional component or converting to class property if you use class.
export default class Parent extends React.PureComponent {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick() {
console.log("click");
}
render() {
return (
<ChildComponent
onClick={ this.onClick }
/>
);
}
}
with useCallback:
Parent = () => {
const onClick = useCallback(
() => console.log('click'),
[]
);
return (
<ChildComponent
onClick={onClick}
/>
);
}
I have a stateless functional component which has no props and populates content from React context. For reference, my app uses NextJS and is an Isomorphic App. I'm trying to use React.memo() for the first time on this component but it keeps re-rendering on client side page change, despite the props and context not changing. I know this due to my placement of a console log.
A brief example of my component is:
const Footer = React.memo(() => {
const globalSettings = useContext(GlobalSettingsContext);
console.log('Should only see this once');
return (
<div>
{globalSettings.footerTitle}
</div>
);
});
I've even tried passing the second parameter with no luck:
const Footer = React.memo(() => {
...
}, () => true);
Any ideas what's going wrong here?
EDIT:
Usage of the context provider in _app.js looks like this:
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
...
return { globalSettings };
}
render() {
return (
<Container>
<GlobalSettingsProvider settings={this.props.globalSettings}>
...
</GlobalSettingsProvider>
</Container>
);
}
}
The actual GlobalSettingsContext file looks like this:
class GlobalSettingsProvider extends Component {
constructor(props) {
super(props);
const { settings } = this.props;
this.state = { value: settings };
}
render() {
return (
<Provider value={this.state.value}>
{this.props.children}
</Provider>
);
}
}
export default GlobalSettingsContext;
export { GlobalSettingsConsumer, GlobalSettingsProvider };
The problem is coming from useContext. Whenever any value changes in your context, the component will re-render regardless of whether the value you're using has changed.
The solution is to create a HOC (i.e. withMyContext()) like so;
// MyContext.jsx
// exported for when you really want to use useContext();
export const MyContext = React.createContext();
// Provides values to the consumer
export function MyContextProvider(props){
const [state, setState] = React.useState();
const [otherValue, setOtherValue] = React.useState();
return <MyContext.Provider value={{state, setState, otherValue, setOtherValue}} {...props} />
}
// HOC that provides the value to the component passed.
export function withMyContext(Component){
<MyContext.Consumer>{(value) => <Component {...value} />}</MyContext.Consumer>
}
// MyComponent.jsx
const MyComponent = ({state}) => {
// do something with state
}
// compares stringified state to determine whether to render or not. This is
// specific to this component because we only care about when state changes,
// not otherValue
const areEqual = ({state:prev}, {state:next}) =>
JSON.stringify(prev) !== JSON.stringify(next)
// wraps the context and memo and will prevent unnecessary
// re-renders when otherValue changes in MyContext.
export default React.memo(withMyContext(MyComponent), areEqual)
Passing context as props instead of using it within render allows us to isolate the changing values we actually care about using areEqual. There's no way to make this comparison during render within useContext.
I would be a huge advocate for having a selector as a second argument similar to react-redux's new hooks useSelector. This would allow us to do something like
const state = useContext(MyContext, ({state}) => state);
Who's return value would only change when state changes, not the entire context.
But I'm just a dreamer.
This is probably the biggest argument I have right now for using react-redux over hooks for simple apps.
I've been getting started with react-redux and finding it a very interesting way to simplify the front end code for an application using many objects that it acquires from a back end service where the objects need to be updated on the front end in approximately real time.
Using a container class largely automates the watching (which updates the objects in the store when they change). Here's an example:
const MethodListContainer = React.createClass({
render(){
return <MethodList {...this.props} />},
componentDidMount(){
this.fetchAndWatch('/list/method')},
componentWillUnmount(){
if (isFunction(this._unwatch)) this._unwatch()},
fetchAndWatch(oId){
this.props.fetchObject(oId).then((obj) => {
this._unwatch = this.props.watchObject(oId);
return obj})}});
In trying to supply the rest of the application with as simple and clear separation as possible, I tried to supply an alternative 'connect' which would automatically supply an appropriate container thus:
const connect = (mapStateToProps, watchObjectId) => (component) => {
const ContainerComponent = React.createClass({
render(){
return <component {...this.props} />
},
componentDidMount(){
this.fetchAndWatch()},
componentWillUnmount(){
if (isFunction(this._unwatch)) this._unwatch()},
fetchAndWatch(){
this.props.fetchObject(watchObjectId).then((obj) => {
this._unwatch = this.props.watchObject(watchObjectId);
return obj})}
});
return reduxConnect(mapStateToProps, actions)(ContainerComponent)
};
This is then used thus:
module.exports = connect(mapStateToProps, '/list/method')(MethodList)
However, component does not get rendered. The container is rendered except that the component does not get instantiated or rendered. The component renders (and updates) as expected if I don't pass it as a parameter and reference it directly instead.
No errors or warnings are generated.
What am I doing wrong?
This is my workaround rather than an explanation for the error:
In connect_obj.js:
"use strict";
import React from 'react';
import {connect} from 'react-redux';
import {actions} from 'redux/main';
import {gets} from 'redux/main';
import {isFunction, omit} from 'lodash';
/*
A connected wrapper that expects an oId property for an object it can get in the store.
It fetches the object and places it on the 'obj' property for its children (this prop will start as null
because the fetch is async). It also ensures that the object is watched while the children are mounted.
*/
const mapStateToProps = (state, ownProps) => ({obj: gets.getObject(state, ownProps.oId)});
function connectObj(Wrapped){
const HOC = React.createClass({
render(){
return <Wrapped {...this.props} />
},
componentDidMount(){
this.fetchAndWatch()},
componentWillUnmount(){
if (isFunction(this._unwatch)) this._unwatch()},
fetchAndWatch(){
const {fetchObject, watchObject, oId} = this.props;
fetchObject(oId).then((obj) => {
this._unwatch = watchObject(oId);
return obj})}});
return connect(mapStateToProps, actions)(HOC)}
export default connectObj;
Then I can use it anywhere thus:
"use strict";
import React from 'react';
import connectObj from 'redux/connect_obj';
const Method = connectObj(React.createClass({
render(){
const {obj, oId} = this.props;
return (obj) ? <p>{obj.id}: {obj.name}/{obj.function}</p> : <p>Fetching {oId}</p>}}));
So connectObj achieves my goal of creating a project wide replacement for setting up the connect explicitly along with a container component to watch/unwatch the objects. This saves quite a lot of boiler plate and gives us a single place to maintain the setup and connection of the store to the components whose job is just to present the objects that may change over time (through updates from the service).
I still don't understand why my first attempt does not work and this workaround does not support injecting other state props (as all the actions are available there is no need to worry about the dispatches).
Try using a different variable name for the component parameter.
const connect = (mapStateToProps, watchObjectId) => (MyComponent) => {
const ContainerComponent = React.createClass({
render() {
return <MyComponent {...this.props} obj={this.state.obj} />
}
...
fetchAndWatch() {
fetchObject(watchObjectId).then(obj => {
this._unwatch = watchObject(watchObjectId);
this.setState({obj});
})
}
});
...
}
I think the problem might be because the component is in lower case (<component {...this.props} />). JSX treats lowercase elements as DOM element and capitalized as React element.
Edit:
If you need to access the obj data, you'll have to pass it as props to the component. Updated the code snippet