Re-render React descendant components without re-rendering ancestors - reactjs

I'm trying to build a Navbar using React 16.8.3. I would like to use composition to pass the Navbar content instead of passing a config object via props, in order to have more flexibility. Something like this:
<Navbar>
<NavItem>Some label</NavItem>
<NavItem>
<span>Some arbitrary content</span>
<NavItem>
</Navbar>
instead of:
const navItems = [
{
label: 'Some label'
},
{
label: 'Some other label'
}
]
<Navbar items={navItems} />
So far the Navbar is working fine. I've added some logic in the shouldComponentUpdate method to prevent multiple re-renders:
shouldComponentUpdate(nextProps) {
return nextProps.selectedItem !== this.props.selectedItem;
}
so the Navbar only re-renders when its selected item changes, and not, for instance, when the Navbar parent re-renders.
Problem is that one NavItem contains a badge with a task count that must be updated whenever the user does some tasks:
Todos screenshot
and the item markup is:
<Navbar>
<NavItem>
<div className="has-badge">
<span>Label</span>
<span className="badge">{this.props.toDoCount}</span>
</div>
</NavItem>
</Navbar>
this.props.toDoCount is a prop of the Navbar parent, and not of the Navbar itself.
How can I update the badge number without re-rendering the whole Navbar?. So far I've tried creating a Badge component, adding some state, and a method to update the badge number using a ref in the Navbar parent:
import React, { PureComponent } from 'react';
interface BadgeProps {
number: number;
}
class Badge extends PureComponent<BadgeProps> {
state = {
number: 0
};
setCount(number) {
this.setState({
number
});
}
render() {
return <span className="badge">{this.state.number}</span>;
}
}
In the Navbar parent:
private todos = createRef<Badge>();
...
componentDidUpdate(prevProps: EhrProps) {
this.todos.current.setCount(toDosCount);
}
and it's working, but... is there an easier or cleaner way of doing this in React??
Thanks!
PS: We are using Redux in the project, but I would like to avoid using the store in the Navbar or its items.
EDIT:
I'm using React.children and React.cloneElement in the Navbar's render method:
render() {
const { className, children, selectedItem, ...rest } = this.props;
const classes = classNames(
{
navbar: true
},
className
);
return (
<nav className={classes} {...rest}>
{React.Children.map(children, child => {
if (child.type === NavItem) {
return React.cloneElement(child, {
onClick: this.handleItemClick,
selected: child.props.name === selectedItem
});
}
return child;
})}
</nav>
);
}
And each NavItem handles its own render:
return (
<div className={classes} onClick={handleClick} onKeyPress={handleKeyPress} role="menuitem" tabIndex={0}>
{children}
</div>
);

Presumably, you have some code for the the Navbar component that looks a bit like this.
class Navbar extends React.Component<Props> {
render() {
return (
<div>
{this.props.navItem.map(item => <NavItem key={item.label}>{item.label}</NavItem>)};
</div>
);
}
}
and then some code to render each child NavItem.
To make the component fairly efficient, it's sufficient to have the whole of Navbar rerender, but only not re-render each child.
What I would recommend is:
Make each child of Navbar be rendered in its own component; in the component above it's called NavItem
Use either componentShouldUpdate or React.PureComponent (look into this! Once you understand it, it's a great general solution to use by default instead of React.Component for every component) to make sure that each child only re-renders when its value changes
What will happen when you update the badge for the single NavItem is that Navbar will re-render. Most of the NavItems will see their Props haven't changed, and not re-render. The single child of Navbar that has the badge will have changed, and will re-render. With this, the real overhead is actually quite low.
If your Navbar has a ton of children or your badge for that single child changes a lot, you can probably optimize it more by using React.Context or Redux to pass in the value for that single child, but that feels messy and seems like premature optimization.
Good luck!

Related

CSSTransition nodeRef for component with direct children

I want to get rid of the warning on StrictMode for findDOMNode when using react-transition-group but I stumbled upon an issue.
My <Slide> component looks like this:
class Slide extends React.Component {
nodeRef = React.createRef();
render() {
return (
<CSSTransition
in={this.props.in}
timeout={ANIMATION_DURATION}
mountOnEnter={true}
unmountOnExit={true}
classNames={{
enter: "slideEnter",
enterActive: "slideEnterActive",
exit: "slideExit",
exitActive: "slideExitActive"
}}
nodeRef={this.nodeRef}
>
{this.props.children}
</CSSTransition>
);
}
}
It receives a Drawer element as children, the Drawer component looks like this:
class Drawer extends React.Component {
render() {
return (
<div className="drawer">
<button onClick={this.props.onClose}>close me</button>{" "}
<div>This is my drawer</div>
</div>
);
}
}
I cannot wrap the children element with a HTML tag (to attach a ref <div ref={this.nodeRef}>{this.props.children}</div> because it breaks the animation of the content. (I'm using this for children that are different drawers with position absolute)
I've also tried with cloneElement but it still doesn't work (with the code from below it behaves like this: 1. in no animation, 2. out no animation, 3. in animation works but I get the warning findDOMNode so it seems that nodeRef is sent as null, 4. out animation does not work.
const onlyChild = React.Children.only(this.props.children);
const childWithRef = React.cloneElement(onlyChild, {
ref: this.nodeRef;
});
Is there any solution for this situation? Thanks!
The problem is that nodeRef needs to point to a DOM Node, as the name suggests, in your case it points to an instance of the Drawer class. You have two options:
Pass the ref through another prop, e.g. forwardedRef, and in the Drawer class pass that prop to the root element:
React.cloneElement(onlyChild, {
forwardedRef: this.nodeRef,
})
<div ref={this.props.forwardedRef} className="drawer">
Convert Drawer to a function component and use React.forwardRef:
const Drawer = React.forwardRef((props, ref) => {
return (
<div ref={ref} className="drawer">
<button onClick={props.onClose}>close me</button>{" "}
<div>This is my drawer</div>
</div>
);
});

React Hooks function component with controlled rendering based on state/prop values

One of the benefits of being able to use shouldComponentUpdate on a React Class component is the ability to control the render based on a condition rather than just a change in state/prop values.
What is the preferred way to make this optimization using react hooks in a function component?
In the example below, the class component does not re-render if it is (or is staying) in a closed state, even if it has new children.
class DrawerComponent extends React.Component {
static propTypes = {
children: PropTypes.any,
}
state = {
isOpen: false,
}
// only re-render if the drawer is open or is about to be open.
shouldComponentUpdate(nextProps, nextState) {
return this.state.isOpen || nextState.isOpen;
}
toggleDrawer = () => {
this.setState({isOpen: !this.state.isOpen});
};
render() {
return (
<>
<div onClick={this.toggleDrawer}>
Drawer Title
</div>
<div>
{this.state.isOpen ? this.props.children : null}
</div>
</>
)
}
}
Function component counterpart (without optimization):
function DrawerComponent({ children }) {
const [isOpen, setIsOpen] = useState(false);
function toggle() {
setIsOpen(!isOpen);
}
return (
<>
<div onClick={toggle}>
Drawer Title
</div>
<div>{isOpen ? children : null}</div>
</>
);
}
In this example, in my opinion there's no need for a shouldComponentUpdate optimization. It will already be fast since you're not rendering the children when the drawer is closed. The cost of running the functional component will be fairly negligible.
That said, if you did want to implement the equivalent behavior in a functional component, you could use React.memo and supply a custom areEqual function: https://reactjs.org/docs/react-api.html#reactmemo.

React constructor called only once for same component rendered twice

I expected this toggle to work but somehow the constructor of component <A/> is called only once. https://codesandbox.io/s/jvr720mz75
import React, { Component } from "react";
import ReactDOM from "react-dom";
class App extends Component {
state = { toggle: false };
render() {
const { toggle } = this.state;
return (
<div>
{toggle ? <A prop={"A"} /> : <A prop={"B"} />}
<button onClick={() => this.setState({ toggle: !toggle })}>
toggle
</button>
</div>
);
}
}
class A extends Component {
constructor(props) {
super(props);
console.log("INIT");
this.state = { content: props.prop };
}
render() {
const { content } = this.state;
return <div>{content}</div>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
I already found a workaround https://codesandbox.io/s/0qmnjow1jw.
<div style={{ display: toggle ? "none" : "block" }}>
<A prop={"A"} />
</div>
<div style={{ display: toggle ? "block" : "none" }}>
<A prop={"B"} />
</div>
I want to understand why the above code is not working
In react if you want to render same component multiple times and treat them as different then you need to provide them a unique key. Try the below code.
{toggle ? <A key="A" prop={"A"} /> : <A key="B" prop={"B"} />}
Since that ternary statement renders results in an <A> component in either case, when the <App>'s state updates and changes toggle, React sees that there is still an <A> in the same place as before, but with a different prop prop. When React re-renders it does so by making as few changes as possible. So since this is the same class of element in the same place, React doesn't need to create a new element when toggle changes, only update the props of that <A> element.
Essentially, the line
{toggle ? <A prop="A"/> : <A prop="B"/> }
is equivalent to
<A prop={ toggle ? "A" : "B" }/>
which perhaps more clearly does not need to create a new <A> component, only update the existing one.
The problem then becomes that you set the state.content of the <A> using props.prop in the constructor, so the state.content is never updated. The cleanest way to fix this would be to use props.prop in the render method of the <A> component instead of state.content. So your A class would look like this:
class A extends Component {
render() {
const { prop } = this.props;
return <div>{ prop }</div>;
}
}
If you must take the prop prop and use it in the <A> component's state, you can use componentDidUpdate. Here's an example:
class A extends Component {
constructor(props) {
super(props);
this.state = {content: props.prop};
}
componentDidUpdate(prevProps) {
if (prevProps.prop !== this.props.prop) {
this.setState({content: this.props.prop});
}
}
render() {
const { content } = this.state;
return <div>{ content }</div>
}
}
React will only call the constructor once. That's the expected outcome.
Looks like you're trying to update the state of the component A based on the props.
You could either use the prop directly or use the componentDidUpdate lifecycle method, as Henry suggested. Another way is using the static method getDerivedStateFromProps to update the state based on the prop passed.
static getDerivedStateFromProps(props, state) {
return ({
content: props.prop
});
}

this.props.children not re-rendered on parent state change

I have a piece of code
import React, {Component} from 'react';
class App extends Component {
render() {
return (
<Container>
<Child/>
</Container>
)
}
}
class Container extends Component {
render() {
console.log('Container render');
return (
<div onClick={() => this.setState({})}>
{this.props.children}
</div>
)
}
}
class Child extends Component {
render() {
console.log('Child render');
return <h1>Hi</h1>
}
}
export default App;
When clicking on 'Hi' msg, only Container component keeps re-rendering but Child component is not re-rendered.
Why is Child component not re-rendered on Container state change?
I would reason, that it doesn't happen due to it being a property of Container component, but still this.props.child is evaluated to a Child component in JSX, so not sure.
<div onClick={() => this.setState({})}>
{this.props.children}
</div>
Full example https://codesandbox.io/s/529lq0rv2n (check console log)
The question is quite old, but since you didn't seem to get a satisfying answer I'll give it a shot too.
As you have observed by yourself, changing
// Scenario A
<div onClick={() => this.setState({})}>
{this.props.children}
</div>
to
// Scenario B
<div onClick={() => this.setState({})}>
<Child />
</div>
will in fact, end up with
Container render
Child render
in the console, every time you click.
Now, to quote you
As fas as I understand, if setState() is triggered, render function of
Container component is called and all child elements should be
re-rendered.
You seemed to be very close to understanding what is happening here.
So far, you are correct, since the Container's render is executed, so must the components returned from it call their own render methods.
Now, as you also said, correctly,
<Child />
// is equal to
React.createElement(Child, {/*props*/}, /*children*/)
In essence, what you get from the above is just an object describing what to show on the screen, a React Element.
The key here is to understand when the React.createElement(Child, {/*props*/}, /*children*/) execution happened, in each of the scenarios above.
So let's see what is happening:
class App extends Component {
render() {
return (
<Container>
<Child/>
</Container>
)
}
}
class Container extends Component {
render() {
console.log('Container render');
return (
<div onClick={() => this.setState({})}>
{this.props.children}
</div>
)
}
}
class Child extends Component {
render() {
console.log('Child render');
return <h1>Hi</h1>
}
}
You can rewrite the return value of App like this:
<Container>
<Child/>
</Container>
// is equal to
React.createElement(
Container,
{},
React.createElement(
Child,
{},
{}
)
)
// which is equal to a React Element object, something like
{
type: Container,
props: {
children: {
type: Child, // |
props: {}, // +---> Take note of this object here
children: {} // |
}
}
}
And you can also rewrite the return value of Container like this:
<div onClick={() => this.setState({})}>
{this.props.children}
</div>
// is equal to
React.createElement(
'div',
{onClick: () => this.setState({})},
this.props.children
)
// which is equal to React Element
{
type: 'div',
props: {
children: this.props.children
}
}
Now, this.props.children is the same thing as the one included in the App's returned React Element:
{
type: Child,
props: {},
children: {}
}
And to be exact, these two things are referentially the same, meaning it's the exact same thing in memory, in both cases.
Now, no matter how many times Container get's re-rendered, since its children are always referentially the same thing between renders (because that React Element was created in the App level and it has no reason to change), they don't get re-rendered.
In short, React doesn't bother to render a React Element again if it is referentially (===) equal to what it was in the previous render.
Now, if you were to change the Container you would have:
<div onClick={() => this.setState({})}>
<Child />
</div>
// is equal to
React.createElement(
'div',
{onClick: () => this.setState({})},
React.createElement(
Child,
{},
{}
)
)
// which is equal to
{
type: 'div',
props: {
children: {
type: Child,
props: {},
children: {}
}
}
}
However in this case, if you were to re-render Container, it will have to re-execute
React.createElement(
Child,
{},
{}
)
for every render. This will result in React Elements that are referentially different between renders, so React will actually re-render the Child component as well, even though the end result will be the same.
Reference
The <Child /> component is not re-rendered because the props have not changed. React uses the concept of Virtual DOM which is a representation of your components and their data.
If the props of a component do not change the component is not re-rendered. This is what keeps React fast.
In you example there are no props sent down to Child, so it will never be re-rendered. If you want it to re-render each time (why would you ?), you can for example use a pure function
const Child = () => <h1>Hi</h1>;
Change {this.props.children} to <Child /> in Container component, (now you can remove <Child /> from App component).
If you are clicking the div you will get both the 'Child render' and 'Container render' in console.
(In this example your child is static component. Then there is no point for the re-rendering.)

Stateless functional component vs class based: why won't my props change?

I have a dumb component that renders a link. I have a simple ternary checking if the prop, props.filter is the same as its props.filterType, then it will render a <span> instead of a <a> tag.
This dumb component is being passed filter from a parent component, which is connected to a Redux store.
The bug that I'm running into is this: my parent component does receive changes/updates to filter, I am console logging it and able to see that in the parent component filter does change.
However in my dumb component, I am console.logging props.filter, which doesn't change at all. On the flip side, using the React dev tools and inspecting the component and checking its props, it DOES change. WHAT?!
Changing the stateless functional component to a class does work. The console.log(props.filter) with the component as a class, does change.
Here is the code for the component, both as a stateless functional and a class:
import React from 'react';
import './styles.css';
/* props.filter DOES CHANGE HERE */
class FilterLink extends React.Component {
render() {
console.log('this.props.filter: ', this.props.filter);
console.log('this.props.filterType: ', this.props.filterType);
console.log(this.props.filter === this.props.filterType);
return (
this.props.filter === this.props.filterType ?
<span className='active-link'>{this.props.children}</span>
:
<a id={this.props.filterType} className='link' href='' onClick={this.props.setFilter}>
{this.props.children}
</a>
);
}
};
/* props.filter DOESN'T CHANGE HERE */
const FilterLink = props => ({
render() {
console.log('props.filter: ', props.filter);
console.log('props.filterType: ', props.filterType);
console.log(props.filter === props.filterType);
return (
props.filter === props.filterType ?
<span className='active-link'>{props.children}</span>
:
<a id={props.filterType} className='link' href='' onClick={props.setFilter}>
{props.children}
</a>
);
},
});
export default FilterLink;
I think there is a huge hole in my understanding of stateless functional components. Any help or advice or direction would be greatly appreciated.
Thank you,
You implemenentation of stateless component is just wrong. It should do render instead of returning an object with render method.
const FilterLink = props => {
console.log('props.filter: ', props.filter);
console.log('props.filterType: ', props.filterType);
console.log(props.filter === props.filterType);
return (
props.filter === props.filterType ?
<span className='active-link'>{props.children}</span>
:
<a id={props.filterType} className='link' href='' onClick={props.setFilter}>
{props.children}
</a>
);
};

Resources