Inside a small portion of my React/Redux/ReactRouterV4 application, I have the following component hierarchy,
- Exhibit (Parent)
-- ExhibitOne
-- ExhibitTwo
-- ExhibitThree
Within the children of Exhibit, there are about 6 different possible routes that can be rendered as well. Don't worry, I will explain with some code.
Here is my Parent Exhibit Component:
export class Exhibit extends Component {
render() {
const { match, backgroundImage } = this.props
return (
<div className="exhibit">
<Header />
<SecondaryHeader />
<div className="journey"
style={{
color: 'white',
backgroundImage: `url(${backgroundImage})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center-center'
}}>
<Switch>
<Route path={`${match.url}/exhibit-one`} component={ExhibitOne} />
<Route path={`${match.url}/exhibit-two`} component={ExhibitTwo} />
<Route path={`${match.url}/exhibit-three`} component={ExhibitThree} />
<Redirect to="/" />
</Switch>
</div>
</div>
)
}
}
Basically, all its does for its job is to display one of the exhibits subcomponents, and set a background image.
Here is one of the subcomponents, ExhibitOne:
export default class ExhibitOne extends Component {
constructor(props) {
super(props)
}
render() {
const { match } = this.props
return (
<div className="exhibit-one">
<Switch>
<Route path={`${match.url}/wall-one`} component={ExhibitHOC(WallOne)} />
<Route path={`${match.url}/wall-two`} component={ExhibitHOC(WallTwo)} />
<Route path={`${match.url}/wall-three`} component={ExhibitHOC(WallThree)} />
<Route path={`${match.url}/wall-four`} component={ExhibitHOC(WallFour)} />
<Route path={`${match.url}/wall-five`} component={ExhibitHOC(WallFive)} />
<Route path={`${match.url}/wall-six`} component={ExhibitHOC(WallSix)} />
</Switch>
</div>
)
}
}
In order to cut down on typing, I decided to wrap the components in a Higher Order Component, whose
purpose is to dispatch an action that will set the proper background image on the top level Exhibit parent component.
This is the Higher Order Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions/wall-background-image'
export default function(ComposedComponent) {
class ExhibitHoc extends Component {
componentDidMount = () => this.props.setBackgroundImage(`./img/exhibit-one/${this.getWall()}/bg.jpg`)
getWall = () => {
// this part isnt important. it is a function that determines what wall I am on, in order to set
// the proper image.
}
render() {
return <ComposedComponent />
}
}
return connect(null, actions)(ExhibitHoc);
}
On initial load of ExhibitOne, I can see that the setBackgroundImage action creator executes twice by looking
at Redux Logger in the console. My initial inclination to use componentDidMount was because I thought using it
would limit the action creator to execute only once. Here is a screenshot of the log:
I think I might be misunderstanding how Higher Order Components work, or maybe its some type of React Router V4 thing?
Anyways, any help would be greatly appreciated as to why this executes twice.
Here in 2020, this was being caused by <React.StrictMode> component that was wrapped around the <App /> in new versions of Create React App. Removing the offending component from index.js fixed the double mount problem for all of my components. This was by design, but it was annoying and misleading to see console.log() twice for everything.
For Next.js, in next.config.js, set reactStrictMode:false.
The problem is that the component prop here is a function application, which yields a new class on each render. This will cause the previous component to unmount and the new one to mount (see the docs for react-router for more information). Normally you would use the render prop to handle this, but this won't work with higher-order components, as any component that is created with a HOC application during rendering will get remounted during React's reconciliation anyway.
A simple solution is to create your components outside the ExhibitOne class, e.g.:
const ExhibitWallOne = ExhibitHOC(WallOne);
const ExhibitWallTwo = ExhibitHOC(WallTwo);
..
export default class ExhibitOne extends Component {
..
<Route path={`${match.url}/wall-one`} component={ExhibitWallOne} />
<Route path={`${match.url}/wall-two`} component={ExhibitWallTwo} />
..
}
Alternatively, depending on what the wrapper does, it might be possible to declare it as a normal component that renders {this.props.children} instead of the parameter <ComposedComponent/>, and wrap the components in each Route:
<Route path={`${match.url}/wall-one`}
render={(props) => <Wrap><WallOne {...props}/></Wrap>}
/>
Note that you'll need to use render instead of component to prevent remounting. If the components don't use routing props, you could even remove {...props}.
If you use 'Hidden Material UI React', it mounts your component every time you call it. For example, I wrote the below one:
<Hidden mdDown implementation="css">
<Container component="main" maxWidth="sm">
{content}
</Container>
</Hidden>
<Hidden smUp implementation="css">
{content}
</Hidden>
It invokes both contents in both hidden components. it took me a lot of time.
Related
I have setup a ToastContainer in my main App.js
return (
<div className='wrapper' style={{height: '100%'}}>
<ToastContainer
limit={1}
containerId={'error'}
position='bottom-left'
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss={false}
draggable
pauseOnHover
theme='light'/>
<BrowserRouter>
<Switch>
<Route exact path='/' component={() => (renderComponent(view.type))} />
</Switch>
</BrowserRouter>
</div>
And I have an errorHandler function as well
const errorHandler = () => {
toast.error(<ErrorToast
errorUUID={localStorage.getItem('errorUUID')}/>, {containerId: 'error', toastId: 'error'});
}
I have about 1000+ components in my application and almost all of them have a renderData async function that does component-specific fetch calls.
What I want to do, is use this error handler function as the primary error function that is used to handle all async fetch calls that are made in all these different classes.
My options as I see them: -
1) Create a ToastContainer for every component that needs it -
Problem -
a) messy code
b) changing the errorHandler from ToastContainer to something else in the future would be a big pain
c) one page could have multiple error toasts that show up in a situation where the fetch call is from the same backend service in separate containers.
2) Pass the errorHandler as a prop to all children, and do a callback
Problem -
a) messy code
b) Ensuring that I pass this errorHandler to every component wherever it is used is bound to fail at some point
c) Will have to maintain this practice of passing errorHandler to all future components
What I want to know is -
Am I doomed to use one of these two methods, or is there a method that I have not yet considered? And if so, what is it, and what are the pros/cons of it?
Maybe you can use redux toolkit and transfer your function in to reducers. After that you can call reducers in all components of your app with different payloads.
redux toolkit doc
I tried to do iHelidor's suggestion, but having had no experience with Redux, it was becoming a bit of a challenge. Halfway through I realized that there is a simpler answer.
I cannot say if this is the best method, but at the very least, I do not have to worry about transferring this function to all children components everywhere they are called.
Step 1 -
Shift The return Component in App.js to a const
const App = <div className='wrapper' style={{height: '100%'}}>
<ToastContainer
limit={1}
containerId={'error'}
position='bottom-left'
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss={false}
draggable
pauseOnHover
theme='light'/>
<BrowserRouter>
<Switch>
<Route exact path='/' component={() => (renderComponent(view.type))} />
</Switch>
</BrowserRouter>
</div>
Step 2 -
Export the function and component as a list
return [
App,
errorHandler
];
Step 3 -
Create a Simple ErrorHandler component
class ErrorHandler extends React.Component {
constructor(props) {
super(props);
this.app = new App();
}
handleError() {
this.app[1]();
}
}
export default ErrorHandler;
Step 4 -
Import the Error Handler and use it
import ErrorHandler from '#Root/ErrorHandler';
...
...
const errorHandler = new ErrorHandler();
...
...
errorHandler.handleError();
I am struggling with the implementation of the "react-intersection-observer" and i can't for the life of me find a solution.
Details:
I have a simple presentation site which i wanna do with React so i can also learn. Right now the website has only the homepage and the homepage has so far these sections: header, about, portfolio and contact form.
What i wanna do is to simply add a class on each section (about, portfolio and contact form) once the section is in viewport. The kind of stuff that with jquery would be over in 2 minutes.
I have installed "react-intersection-observer" and so far the code in my homepage.component.jsx looks like this:
import React from 'react';
import Header from "../../components/header/header.component";
import About from "../../components/about/about.component";
import PortfolioList from "../../components/portfolio-list/portfolio-list.component";
import ContactForm from "../../components/contact-form/contact-form.component";
import { useInView } from 'react-intersection-observer';
const HomePage = () => {
const { ref, inView, entry } = useInView({
/* Optional options */
triggerOnce: true,
threshold: 1,
onChange: (inView, entry) => {
console.log("salam");
console.log(inView);
console.log(entry);
if (inView) {
entry.target.classList.add("in-view");
}
}
});
return (
<div>
<Header/>
<main className="main">
<About />
<PortfolioList />
<ContactForm />
</main>
</div>
);
};
export default HomePage;
When i have added ref={ref} on each component like this:
<About ref={ref} />
<PortfolioList ref={ref} />
<ContactForm ref={ref} />
i have received an error: Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
The thing is that i don't want to add the useInView module in each of the 3 jsx components because it seems bad practice to have repeat code.
Passing ref as props is bad practice.
Use React.forwardRef instead:
https://beta.reactjs.org/apis/react/forwardRef
Check this example:
https://beta.reactjs.org/apis/react/forwardRef#forwarding-a-ref-through-multiple-components
I want to create a component which takes the children provided to it and displays them in a different component.
For example, let's say I have the following layout:
<Layout>
<PageContent />
<Sidebar />
</Layout>
...and within the PageContent, I want to use another component (e.g. <SidebarContent>), such that the children given to the SidebarContent component are rendered as children of the Sidebar.
Example PageContent:
import { useState, useEffect } from 'react'
const PageContent = () => (
<>
<p>Page Content</p>
<SidebarContent>Sidebar Content</SidebarContent>
</>
)
}
export default PageContent
What is considered best practice for achieving this?
I'm rendering a very large list of images that, when clicked individually, are added to a "viewer" div. The problem is that each time I add an image to the viewer, the original list re-renders, even though no changes have been made to the list's content.
I've tried using shouldComponentUpdate() at every level, as well as using React.memo. Neither appear to have any effect. I've also looked in to whether the time should be spent making the components functional and researching hooks (useContext() looks enticing), but I'm too new at React to know if that would just be more time wasted. (Please feel free weigh in on whether this is a waste of time.)
I don't know where the problem is, so I'm not sure a snippet would do much good. Instead, I've stripped down the problem to its bones and posted a sandbox version here
https://codesandbox.io/s/async-darkness-l920b
At the moment, my shouldComponentUpdate comparison is pretty straightforward for each class; something like:
if (nextProps.photoData === this.props.photoData) {
return false;
} else {
return true;
}
If you open the codesandbox console you'll see I'm logging Year.js > <ImageList /> is rendering to flag each successive render of the list in question.
Any help, even a nudge in the right direction, would be hugely appreciated. I've been reading blog articles for a solid day now and nothing seems to help.
That's because the PhotoView in App.js is defined inside render method, so when state update causing the render, then the PhotoView redefined again. It's a new component every time for The App component.
Please define components outside the render function:
import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Year from "./Year";
import Viewer from "./Viewer";
import dataObj from "./dataObj.json";
import "./App.css";
class App extends React.Component {
constructor(props) {
super(props);
this.PhotoView = this.PhotoView.bind(this)
this.state = {
current: {
year: 2019,
url: ""
},
viewerData: [],
photoData: null
};
}
componentDidMount() {
this.setState({
photoData: dataObj
});
}
addToViewer = moment =>
this.setState(state => {
const viewerData = state.viewerData.concat(moment.props.data);
return {
viewerData,
value: ""
};
});
About() {
return (
<div>
<h1>About</h1>
</div>
);
};
PhotoView(url) {
return (
<div className="PhotoView">
<Year
setCurrent={this.setCurrent}
photoData={this.state.photoData}
addToViewer={this.addToViewer}
/>
<Viewer
viewerData={this.state.viewerData}
setCurrent={this.setCurrent}
/>
</div>
);
}
render() {
return (
<div className="App">
<Router>
<nav>
<Link to="/about">About</Link>
<Link to="/">Photo View</Link>
</nav>
<Switch>
<Route path="/about" exact component={this.About} />
<Route path="/" component={this.PhotoView} />
</Switch>
</Router>
</div>
);
}
}
export default App;
Or move them to individual files.
I built a large application where a single button on the navbar opens a modal.
I'm keeping track of the modalOpen state using context API.
So, user clicks button on navbar. Modal Opens. Modal has container called QuoteCalculator.
QuoteCalculator looks as follows:
class QuoteCalculator extends React.Component {
static contextType = ModalContext;
// ...
onSubmit = () => {
// ...
this.context.toggleModal();
this.props.history.push('/quote');
// ..
};
render() {
//...
return(<Question {...props} next={this.onSubmit} />;)
}
}
export default withRouter(QuoteCalculator);
Now, everything works as expected. When the user submits, I go to the right route. I just see the following warning on the console
index.js:1446 Warning: withRouter(QuoteCalculator): Function
components do not support contextType.
I'm tempted to ignore the warning, but I don't think its a good idea.
I tried using Redirect alternatively. So something like
QuoteCalculator looks as follows:
class QuoteCalculator extends React.Component {
static contextType = ModalContext;
// ...
onSubmit = () => {
// ...
this.context.toggleModal();
this.setState({done: true});
// ..
};
render() {
let toDisplay;
if(this.state.done) {
toDisplay = <Redirect to="/quote"/>
} else {
toDipslay = <Question {...props} next={this.onSubmit} />;
}
return(<>{toDisplay}</>)
}
}
export default QuoteCalculator;
The problem with this approach is that I kept on getting the error
You tried to redirect to the same route you're currently on
Also, I'd rather not use this approach, just because then I'd have to undo the state done (otherwise when user clicks button again, done is true, and we'll just get redirected) ...
Any idea whats going on with withRouter and history.push?
Here's my app
class App extends Component {
render() {
return (
<Layout>
<Switch>
<Route path="/quote" component={Quote} />
<Route path="/pricing" component={Pricing} />
<Route path="/about" component={About} />
<Route path="/faq" component={FAQ} />
<Route path="/" exact component={Home} />
<Redirect to="/" />
</Switch>
</Layout>
);
}
}
Source of the warning
Unlike most higher order components, withRouter is wrapping the component you pass inside a functional component instead of a class component. But it's still calling hoistStatics, which is taking your contextType static and moving it to the function component returned by withRouter. That should usually be fine, but you've found an instance where it's not. You can check the repo code for more details, but it's short so I'm just going to drop the relevant lines here for you:
function withRouter(Component) {
// this is a functional component
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
<Route
children={routeComponentProps => (
<Component
{...remainingProps}
{...routeComponentProps}
ref={wrappedComponentRef}
/>
)}
/>
);
};
// ...
// hoistStatics moves statics from Component to C
return hoistStatics(C, Component);
}
It really shouldn't negatively impact anything. Your context will still work and will just be ignored on the wrapping component returned from withRouter. However, it's not difficult to alter things to remove that problem.
Possible Solutions
Simplest
Since all you need in your modal is history.push, you could just pass that as a prop from the modal's parent component. Given the setup you described, I'm guessing the modal is included in one place in the app, fairly high up in the component tree. If the component that includes your modal is already a Route component, then it has access to history and can just pass push along to the modal. If it's not, then wrap the parent component in withRouter to get access to the router props.
Not bad
You could also make your modal component a simple wrapper around your modal content/functionality, using the ModalContext.Consumer component to pass the needed context down as props instead of using contextType.
const Modal = () => (
<ModalContext.Consumer>
{value => <ModalContent {...value} />}
</ModalContext.Consumer>
)
class ModalContent extends React.Component {
onSubmit = () => {
// ...
this.props.toggleModal()
this.props.history.push('/quote')
// ..
}
// ...
}