preact createPortal renders multiple times - javascript

I am using preact (a small version of react) in my project.
before updating to preactX version I was using Modal component like this and there was no problem with it, this is how my Modal component looked like:
import { Component } from 'preact';
import Portal from 'preact-portal';
export default class Modal extends Component {
componentWillReceiveProps({ isOpen }) {
if (this.state.isOpen !== isOpen) {
this.setState({ isOpen });
}
}
handleClose = () => {
const { onClose } = this.props;
this.setState({ isOpen: false });
onClose && onClose();
};
render({ children, closeIcon, isOpen }) {
return isOpen && (
<Portal into="body">
<Wrapper className="notranslate">
<Overlay />
<Holder>
<Close onClick={this.handleClose}>
<img src={closeIcon || DefaultCloseIcon} alt="Close" />
</Close>
{children}
</Holder>
</Wrapper>
</Portal>
);
}
}
after upgrading to preactX they dropped out Portal component support and change it to createPortal method like the one in react and here where the problem happenes, it renders whenever the props isOpen changes and because of that modals are opening multiple times.
here is my implementation for modal component with createPortal using hooks:
import { createPortal, useState, useEffect, memo } from 'preact/compat';
function Modal({ children, onCloseClick, closeIcon, isOpen }) {
const [isStateOpen, setIsStateOpen] = useState(isOpen);
useEffect(() => {
if (isStateOpen != isOpen) {
setIsStateOpen(isOpen);
}
return () => {
setIsStateOpen(false);
};
}, [isOpen]);
return (
isStateOpen &&
createPortal(
<Wrapper>
<Overlay />
<Holder>
<Close onClick={onCloseClick}>
<img src={closeIcon || DefaultCloseIcon} alt="Close" />
</Close>
{children}
</Holder>
</Wrapper>,
document.body
)
);
}
export default memo(Modal);
and I am using Modal component like that:
<App>
<SomeOtherComponents />
<Modal
isOpen={hasModalOpen}
closeIcon={CloseIcon}
onCloseClick={cancelModal}
>
<div>
some other content here
</div>
</Modal>
</App>
the place where I used my Modal components may render multiple times and that makes Modal component renders too, that was fine before when I was using Portal but when I used createPortal it seems createPortal is not recognizing whether if the Modal component is already in the dom or not.
I suppose that the same would happen in react also.

this isn't a createPortal issue, it's your use of Hooks.
I created a demo based on the code you posted, and it was continuously re-rendering because of the "cleanup" callback returned from your useEffect() hook. That callback was unnecessary, and removing it fixes the whole demo:
https://codesandbox.io/s/preact-createportal-renders-multiple-times-32ehe

I solved the problem by adding shouldComponentUpdate method to the Modal component.
it seems that the component was rendering whenever any prop changes in the parent component even if isOpen prop hasn't changed.
shouldComponentUpdate(nextProps) {
return this.props.isOpen !== nextProps.isOpen;
}
it seems also that createPortal doesn't apply the shallow rendering as the old Portal applies it in versions before preactX

Related

How can I call React useRef conditionally in a Portal wrapper and have better code?

I am attempting to make a simple Portal wrapper that does the following.
Can render multiple portals on the same view
Can render multiple portals by targeting parent ids. This cannot be done by passing a ref into the component since the container may live anywhere.
Can render multiple portal without any parent targets
This is what I came up with this morning. The code works exactly how I want it to work. It creates portals with or without a parent target and cleans up completely. However React says not to call hooks from conditions, which you can see I have done here by calling useRef within a ternary. I have not seen any warnings or errors but I am also developing this code in a custom setup with Webpack and Typescript. I have not pushed this code to NPM to see what happens when I import the library into a project. My guess is there's going to be issues. Is there a better way to do this?
Thanks in advance.
Calling Component:
<div>
{open1 && (
<Portal parentId="panel-1">
<Panel title="Poopster" onClose={handleClose1}>
<div>Panel</div>
</Panel>
</Portal>
)}
{open2 && (
<Portal>
<Panel onClose={handleClose2}>
<div>Panel</div>
</Panel>
</Portal>
)}
<div>
Portal Component
import * as React from 'react';
import { createPortal } from 'react-dom';
export interface PortalProps {
children: React.ReactElement;
parentId?: string;
}
export const Portal = ({
children,
parentId
}: PortalProps): React.ReactElement => {
let ref = !parentId ?
React.useRef(document.createElement('div')) :
React.useRef(document.getElementById(parentId));
React.useEffect((): VoidFunction => {
return (): void => {
if (!parentId && ref.current) {
document.body.removeChild(ref.current);
}
ref = null;
};
}, []);
React.useEffect(() => {
if (!parentId && ref.current) {
document.body.appendChild(ref.current);
}
}, [ref, parentId]);
return createPortal(children, ref.current);
};
export default Portal;
You could make it like so, for short and more intuitive
let ref = React.useRef(
parentId ? document.getElementById(parentId) : document.createElement("div")
);
This will solve the hook rules error

ReactJS - Disabling a component

I need to disable PostList component in its initial state.
import React from 'react';
import PostList from './PostList';
const App = () => {
return (
<div className="ui container">
<PostList />
</div>
);
};
export default App;
Whats the best way to disable (and grey out) a component? Possible solutions are to pass a value as props and then apply it to a ui element, However please keep in mind that PostList may have inner nested components as well. Please share an example.
Since you mentioned in a comment that instead of hiding it, you want to grey it instead. I would use the disabled state and style the component. Since PostList could be nested, we don't know what the props are since you did not specify them.
Also, I assuming that you are not using styled-components.
import React, { useState } from "react";
import PostList from "./PostList";
const App = () => {
const [disabled, setDisabled] = useState(true);
return (
<div className="ui container">
<PostList
style={{
opacity: disabled ? 0.25 : 1,
pointerEvents: disabled ? "none" : "initial"
}}
/>
</div>
);
};
export default App;
There are 2 different ways I like to do something like this.
One way you can do it is by using state
this.state = {
showList: false
}
and than something like
return (
{this.state.showList && <PostList />}
)
Another option is to pass the showList in state as a prop, so something like
return(
<PostList show = {this.state.showList} />
)
and than in PostList something like
return props.show && (your component here)
You can also use conditional classNames, so if you want that component shown, you can throw a className and style it how you normally would, but if not, just throw a display: none. I usually save doing that for replacing a navbar with a dropdown button on smaller screens, but it is another option

React - toggle in stateless component

I am trying to toggle visiblity of a div in a stateless component like this:
const playerInfo = (props) => {
let isPanelOpen = false;
return (
<div onClick={() => isPanelOpen = !isPanelOpen }>Toggle</div>
{isPanelOpen && <div className="info-panel">
{props.children}
</div>}
);
};
I see that the value of isPanelOpen changes to true, but the panel is not being shown. I assume that is because this is the stateless function that doesn't get called again, so once we return the jsx it will have the value of false, and won't update it later.
Is there a way of fixing this, and avoiding of pasing this single variable as props through 4 more parent stateless components?
You can't tell React to re-render the UI by assigning new value directly to the variable (in your case you did isPanelOpen = !isPanelOpen).
The correct method is to use setState.
But you cannot do it in a stateless component, you must do it in a stateful component, so your code should looks like this
import React, {Component} from 'react';
class playerInfo extends Component {
constructor(props){
super(props);
this.state = {
isPanelOpen: false
}
}
render() {
return (
<div onClick={() => this.setState({isPanelOpen: !this.state.isPanelOpen})}>Toggle</div>
{this.state.isPanelOpen && <div className="info-panel">
{this.props.children}
</div>}
);
}
}
Explanation
Remember two things:
1) Your UI should only bind to this.state.XXXX (for stateful component) or props.XXX (for stateless component).
2) The only way to update UI is by calling setState() method, no other way will trigger React to re-render the UI.
But... how do I update stateless component since it doesn't have the setState method?
ONE ANSWER:The stateless component should be contained in another stateful component.
Example
Let's say your stateless component is called Kid, and you have another stateful component called Mum.
import React, {Component} from 'react';
class Mum extends Component {
constructor(props){
super(props);
this.state = {
isHappy: false
}
}
render() {
return (
<div>
<button onClick={() => this.setState({isHappy: true})}>Eat</button>
<Kid isHappy={this.state.isHappy}/>
</div>
);
}
}
const Kid = (props) => (props.isHappy ? <span>I'm happy</span> : <span>I'm sad</span>);
You can do this by using useState hooks like this:
import { useState } from "react";
function playerInfo () {
const [panel, setPanel] = useState(false);
function toggleButton () {
if(!panel) setPanel(true);
else setPanel(false);
}
return (
<div>
<button onClick={toggleButton}>Toggle</div>
panel ? {this.props.children} : null;
</div>}
);
};
export default playerInfo;
I assume that is because this is the stateless function that doesn't get called again
Basically, the only way to re-render component is to change state or props. :)
So when you change a local variable, React doesn't get notified about it and doesn't start reconcilation.
You can do this with native Javascipt otherwise in React you can not do this with stateless Component :)
const playerInfo = (props) => {
let isPanelOpen = false;
return ( <
div onClick = {
() => {
if (document.getElementsByClassName("info-panel")[0].style.display == 'none') {
isPanelOpen = true;
document.getElementsByClassName("info-panel")[0].style.display = '';
} else {
isPanelOpen = false;
document.getElementsByClassName("info-panel")[0].style.display = 'none';
}
}
} > Toggle < /div> <
div className = "info-panel" > {
this.props.children
} <
/div>
);
};
As of version 16.8 of React you can handle this for stateless components using a hook - in this case useState. In it's simplest form it can be implemented like this:
import React, { useState } from 'react';
const PlayerInfo = (props) => {
const [showPanel, togglePanel] = useState(false);
return (
<div onClick={() => togglePanel(!showPanel) }>Toggle</div>
{showPanel && (
<div className="info-panel">
{props.children}
</div>
)}
</div>
);
};
export default PlayerInfo;
A functional component will not re-render unless a previous parent's state changes and passes down updated properties that propagate down to your functional component. You can pass an onClick handler in from a stateful parent and call this from your stateless component when its onClick is triggered. The parent will control the toggling of the display and pass it in as a prop to the child (see snippet below).
To architect this, you should determine if your HOC (higher order component) should be in charge of UI state. If so, then it can make the determination if its child component should be in an open state or not and then pass that as a property to the child state. If this is a component that should open and close independent of the world around it, then it should probably have its own state. For example, if you are making a tabbed widget, it should probably be controlling its own open and closed states.
class App extends React.Component {
state= {
isOpen: false
}
handleClick = () => {
this.setState({
isOpen: !this.state.isOpen
})
}
render() {
return (
<div>
<YourComponent isOpen={this.state.isOpen} handleClick={this.handleClick} />
</div>
)
}
}
const YourComponent = ({isOpen, handleClick} = props) => {
const onClick = () => {
if (handleClick) {
handleClick();
}
}
return (
<div onClick={onClick}>
{isOpen ?
<h2>it is open</h2>
:
<h2>it is closed</h2>
}
</div>
)
}
ReactDOM.render(<App />, 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>
If your major concern is about passing properties/methods down to too many components, you could create a pure component which will give you access to state but not all the overhead of a React Component subclass. You could also look into using a state management tool like Redux.

Calling a method that belongs to a child component from a component that sits in a parent component

I am trying to call a method in a child component from a component that sits on the paerent component. Please see below:
App class
// App.js
class App extends Component {
render() {
return (
<div className="App">
<SimpleModal/>
<FloatingButton onClick={/*would like to call HandleOpen in SimpleModal*/}/>
</div>
);
}
}
export default App;
And here's that SimpleModal component
//SimpleModal
class SimpleModal extends React.Component {
state = {
open: false,
};
handleOpen = () => {
this.setState({ open: true });
};
handleClose = () => {
this.setState({ open: false });
};
render() {
return (
<Modal
aria-labelledby="simple-modal-title"
aria-describedby="simple-modal-description"
open={this.state.open}
onClose={this.handleClose}
>
<div style={getModalStyle()}>
<Typography type="title" id="modal-title">
Text in a modal
</Typography>
<Typography type="subheading" id="simple-modal-description">
Description
</Typography>
</div>
</Modal>
);
}
}
export default SimpleModal;
Basically I would like to call HandleOpen and HandleCall from FloatingButton. How should I do this? Thank you in advance.
Define both functions in the parent component (<App />) and pass them to the child via props. This way both <FloatingButton> and <SimpleModal> can have access to the same function. This technique is called "lift state up" and there is a very good article about it.
Instead of calling the functions from a sibling component, you could lift the state to the common parent component.
This allows you to pass the event handler into the FloatingButton component, and the current state of the modal into the Modal component.
For more infos, check out the relevant section in the React docs:
Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.

Making react children NOT to refresh when parents state changes

So here is my problem. I have an root component that contains navigation and Switch with every component in my page. Navigation is sliding in and out from the left and the way I'm doing this, I'm changing the state in root component, passing prop to and deciding whether or not, should I add class to my nav. The problem is that every component in my app is re-rendering on opening/closing nav. Here is my root component:
class App extends Component {
constructor(props) {
super(props);
this.state = {
navOpen: false
}
}
toggleNav = () => {
this.setState({
navOpen: !this.state.navOpen
})
}
closeNav = (e) => {
if (this.state.navOpen) {
this.setState({navOpen: false})
}
}
render() {
return (
<main>
<Header/>
<Hamburger navOpen={this.state.navOpen} toggleNav={this.toggleNav}/>
<Navigation navOpen={this.state.navOpen} toggleNav={this.toggleNav}/>
<section className="container-fluid content" onClick={this.closeNav}>
<Switch>
<Route path="/add-recipe/:groupId?" component={NewRecipe}/>
<Route path="/recipes/:page?/:sortType?/:size?" component={RecipeList}/>
<Route path="/recipe/:id" component={Recipe}/>
<Route path="/sign/" component={SignForm}/>
<Route path="/user/:id" component={User}/>
</Switch>
</section>
</main>
);
}
componentDidMount() {
this.props.userActions.getUser(this.props.url);
}
}
function mapStateToProps(state) {
return {url: state.url.url, user: state.user.loggedUser}
}
function mapDispatchToProps(dispatch) {
return {
userActions: bindActionCreators(userActions, dispatch)
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));
Navigation is the only component ( besides hamburger) that cares about his parents state so I have no idea why everything is re-rendering. Does anyone have some ideas?
EDIT:
I've added sCU to my nested components like that:
shouldComponentUpdate(nextProps, nextState) {
// console.log(this.props)
// console.log("next")
// console.log(nextProps)
if (this.props == nextProps) {
return false
}
return true
}
But it didn't change anything. When I open the navigation props for routes remain the same but they still rerender. I tried to move "navOpen" state to navigation and open it from root component via "ref" but every time I call its method I get "Cannot read property 'toggleNav' of null"
You can prevent re-rendering by implementing shouldComponentUpdate on the affected component. Beware though, that rendering does not mean that the DOM is being changed. Rendering in react means that react creates a new copy of its internal node tree and compares it to the DOM to see if and where it needs to update it. So don't worry about re-renders unless your component is expensive to create (i.e. it performs a request to a backend or is heavily animated)

Categories

Resources