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

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.

Related

React: how to propagate state to enclosing parent

I have 2 classes to provide the modal-dialog functionality:
import React from 'react'
import Modal from 'react-modal'
export default class ModalBase extends React.Component {
state = { show:false }
handleOpen = opts => {
this.setState( { ...opts, show:true } )
console.info( 'ModalBase handleOpen', this.constructor.name, 'show', this.state.show )
}
handleClose = () => this.setState( { show:false } )
render() {
console.info( 'ModalBase render show', this.state.show )
return <Modal isOpen={this.state.show} onRequestClose={this.handleClose} className="Modal" overlayClassName="Overlay">
{this.props.children}
</Modal>
}
}
and
export default class InfoPopup extends ModalBase {
state = { ...this.state, tech:{} }
render() {
console.info('InfoPopup render show', this.state.show)
return (
<ModalBase>
<div/><div/>
</ModalBase>
)
}
}
When I call InfoPopup.handleOpen({a:42}), the following shows up in the console:
ModalBase handleOpen InfoPopup show true
InfoPopup render show true
ModalBase render show false
so, the ModalBase's state.show is not changed and hence the popup is not shown.
How shall I properly propagate the state to enclosing parent object?
TIA
Use composition instead of inheritance
From the React docs:
React has a powerful composition model, and we recommend using composition instead of inheritance to reuse code between components.
See: https://reactjs.org/docs/composition-vs-inheritance.html
So export default class InfoPopup extends ModalBase is not advised.
1. Let InfoPopup render ModalBase but keep track of open/close state
You could turn it around and have a generic BaseModal component for modal styling that you pass props such as title and content. The InfoPopup keeps track of the opened/closed state. From the same React docs page:
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}
render() {
return (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}
handleChange(e) {
this.setState({login: e.target.value});
}
handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}
2. Render ModalBase and pass the type of modal as prop
You could also always render ModalBase for an info, warning, error modal etc. Then you pass the type of modal as prop to ModalBase. ModalBase determines some specifics based on that type prop.
3. use a render prop
Described here: https://reactjs.org/docs/render-props.html
Let ModalBase accept a function as children prop.
So in InfoPopup:
<ModalBase>
{({ toggle }) => (
<button onClick={toggle} />
)}
</ModalBase>
And in ModalBase:
render() {
return <Modal ...>{this.props.children({ toggle: this.openOrClose })}</Modal>
}
4. Pass a component to ModalBase to render when open
A bit of a variant on 2. You could also pass a component as prop to ModalBase that it should show when it's open.
<ModalBase
modalContent={<InfoPopup />}
/>

Re-render React descendant components without re-rendering ancestors

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!

Using React, is it possible to define a component inside the definition of another component?

(this question differs to 'is it possible to use a component inside another', this question is 'can I define a component inside the definition of another', it is not a duplicate of 'Can I write Component inside Component in React?')
Is it possible to define a component inside the definition of another component? This way I can use the props of the outside component in the inner component. It would keep the code more concise. Something like the following...
class AComponent extends Component {
CustomButton = (props) => {
let { disabled, ...otherProps } = props // <-- props of the inner component
const {isDisabled, currentClassName} = this.props // <-- props of the main component
return (
<button
className={className}
disabled={isDisabled}
{...otherProps}>
</button>
)
}
render() {
return (
<div>
<CustomButton>Add something</CustomButton>
<CustomButton>Delete something</CustomButton>
<CustomButton>Edit</CustomButton>
</div>
)
}
}
If the custom button was defined on its own (the usual way of defining components) I would have to do something like below which is ok but more verbose and less dry as I repeat the definition of {...buttonProps} for each component
let buttonProps = {
className: this.props.currentClassName,
disabled: this.props.disabled
}
return (
<div>
<button {...buttonProps}>Add something</button>
<button {...buttonProps}>Delete something</button>
<button {...buttonProps}>Edit</button>
</div>
)
While yes, it's possible to define one function component inside another function component, this is not recommended.
Referring to the ReactJs docs:
reactjs.org/docs/components-and-props
reactjs.org/docs/conditional-rendering
Most, if not all, examples show child components being defined outside of the parent component.
Defining a component inside another will cause the child component to be re-created on mount and unmount of the parent, which could cause unexpected behavior if the child is using props from the parent, and cannot handle if those props are suddenly undefined.
It's best to define components separately.
Yes! I needed to define the function component in the render method...which makes sense
class AComponent extends Component {
render() {
const ButtonLocal = (props) => {
const {isDisabled, currentClassName} = this.props
return (
<Button
disabled={isDisabled}
className={currentClassName}
{...buttonProps}>
</Button>
)
}
return (
<div>
<ButtonLocal>Add something</ButtonLocal>
<ButtonLocal>Delete something</ButtonLocal>
<ButtonLocal>Edit</ButtonLocal>
</div>
)
}
}
Very much possible to add components inside a parent component. Consider following example:
<ParentComponent style={styles}>
<ChildComponent style={styles} {...props} />
</ParentComponent>

Render a component when another component is clicked

I want to render BlackSpark when RedSpark is clicked, but I'm not sure how to change the state of a component in another component. I know how to set state in the component itself, but how do I affect another component when I click a different component?
class BlackSpark extends React.Component {
render() {
return (
<div className="black"></div>
);
}
}
class RedSpark extends React.Component {
render() {
return (
<div className="red"></div>
);
}
}
class App extends React.Component {
render() {
return (
<div>
<BlackSpark />
<RedSpark />
</div>
);
}
}
In React, there's a concept of component composition as you've already embraced -- it allows you to accomplish what you want by rendering children based on the parent's state, another key concept known as lifting state up. What this means, is if you have mutually dependent components, create a single parent which composes them, and have state in the parent control the presentation and logic of the children. With the parent App, you can keep your state inside App, and based on App's state, conditionally render whatever you want -- either BlackSpark or both. For example, using the logical && operator:
{condition && <Component />}
This will only render <Component> when condition is truthy, or else it will not render anything at all (except for when condition is 0). Applying it to this situation, try adding state to your App component to utilize conditional rendering.
There's another key concept you need to understand: component props. They are essentially inputs to a component, certain properties passed to the component to tell how it should behave -- like attributes on regular HTML elements such as input placeholders, URLs, and event handlers. For example:
<Component foo="bar" bar={3} />
This will pass the props foo and bar down to Component with the values "bar" and 3 respectively and are accessible through this.props. If you were to access this.props.foo inside the Component component it would give you "bar". If you pair this up with composition, you can accomplish what you want:
class Example extends React.Component {
constructor() {
super();
this.state = {
showHello: true
}
this.handleChange = this.handleChange.bind(this);
}
handleChange() {
this.setState(prevState => ({
showHello: !prevState.showHello
}));
}
render() {
return (
<div>
{this.state.showHello && <Child2 />}
This is a test.
<Child1 onClick={this.handleChange} />
</div>
);
}
}
class Child1 extends React.Component {
render() {
return <div onClick={this.props.onClick}>Click me!</div>
}
}
class Child2 extends React.Component {
render() {
return <div>Hello!</div>
}
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
The above example lifts state up by having a parent compose the children and maintain the state. It then uses props to pass down an onClick handler to Child1, so that whenever Child1 is clicked, the state of the parent changes. Once the state of the parent changes, it will use conditional rendering to render <Child2> if the condition is truthy. Further reading at the React documentation and on the logical && operator.
I know how to set state in the component itself, but how do I affect another component when I click a different component?
The recommended way to do it would be to create a parent component that has the state. You'd then use that state to determine when to render the other child component.
I want to render BlackSpark when RedSpark is clicked, but I'm not sure how to change the state of a component in another component. Also, what if I want to hide BlackSpark when GreenSpark is clicked and GreenSpark is inside BlackSpark?
In this case, here's how you'd do it.
const GreenSpark = ({ onClick }) => (
<button className="green" onClick={onClick}>X</button>
)
const BlackSpark = ({ onClick }) => (
<div className="black">
<GreenSpark onClick={onClick} />
</div>
)
const RedSpark = ({ onClick }) => (
<div className="red" onClick={onClick}></div>
)
class Spark extends React.Component {
constructor(props) {
super(props)
this.state = {
showBlack: false
}
this.boundShowBlack = this.showBlack.bind(this)
this.boundHideBlack = this.hideBlack.bind(this)
}
showBlack() {
this.setState({ showBlack: true })
}
hideBlack() {
this.setState({ showBlack: false })
}
render() {
return (
<div>
<RedSpark onClick={this.boundShowBlack} />
{this.state.showBlack && <BlackSpark onClick={this.boundHideBlack} />}
</div>
)
}
}

Updating Child Component Via Change of Props from Parent in React

I have a component whose state is passed as a prop to a child component. I noticed that even if the Parent's state is changed, the child component is not re-rendered.
Based on the official React Documentation for utilizing shouldComponentUpdate:
"The first time the inner component gets rendered, it will have { foo:
'bar' } as the value prop. If the user clicks on the anchor, the
parent component's state will get updated to { value: { foo: 'barbar'
} }, triggering the re-rendering process of the inner component, which
will receive { foo: 'barbar' } as the new value for the prop.
The problem is that since the parent and inner components share a
reference to the same object, when the object gets mutated on line 2
of the onClick function, the prop the inner component had will change.
So, when the re-rendering process starts, and shouldComponentUpdate
gets invoked, this.props.value.foo will be equal to
nextProps.value.foo, because in fact, this.props.value references the
same object as nextProps.value.
Consequently, since we'll miss the change on the prop and short
circuit the re-rendering process, the UI won't get updated from 'bar'
to 'barbar'."
If I can't use shouldComponentUpdate, how would I force the child component to re-render based on a change of props from the Parent?
Parent Component
I want the child component to rerender based on the boolean given to showDropzone.
<Dropzone
onDrop={this.onDrop}
clearFile = {this.clearFile}
showDropzone = {this.state.filePresent}/>
Child Component
export default class Dropzone extends Component {
constructor(props) {
super(props);
}
render() {
const { onDrop } = this.props;
const {showDropzone} = this.props;
const {clearFile} = this.props;
return (
<div className="file-adder">
<div className="preview">
{{showDropzone}?
<Dropzone
onDrop={onDrop}
ref="dropzone">
</Dropzone>
:<button type="button"
className="btn btn-primary"
onClick={clearFile}>
Clear File</button>}
</div>
</div>
);
}
}
I'am sorry but your code has a lot of errors
First (This is a tip) inside:
const { onDrop } = this.props;
const { showDropzone } = this.props;
const { clearFile } = this.props;
You can write just one line
const { onDrop, showDropzone, clearFile } = this.props;
Now your problems starts from here:
You used showDropzone inside curly brackets and this is your
problem remove them
Your mistake {{showDropzone}? firstOption : anotherOption }
Will be { showDropzone? firstOption : anotherOption }
Another mistake:
You used the Dropzone component inside itself and this is a big mistake
You can't use Dropzone component here {{showDropzone}? <Dropzone {...}/> : anotherOption } You can use it from another component
Finally i tried to format your code to make it like this
Parent Component
{ this.state.filePresent
? <Dropzone onDrop={this.onDrop} />
: <button
type="button"
className="btn btn-primary"
onClick={clearFile}
children="Clear File"
/>
}
Child Compnent
class Dropzone extends Component {
render() {
const { onDrop } = this.props;
return (
<div className="file-adder">
<div className="preview" onDrop={onDrop} ref="dropzone">
<p>Hi! This is Dropzone component</p>
</div>
</div>
);
}
}
export default Dropzone;
In child component you have used "showZone" instead of "showDropzone".
Thanks

Resources