I've written a container component in ReactJS and am passing in a prop to that component which is to be rendered as the 'main' content, like so:
class RegistrationContainer extends Component {
render() {
const MainContent = this.props.mainContent;
return (
<Row>
<Col offset="lg-3" lg={6}>
<MainContent />
</Col>
</Row>
);
}
}
export default RegistrationContainer;
And I'm passsing to it a mainContent prop like so:
import RegistrationContainer from './RegistrationContainer';
import RegistrationEntryView from './RegistrationEntryView';
class RegistrationCodeEntry extends Component {
render() {
return (
<RegistrationContainer mainContent={RegistrationEntryView} />
);
}
}
export default RegistrationCodeEntry;
My issue is that I would like RegistrationEntryView to have props, but can't seem to figure out how to define/pass in props on it. If I do the following I get an error:
class RegistrationCodeEntry extends Component {
render() {
const RegistrationView = <RegistrationEntryView someProp="blah" /> ;
return (
<RegistrationContainer mainContent={RegistrationView} />
);
}
}
export default RegistrationCodeEntry;
Error is as follows:
invariant.js?7313:42 Uncaught Error: Element type is invalid: expected
a string (for built-in components) or a class/function (for composite
components) but got: object. Check the render method of
RegistrationContainer.
Is this something that this.props.children could solve? I've been struggling to get my head around the concept of that, so any advice on where I'm going wrong would be appreciated.
You can solve this with this.props.children like this
class RegistrationCodeEntry extends Component {
render() {
return (
<RegistrationContainer>
// Render it as children
<RegistrationEntryView someProp="blah" />
</RegistrationContainer>
);
}
}
then in your container
class RegistrationContainer extends Component {
render() {
const MainContent = this.props.mainContent;
return (
<Row>
<Col offset="lg-3" lg={6}>
// Render the passed children
{this.props.children}
</Col>
</Row>
);
}
}
Your approach is correct. You just went wrong here:
<Row>
<Col offset="lg-3" lg={6}>
<MainContent />
</Col>
</Row>
Instead do this:
<Row>
<Col offset="lg-3" lg={6}>
{ MainContent }
</Col>
</Row>
I personally think, this approach is better than using children.
When you did this - const RegistrationView = <RegistrationEntryView someProp="blah" /> ; The component was already rendered and converted to appropriate format. Hence you cannot re-render it with <MainContent />.
So using {} is correct in this case.
Good Luck!
Related
I have two different react components placed one after the other in my app named SearchBar and InfiniteScroller;
function App() {
const [searchTerm, setSearchTerm] = useState("");
return (
<div className="App">
<SNavbar></SNavbar>
<MainLogo></MainLogo>
<SearchBar search={setSearchTerm}></SearchBar>
<hr/>
<InfiniteScroller term={searchTerm}/>
<Footer/>
</div>
);
}
The search bar component has its own state where it updates a search term as its input is being edited and it calls the setSearch function of its parent when the button is clicked (the function is passed as a prop in the parent)
function SearchBar(props)
{
const [search,setSearch] = useState("");
return(
<Container className="Search-Bar">
<Row>
<Col>
<InputGroup >
<FormControl
placeholder="What are we making today?"
onChange={event => setSearch(event.target.value)}
/>
<Button onClick={() => props.search(search)}>
Go!
</Button>
</InputGroup>
</Col>
</Row>
</Container>)
}
The search term that is updated by the SearchBar component is passed onto the InfiniteScroller component as a property and is set as the searchTerm field in its state object.
class InfiniteScroller extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
items:[],
page:1,
hasMore:true,
searchTerm:props.term
};
}
render(){
return(
<InfiniteScroll
dataLength={this.state.items.length}
next={this.fetchData}
hasMore={this.state.hasMore}
loader={<h4>Loading...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
>
<Row>
{this.state.items.map((i, index) => (
<Col key={index} lg="2" md="4" sm="6" xs="12">
<ImageCell className="ImageCell" link = {this.state.items[index].link}> - #{index}</ImageCell>
</Col>
))}
</Row>
</InfiniteScroll>
)
}
}
However when the setSearchTerm function of App.js is triggered by pressing the button on the SearchBar component, the InfiniteScroller does not seem to get updated. As the SearchTerm field of its state still comes up as "undefined" and the component itself does not re-render to represent the change in property.
I want the InfiniteScroller to completely re-render itself and make some API calls to populate itself with content, How can I achieve this?
So far I've tried adding in HTML tags that have the SearchTerm property in them to check if react skips re-rendering components that don't "use" any properties but that has not worked.
The props' change does not make the UI re-rendering but the states' change does.
It has 2 potential ways to fix have a proper UI re-rendering.
For the first one, you can add key attribute to your component that will help you do a trick for re-rendering whenever key gets changed
<InfiniteScroller term={searchTerm} key={searchTerm}/>
The second way, you can update your local states of that component by componentDidUpdate (useEffect in function-based components)
class InfiniteScroller extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
items:[],
page:1,
hasMore:true,
searchTerm:props.term
};
}
//update states according to props change
componentDidUpdate(prevProps) {
if(this.props.searchTerm !== prevProps.searchTerm) {
setState({ searchTerm: this.props.searchTerm })
}
}
render(){
return(
<InfiniteScroll
dataLength={this.state.items.length}
next={this.fetchData}
hasMore={this.state.hasMore}
loader={<h4>Loading...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
>
<Row>
{this.state.items.map((i, index) => (
<Col key={index} lg="2" md="4" sm="6" xs="12">
<ImageCell className="ImageCell" link = {this.state.items[index].link}> - #{index}</ImageCell>
</Col>
))}
</Row>
</InfiniteScroll>
)
}
}
when i'm updating my state using setState from parent component my child component get rendering(because props getting change)
Parent component
addonsHandler =(addons) =>{
this.setState({addons:addons}, () => {
// console.log(this.state.addons);
});
};
render() {
return (
<div>
<Row>
<Col span={15} offset={2}>
<AntForm pickupHandler= {this.pickupHandler} dropHandler={this.dropHandler} addonsHandler={this.addonsHandler} ambulanceTypeHandler={this.ambulanceTypeHandler}/>
<Button type="primary" onClick={this.drop} >Drop</Button>
<Button type="primary" onClick={this.calculateRoute}>Direction</Button>
{/*<div id="map" style={{height: "600px"}}></div>*/}
<Map onRef={ref => (this.MapRef = ref)} />
</Col>
<Col span={6} offset={1}>
<BookingDetails addons={this.state.addons} price={this.addonObj} ambulaceType={this.state.AmbulanceType} VehiclePrice={this.ambulacneTypeObj} />
</Col>
</Row>
<Row>
<Col span={15} offset={2}>
</Col>
</Row>
</div>
);
}
so i want to stop rendering only Map component when addons state get change in parent component
so i used shouldComponentUpdate in Map component but it's not stoping rendering to component
shouldComponentUpdate(nextProps, nextState) {
return false;
}
shouldComponentUpdate() affect on parent component. If it returns true, parent component will be rerender.
So, I think you should move shouldComponentUpdate() into BookingDetails component instead.
I have a following example simple page:
App.js:
export default class App extends React.Component {
render() {
return <Router>
<Switch>
<Route exact path='/' component={ArticlesPage}/>
<Route path='/search' component={SearchPage}/>
</Switch>
</Router>
};
};
ArticlesPage.js:
export default class ArticlesPage extends React.Component {
constructor(props) {
super(props);
}
render() {
return <Grid>
<Row>
<Col lg={12}>
<SearchBox/>
</Col>
</Row>
<Row>
<Col lg={12}>
articles
</Col>
</Row>
</Grid>;
}
};
SearchPage.js:
export default class SearchPage extends React.Component {
constructor(props) {
super(props);
const {q} = queryString.parse(location.search);
this.state = {
query: q
};
}
render() {
return <Grid>
<Row>
<Col lg={12}>
<SearchBox/>
</Col>
</Row>
<Row>
<Col lg={12}>
search {this.state.query}
</Col>
</Row>
</Grid>;
}
};
SearchBox.js:
export default class SearchBox extends React.Component {
constructor(props) {
super(props);
this.state = {
q: ''
};
}
onFormSubmit = (e) => {
e.preventDefault();
const {router} = this.context;
router.history.push('/search?q=' + this.state.q);
};
handleChange = (e) => {
this.setState({q: e.target.value});
};
render() {
return <form onSubmit={this.onFormSubmit}>
<Col lg={10} lgOffset={1}>
<FormGroup>
<input type="text" name="q" id="q" ref={i => this.searchInput = i} onChange={this.handleChange} />
</FormGroup>
</Col>
</form>;
}
};
And now, when I'm on the index page and type something in the input next send form, React render SearchPage.js and return correctly text search *and what I typed*, try again type something else in the input and send form, and React still show my previous text (not rerender).
What can be wrong with this simple page?
You have two different state variables, query on <SearchPage /> and q on <SearchBox />. What you are changing is q, but the variable you are rendering as text is query.
You need to lift state up and pass query as prop to <SearchPage />.
Here's why the text on SearchPage doesn't update: the constructor runs once and updates the variable in state, but when the app re-renders, React, wanting to be efficient, sees that it would re-render a new SearchPage in the same spot as the previous one, so instead of replacing it, it keeps the state of the old one. Because of this, SearchPage's state still keeps the old q variable.
Here's how you can fix it: make your SearchPage accept the search query as a prop, and render that.
class SearchPage extends React.Component {
render() {
return (
<Grid>
<Row>
<Col lg={12}>
<SearchBox />
</Col>
</Row>
<Row>
<Col lg={12}>search {this.props.query}</Col>
</Row>
</Grid>
)
}
}
In the parent, where the route for it is being rendered, use a render function, take the props of it, parse the actual query from props.location.search, and pass it directly to SearchPage.
<Route
path="/search"
render={props => <SearchPage query={getSearchQuery(props.location.search)} />}
/>
// utility function to keep things clean
function getSearchQuery(locationSearch) {
return queryString.parse(locationSearch.slice(1)).q
}
Here's a working demo.
I have a problem with my Higher Order Component. I am trying to pass props from a <Layout /> component down a route (React Router v4). The components specified in the routes are wrapped by a HOC, but the props that I pass never reaches the component.
Also, I can't use the HOC without using export default () => MyHOC(MyComponent). I can't figure out why, but that might have something to do with it?
Layout.js
const Layout = ({ location, initialData, routeData, authenticateUser }) => (
<Wrapper>
<Container>
<Switch>
// how do I get these props passed through the HOC? render instead of component made no difference.
<Route exact path="/pages/page-one" component={() => <PageOne routeData={routeData} title="PageOne" />} />
<Route exact path="/pages/page-two" component={() => <PageTwo routeData={routeData} title="PageTwo" />} />
<Route component={NotFound} />
</Switch>
</Container>
</Wrapper>
)
export default Layout
Page.js
// I've tried swapping to (WrappedComponent) => (props) without success
const Page = (props) => (WrappedComponent) => {
const renderHeader = props.header
? <Header title={props.headerTitle} />
: false
return (
<Wrapper>
{renderHeader}
<Container withHeader={props.header}>
<WrappedComponent />
</Container>
</Wrapper>
)
}
export default Page
PageOne.js
class PageOne extends React.Component {
render() {
return (
<Content>
<Title>{this.props.title}</Title> // <----- not working!
{JSON.stringify(this.props.routeData, null, 4)} // <---- not working!
</Content>
)
}
}
export default () => Page({ header: true, headerTitle: 'header title' })(PageOne)
// does not work without () => Page
// when using just export default Page I get the "Invariant Violation: Element type is invalid:
// expected a string (for built-in components) or a class/function (for composite components)
// but got: object. Check the render method of Route." error.
You need one more arrow to make your Page to be a HOC. It takes params, wrapped component and has to return a component. Yours were rendering after getting WrappedComponent
const Page = (props) => (WrappedComponent) => (moreProps) => {
const renderHeader = props.header
? <Header title={props.headerTitle} />
: false
return (
<Wrapper>
{renderHeader}
<Container withHeader={props.header}>
<WrappedComponent {...moreProps} />
</Container>
</Wrapper>
)
}
Now you can use it like this
export default Page({ header: true, headerTitle: 'header title' })(PageOne)
My goal is to reuse modal from bootstrap in multiple components. I have a question how can I pass a component to container?
<ModalContainer
title="Password recovery"
body="<LoginRecoverForm someprophere='' />"
/>
This gives an error:
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. Check the render method of `ModalLoginRecover`.
Here's simple container:
import { Modal } from 'react-bootstrap/lib/'
var React = require('react')
var ModalContainer = React.createClass({
getInitialState() {
return { showModal: true };
},
close() {
this.setState({ showModal: false });
browserHistory.push('/login');
},
open() {
this.setState({ showModal: true });
},
render: function () {
return (
<div>
<Modal show onHide={this.close}>
<Modal.Header closeButton>
<Modal.Title>{this.props.title}</Modal.Title>
</Modal.Header>
<Modal.Body>
{this.props.body}
</Modal.Body>
</Modal>
{this.props.children}
</div>
)}});
module.exports = ModalContainer;
Here's simple component:
import {Modal, HelpBlock, FormGroup, Button, FormControl } from 'react-bootstrap/lib/'
var React = require('react');
import { Router, ReactRouter, browserHistory } from 'react-router';
var ModalContainer = require('../../containers/ModalDialog')
function FieldGroup({ id, help, type, placeholder }) {
return (
<FormGroup controlId={id}>
<FormControl type={type} placeholder={placeholder} />
{help && <HelpBlock>{help}</HelpBlock>}
</FormGroup>
);
}
function LoginRecoverForm(){
return (
<form method="POST" action="">
<FieldGroup
id="formControlsEmail"
type="email"
placeholder="Enter your E-mail"
/>
<Button type="submit">Recover!</Button>
</form>
)};
var ModalLoginRecover = React.createClass({
render: function () {
return (
<div>
<ModalContainer
title="Password recovery"
body="<LoginRecoverForm someprophere='' />"
/>
</div>
)}});
module.exports = ModalLoginRecover;
Try:
<ModalContainer
title="Password recovery"
body={(() => {return <LoginRecoverForm someprophere='' />})()}
/>
Since JSX is just javascript, you can pass it in as props, just like any JS expression:
<ModalContainer
title="Password recovery"
body={<LoginRecoverForm someprophere=""/>}
/>
What you can do is pass the component as a child like this:
<ModalContainer
title="Password recovery"
body=""><LoginRecoverForm someprophere='' />
</ModalContainer>
React will put LoginRecoveryForm component at the place of {this.props.children} in ModalContainer
You can simply insert components between the tag. The inserted component will treated as default children prop. Like in this example code, <Container> is parent my component and I am trying to add <Routes>...</Routes> as its children.
const RouterProfile = () => {
return(
<Container>
<Routes>
<Route path="/" element={<ProfilePage />} />
</Routes>
</Container>
)
}
Now, You can see in this below code that child component is called using children prop that we are recieving from parent. Or you can log in console and see what are the argument you are recieving.
const Container = ({children}) => {
return(
<div className="d-flex flex-column" style={{width:"100vw", height:"100vh"}}>
<div class="flex-grow-1" style={{overflow : "scroll"}}>
{children}
</div>
</div>
)
}