React: triggering method inside HOC component - reactjs

What I want to do, is create a HOC that has a method that can be triggered by whatever Parent Component is using that HOC to wrap.
For this HOC, I'm trying to fade out the HOC and any components inside it:
HOC:
export function fadeOutWrapper(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
showElement: true,
removeElement: false,
};
}
_triggerFade = () => {
this._fadeOut(this.props.time).then(time => this._removeElement(time));
}
_fadeOut = time => {
let _this = this;
return new Promise((resolve, reject) => {
_this.setState({
showElement: false
});
setTimeout(() => {
resolve(time);
}, time);
});
};
_removeElement = time => {
let _this = this;
setTimeout(() => {
_this.setState({
removeElement: true
});
}, time + 500);
};
render() {
return this.state.removeElement ? null : (
<div
className={
this.state.showElement
? "cfd-container"
: "cfd-container cfd-fadeout"
}
>
<WrappedComponent {...this.props} />
</div>
);
}
};
}
How this component is being used in parent component:
import ComponentToBeFaded from '...';
import { fadeOutWrapper } from '...';
const WrappedComponent = fadeOutWrapper(ComponentToBeFaded);
class ParentComponent extends Component {
const...
super...
handleChildClick = () => {
// ? how to trigger the HOC _triggerFade method?
// WrappedComponent._triggerFade()
}
render() {
return (
<WrappedComponent time={1000} handleClick={this.handleChildClick} {...other props component needs} />
)
}
}
What I want to be able to do is call a method that is inside the HOC, can't seem to check for a change in props inside the HOC... only inside the HOC's render()
Need to keep writing more to meet the submission quota. Any thoughts on how to do this is appreciated. Hope your day is going well!

You don't need showElement in local state of the wrapped component because it's not controlled by that component. Pass it as props and use componentDidUpdate to start fading out.
const { Component, useState, useCallback } = React;
const Button = ({ onClick }) => (
<button onClick={onClick}>Remove</button>
);
function App() {
const [show, setShow] = useState(true);
const onClick = useCallback(() => setShow(s => !s), []);
return (
<WrappedButton
time={1000}
onClick={onClick}
showElement={show}
/>
);
}
function fadeOutWrapper(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
removeElement: false,
fadeout: false,
};
}
componentDidUpdate(prevProps) {
if (
this.props.showElement !== prevProps.showElement &&
!this.props.showElement
) {
this._triggerFade();
}
}
_triggerFade = () => {
this._fadeOut(this.props.time).then(() =>
this._removeElement()
);
};
_fadeOut = time => {
this.setState({ fadeout: true });
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, time);
});
};
_removeElement = time => {
this.setState({
removeElement: true,
});
};
render() {
return this.state.removeElement ? null : (
<div>
{JSON.stringify(this.state)}
<WrappedComponent {...this.props} />
</div>
);
}
};
}
const WrappedButton = fadeOutWrapper(Button);
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

How to stop react component from infinite re-render

I wanna use js-cookie in my app, but the time I get the cookie my component keeps re-rendering until the browser crash.
The error I get is: setState(...): Cannot update during an existing state transition ...
I've just used the shouldComponentUpdate but it caused the click events not working.
shouldComponentUpdate(nextProps, nextState)
{
return nextState.language != this.state.language;
}
Does anybody know any other solution rather than shouldComponentUpdate to stop a component from infinite re-render?
class MainLayout extends Component {
constructor(props) {
super(props);
console.log('constructor');
this.state = {
sideBarOpen: false,
languages: getStoreLanguages,
language: Cookies.get('langCode')
}
}
componentWillMount() {
this.props.langCode();
this.props.defaultLangCode();
}
componentDidMount() {
$('.dropdown-toggle').megaMenu && $('.dropdown-toggle').megaMenu({ container: '.mmd' });
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.language != this.state.language;
}
toggleSidebar = () => {
this.setState({
sideBarOpen: !this.state.sideBarOpen,
});
}
overlayClickHandler = () => {
this.setState({
sideBarOpen: false,
});
}
handleLanguage = (langCode) => {
if (Cookies.get('langCode')) {
return Cookies.get('langCode');
} else {
Cookies.set('langCode', langCode, { expires: 7 });
return langCode;
}
}
render() {
let overlay = { display: this.state.sideBarOpen ? 'block' : 'none' };
const langCode = this.handleLanguage(this.props.params.id);
const isDefaultLang = isDefaultLanguage(langCode);
const isValidLang = isValidLanguage(langCode);
if (langCode && !isValidLang) {
this.props.router.push(`/${langCode}/error`);
}
if (langCode && isValidLang) {
const path = getLocalisedPath(langCode, isDefaultLang)
this.props.router.push("/" + path);
}
return (
<div>
<Helmet>
<script type="application/ld+json">{structuredData()}</script>
</Helmet>
<TokenManager>
{(state, methods) => (
<div>
<WithHelmet {...this.props} />
<WithUserHeaderInfo {...this.props} />
<WithStoreDetail />
<WithDataLayerStoreDetail />
<header className='header__nav'>
<NavbarManagerWillHideOnEditor
sideBarOpen={this.state.sideBarOpen}
toggleSidebar={this.toggleSidebar}
languages={this.state.languages}
{...this.props}
/>
<div
className="mmm__overlay"
onClick={this.overlayClickHandler}
style={overlay}
/>
<div
className="mmm__overlay--hidenav"
onClick={this.overlayClickHandler}
style={overlay}
/>
</header>
<main>{this.props.children}</main>
<Modal modalId="modal-account" size="md">
{(closeModal) => (
<Auth
closeModal={closeModal} />
)}
</Modal>
{!this.props.location.pathname.startsWith('/checkout') && <FooterWillHideOnEditor languages={this.state.languages}/>}
</div>
)
}
</TokenManager>
</div>
);
}
}
const mapDispatchToProps = (dispatch, value) => {
const langCode = Cookies.get('langCode') || value.params.id;
const defaultLang = getDefaultLanguage();
const isDefault = isDefaultLanguage(langCode);
const isValid = isValidLanguage(langCode);
const lang = !isValid || isDefault ? defaultLang : langCode;
return {
langCode: () => dispatch({ type: 'SET_LANGUAGE', payload: lang }),
defaultLangCode: () => dispatch({ type: 'SET_DEFAULT_LANGUAGE', payload: defaultLang })
}
}
export default connect(null, mapDispatchToProps)(MainLayout);
let overlay = { display: this.state.sideBarOpen ? 'block' : 'none' };
const langCode = this.handleLanguage(this.props.params.id);
const isDefaultLang = isDefaultLanguage(langCode);
const isValidLang = isValidLanguage(langCode);
if (langCode && !isValidLang) {
this.props.router.push(`/${langCode}/error`);
}
if (langCode && isValidLang) {
const path = getLocalisedPath(langCode, isDefaultLang)
this.props.router.push("/" + path);
}
Here the code is resetting the state/ or mapDispatchToProps dispatched again .that's why it's rerendering.
I found the reason why component keep re-rendering, actually it was because of the condition in:
if (langCode && isValidLang) {
const path = getLocalisedPath(langCode, isDefaultLang)
this.props.router.push("/" + path);
}
which is always true and gets the path and push to the route which cause the component re-render.
Thanks

createPortal does not overwrite div contents (like ReactDOM.render)

I am trying to get ReactDOM.createPortal to override the contents of the container I am mounting it too. However it seems to appendChild.
Is it possible to override contents? Similar to ReactDOM.render?
Here is my code:
import React from 'react';
import { createPortal } from 'react-dom';
class PrivacyContent extends React.Component {
render() {
return createPortal(
<div>
<button onClick={this.handleClick}>
Click me
</button>
</div>,
document.getElementById('privacy')
)
}
handleClick() {
alert('clicked');
}
}
export default PrivacyContent;
If you know what you're doing, here is a <Portal /> component that under the hoods creates a portal, empties the target DOM node and mounts any component with any props:
const Portal = ({ Component, container, ...props }) => {
const [innerHtmlEmptied, setInnerHtmlEmptied] = React.useState(false)
React.useEffect(() => {
if (!innerHtmlEmptied) {
container.innerHTML = ''
setInnerHtmlEmptied(true)
}
}, [innerHtmlEmptied])
if (!innerHtmlEmptied) return null
return ReactDOM.createPortal(<Component {...props} />, container)
}
Usage:
<Portal Component={MyComponent} container={document.body} {...otherProps} />
This empties the content of document.body, then mounts MyComponent while passing down otherProps.
Hope that helps.
In the constructor of the component, you could actually clear the contents of the div before rendering your Portal content:
class PrivacyContent extends React.Component {
constructor(props) {
super(props);
const myNode = document.getElementById("privacy");
while (myNode.firstChild) {
myNode.removeChild(myNode.firstChild);
}
}
render() {
return createPortal(
<div>
<button onClick={this.handleClick}>
Click me
</button>
</div>,
document.getElementById('privacy')
)
}
handleClick() {
alert('clicked');
}
}
export default PrivacyContent;
I find this is better and doesn't need useState:
export const Portal = () => {
const el = useRef(document.createElement('div'));
useEffect(() => {
const current = el.current;
// We assume `root` exists with '?'
if (!root?.hasChildNodes()) {
root?.appendChild(current);
}
return () => void root?.removeChild(current);
}, []);
return createPortal(<Cmp />, el.current);
};
Bit of an old question, but here's another sync solution (without useState). Also in a reusable component format.
const Portal = ({ selector, children, replaceContent = true }) => {
const target = useRef(document.querySelector(selector)).current;
const hasMounted = useRef(false);
if (!target) return null;
if (replaceContent && !hasMounted.current) {
target.innerHTML = '';
hasMounted.current = true;
}
return createPortal(children, target);
};
A solution with zero hook dependencies
import { createPortal } from 'react-dom';
const getNode = (id) => {
const domNode = document.getElementById(id);
const div = document.createElement("div");
domNode?.replaceChildren(div);
return div;
};
const Portal = ({ children }) => {
const domNode = getNode("privacy");
if (domNode) {
return createPortal(children, domNode);
}
return null;
};

Switching between two components in React

rotateRender() {
if(false) {
return(
<TimerPage></TimerPage>
);
} else {
return(
<RepoPage></RepoPage>
);
}
}
I have two components called TimerPage and RepoPage.
I created a simple conditional render function as above, but cannot come up with a condition to make it render iteratively after a certain amount of time.
For example, I first want to render RepoPage and switch to TimerPage after 5 minutes and then stay in TimerPage for 15 mins before I switch again to the RepoPage.
Any way to do this?
Might not be that elegant, but this works
Actually I was thinking that this block might be more elegant than the first one
const FIRST_PAGE = '5_SECONDS';
const SECOND_PAGE = '15_SECONDS';
const FirstComponent = () => (
<div>5 SECONDS</div>
);
const SecondComponent = () => (
<div>15 SECONDS</div>
);
class App extends Component {
state = {
currentPage: FIRST_PAGE
};
componentDidUpdate() {
const {currentPage} = this.state;
const isFirst = currentPage === FIRST_PAGE;
if (isFirst) {
this._showSecondPageDelayed();
} else {
this._showFirstPageDelayed();
}
}
componentDidMount() {
this._showSecondPageDelayed();
};
_showSecondPageDelayed = () => setTimeout(() => {this.setState({currentPage: SECOND_PAGE})}, 5000);
_showFirstPageDelayed = () => setTimeout(() => {this.setState({currentPage: FIRST_PAGE})}, 15000);
render() {
const {currentPage} = this.state;
const isFirst = currentPage === FIRST_PAGE;
const ComponentToRender = isFirst ? FirstComponent : SecondComponent;
return <ComponentToRender/>;
}
}
As stated in the comment section, you can create a higher order component that will cycle through your components based on the state of that component. Use setTimeout to handle the timer logic for the component.
state = {
timer: true
}
componentDidMount = () => {
setInterval(
() => {
this.setState({ timer: !this.state.timer })
}, 30000)
}
render(){
const {timer} = this.state
if(timer){
return <TimerPage />
} else {
return <RepoPage />
}
}
Edit
Changed setTimeout to setInterval so that it will loop every 5 minutes instead of just calling setState once
You could use the new context API to achieve this. The benefit is now I have a configurable, reusable provider to play with throughout my application. Here is a quick demo:
https://codesandbox.io/s/k2vvy54r8o
import React, { Component, createContext } from "react";
import { render } from "react-dom";
const ThemeContext = createContext({ alternativeTheme: false });
class ThemeWrapper extends Component {
state = {
alternativeTheme: false
};
themeInterval = null;
componentDidMount() {
this.themeInterval = setInterval(
() =>
this.setState(({ alternativeTheme }) => ({
alternativeTheme: !alternativeTheme
})),
this.props.intervalLength
);
}
componentWillUnmount() {
if (this.themeInterval) {
clearInterval(this.themeInterval);
}
}
render() {
return (
<ThemeContext.Provider value={this.state}>
{this.props.children}
</ThemeContext.Provider>
);
}
}
const App = () => (
<ThemeWrapper intervalLength={2000}>
<ThemeContext.Consumer>
{({ alternativeTheme }) =>
alternativeTheme ? <p>Alternative Theme</p> : <p>Common Theme</p>
}
</ThemeContext.Consumer>
</ThemeWrapper>
);
render(<App />, document.getElementById("root"));
Whatever you do make sure on componentWillUnmount to clear your interval or timeout to avoid a memory leak.

Pass state value to component

I am really new in React.js. I wanna pass a state (that i set from api data before) to a component so value of selectable list can dynamically fill from my api data. Here is my code for fetching data :
getListSiswa(){
fetch('http://localhost/assessment-app/adminpg/api/v1/Siswa/')
.then(posts => {
return posts.json();
}).then(data => {
let item = data.posts.map((itm) => {
return(
<div key={itm.siswa_id}>
<ListItem
value={itm.siswa_id}
primaryText={itm.nama}
/>
</div>
)
});
this.setState({item: item});
});
}
From that code, i set a state called item. And i want to pass this state to a component. Here is my code :
const ListSiswa = () => (
<SelectableList>
<Subheader>Daftar Siswa</Subheader>
{this.state.item}
</SelectableList>
);
But i get an error that say
TypeError: Cannot read property 'item' of undefined
I am sorry for my bad explanation. But if you get my point, i am really looking forward for your solution.
Here is my full code for additional info :
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {List, ListItem, makeSelectable} from 'material-ui/List';
import Subheader from 'material-ui/Subheader';
let SelectableList = makeSelectable(List);
function wrapState(ComposedComponent) {
return class SelectableList extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
};
getListSiswa(){
fetch('http://localhost/assessment-app/adminpg/api/v1/Siswa/')
.then(posts => {
return posts.json();
}).then(data => {
let item = data.posts.map((itm) => {
return(
<div key={itm.siswa_id}>
<ListItem
value={itm.siswa_id}
primaryText={itm.nama}
/>
</div>
)
});
this.setState({item: item});
});
}
componentWillMount() {
this.setState({
selectedIndex: this.props.defaultValue,
});
this.getListSiswa();
}
handleRequestChange = (event, index) => {
this.setState({
selectedIndex: index,
});
};
render() {
console.log(this.state.item);
return (
<ComposedComponent
value={this.state.selectedIndex}
onChange={this.handleRequestChange}
>
{this.props.children}
</ComposedComponent>
);
}
};
}
SelectableList = wrapState(SelectableList);
const ListSiswa = () => (
<SelectableList>
<Subheader>Daftar Siswa</Subheader>
{this.state.item}
</SelectableList>
);
export default ListSiswa;
One way to do it is by having the state defined in the parent component instead and pass it down to the child via props:
let SelectableList = makeSelectable(List);
function wrapState(ComposedComponent) {
return class SelectableList extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
};
componentWillMount() {
this.setState({
selectedIndex: this.props.defaultValue,
});
this.props.fetchItem();
}
handleRequestChange = (event, index) => {
this.setState({
selectedIndex: index,
});
};
render() {
console.log(this.state.item);
return (
<ComposedComponent
value={this.state.selectedIndex}
onChange={this.handleRequestChange}
>
{this.props.children}
{this.props.item}
</ComposedComponent>
);
}
};
}
SelectableList = wrapState(SelectableList);
class ListSiswa extends Component {
state = {
item: {}
}
getListSiswa(){
fetch('http://localhost/assessment-app/adminpg/api/v1/Siswa/')
.then(posts => {
return posts.json();
}).then(data => {
let item = data.posts.map((itm) => {
return(
<div key={itm.siswa_id}>
<ListItem
value={itm.siswa_id}
primaryText={itm.nama}
/>
</div>
)
});
this.setState({item: item});
});
}
render() {
return (
<SelectableList item={this.state.item} fetchItem={this.getListSiswa}>
<Subheader>Daftar Siswa</Subheader>
</SelectableList>
);
}
}
export default ListSiswa;
Notice that in wrapState now I'm accessing the state using this.props.item and this.props.fetchItem. This practice is also known as prop drilling in React and it will be an issue once your app scales and multiple nested components. For scaling up you might want to consider using Redux or the Context API. Hope that helps!
The error is in this component.
const ListSiswa = () => (
<SelectableList>
<Subheader>Daftar Siswa</Subheader>
{this.state.item}
</SelectableList>
);
This component is referred as Stateless Functional Components (Read)
It is simply a pure function which receives some data and returns the jsx.
you do not have the access this here.

React JS - updating state within an eventListener

I'm trying to update the state of my component inside of an eventListener. I'm getting the following console error:
'Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the Header component'
This is my component code:
class Header extends React.Component {
constructor(props) {
super(props);
this.state = {
fixed: false
}
}
handleScroll(event) {
this.setState({
fixed: true
});
}
componentDidMount() {
window.addEventListener("scroll",() => {
this.handleScroll();
});
}
componentWillUnmount() {
window.removeEventListener("scroll",() => {
this.handleScroll();
});
}
render() {
var {
dispatch,
className = "",
headerTitle = "Default Header Title",
onReturn,
onContinue
} = this.props;
var renderLeftItem = () => {
if (typeof onReturn === 'function') {
return (
<MenuBarItem icon="navigation-back" onClick={onReturn}/>
)
}
};
var renderRightItem = () => {
if (typeof onContinue === 'function') {
return (
<MenuBarItem icon="navigation-check" onClick= {onContinue}/>
)
}
};
return (
<div className={"header " + className + this.state.fixed}>
{renderLeftItem()}
<div className="header-title">{headerTitle}</div>
{renderRightItem()}
</div>
)
}
}
Header.propTypes = {
};
let mapStateToProps = (state, ownProps) => {
return {};
};
export default connect(mapStateToProps)(Header);
IMHO this is because you do ont unregister the function as you expect it, and a scroll event is sent after an instance of this component has been unmounted
try this:
componentDidMount() {
this._handleScroll = this.handleScroll.bind(this)
window.addEventListener("scroll", this._handleScroll);
}
componentWillUnmount() {
window.removeEventListener("scroll", this._handleScroll);
}

Resources