React: how to propagate state to enclosing parent - reactjs

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 />}
/>

Related

React.js Controlled Input in child component

I am trying to have a controlled input set up in a child component (the Search component). I wanted to keep the input state in the main App component so that I can access it in my apiCall method. I am getting the following error:
Warning: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly.
However, I did add an onChange handler. I'm assuming the problem is that the onChange handler function is in the parent component and React doesn't like this. I did try moving the input to the main App component and worked fine (logged input to console).
Am I going about this wrong? And is there a way to set it up so that I can access the input from the Search component in the App component? I was hoping to keep most of my code/functions/state in the main App component.
Here is the App component
import React, { Component } from "react";
import './App.css';
import Header from './Components/Header'
import Search from './Components/Search'
import MainInfo from './Components/MainInfo'
import Details from './Components/Details'
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
weather: null,
main: '',
wind: '',
loading: null,
cityInput: 'Houston',
city: 'City Name',
date: new Date()
};
this.apiCall = this.apiCall.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({
cityInput: event.target.value
})
console.log(this.state.cityInput)
}
// Fetch data from OpenWeatherAPI
apiCall() {
this.setState({
loading: true
})
const currentWeather = fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${this.state.cityInput}&appid={apiKey}&units=imperial`
).then((res) => res.json());
const futureWeather = fetch(
`https://api.openweathermap.org/data/2.5/forecast?q=houston&appid={apiKey}&units=imperial`
).then((res) => res.json());
const allData = Promise.all([currentWeather, futureWeather]);
// attach then() handler to the allData Promise
allData.then((res) => {
this.setState({
weather: res[0].weather,
main: res[0].main,
wind: res[0].wind,
city: res[0].name
})
});
}
componentDidMount() {
this.apiCall();
}
render() {
return (
<div className="container-fluid bg-primary vh-100 vw-100 d-flex flex-column align-items-center justify-content-around p-3">
<Header />
<Search cityInput={this.state.cityInput} />
<MainInfo main={this.state.main} date={this.state.date} city={this.state.city} weather={this.state.weather} />
<Details main={this.state.main} wind={this.state.wind} />
</div>
);
}
}
export default App;
Here is Search component
import React, { Component } from "react";
class Search extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="row">
<div className="col-12">
<div className="d-flex">
<input className="form-control shadow-none mx-1" placeholder="Enter a city..." value={this.props.cityInput} onChange={this.handleChange}></input>
<button className="btn btn-light shadow-none mx-1" onClick={this.apiCall}>Test</button></div>
</div>
</div>
);
}
}
export default Search;
The Search component is indeed unaware of the implementation of the onChange function you have made in your App. If you really want to use a function from the parent (App) component in the child (Search), you'll need to add it as a property, as such:
<Search cityInput={this.state.cityInput} onChange={this.onChange} />
Then, you need to set it in the Child component's constructor:
constructor(props) {
super(props);
this.onChange = props.onChange;
}
I also suggest you'll have a look at React's functional approach with hooks https://reactjs.org/docs/hooks-intro.html, which makes all this a whole lot less fiddly, in my opinion. But it might take a bit to get used to.
u can pass functions like ur handler over the prop to childrens and update so from a child to the mother of the children, in the children u give the value the prop u supply from mother
<Select dataFeld="broker" id="yourid" value={this.state.brokerSel} ownonChange={(e) => this.setState({statename: e})

React - Proper way to render dynamic content?

I want to make a modal view has dynamic content by injecting a component to it.
class RootView extends Component {
state = {
modalBody: null
}
setModalBody = (body) => {
this.setState({modalBody: body})
}
render() {
return(<ContextProvider value={this.setModalBody}><Modal>{this.state.modalBody}</Modal></ContextProvider>)
}
}
Then inside any children view i use setState to change parent modalBody
The modalBody can be setted on each route, which means the modalBody can be input list, selection list or text only. So the modalBody must have its state for controlling these inputs.
By this way, it renders ok, but the dynamic content couldn't be updated after state changed. The parent's dynamic content couldn't receive the ChildView new state, i have to setModalBody again and again after it rerendered.
For example, if input in modalBody has changed, the parent couldn't be updated.
class ChildView extends Component {
state = {
inputValue: null
}
handleChange = (e) => {
this.setState({inputValue: e.target.value})
}
setModalBody(body) {
this.props.context.setModalBody(<input value={this.state.inputValue} onChange={this.handleChange} />)
}
render() {
return(<Modal>{this.state.modalBody}</Modal>)
}
}
Full code: https://codesandbox.io/s/lp5p20mx1m
Any proper way to render dynamic content to parent?
I'm not sure why you'd need to create a parent Modal component, when you can make the Modal a simple reusable child component.
See here for a detailed explanation on how to achieve a stateful parent that controls a child modal.
However, if you MUST have a parent Modal component, then you can create a render prop to pass down props to be used by its children.
Working example:
components/Modal.js (parent component -- this has a lot of smaller components that were separated for reusability and ease of understanding -- they're basically simple divs with some styles attached -- see notes below)
import React, { Fragment, Component } from "react";
import PropTypes from "prop-types";
import BackgroundOverlay from "../BackgroundOverlay"; // grey background
import ClickHandler from "../ClickHandler"; // handles clicks outside of the modal
import Container from "../Container"; // contains the modal and background
import Content from "../Content"; // renders the "children" placed inside of <Modal>...</Modal>
import ModalContainer from "../ModalContainer"; // places the modal in the center of the page
class Modal extends Component {
state = { isOpen: false };
handleOpenModal = () => {
this.setState({ isOpen: true });
};
handleCloseModal = () => {
this.setState({ isOpen: false });
};
// this is a ternary operator (shorthand for "if/else" -- if cond ? then : else)
// below can be read like: if isOpen is true, then render the modal,
// else render whatever the child component is returning (in this case,
// initially returning an "Open Modal" button)
render = () =>
this.state.isOpen ? (
<Container>
<BackgroundOverlay />
<ModalContainer>
<ClickHandler
isOpen={this.state.isOpen}
closeModal={this.handleCloseModal}
>
<Content>
{this.props.children({
isOpen: this.state.isOpen,
onCloseModal: this.handleCloseModal,
onOpenModal: this.handleOpenModal
})}
</Content>
</ClickHandler>
</ModalContainer>
</Container>
) : (
<Fragment>
{this.props.children({
isOpen: this.state.isOpen,
onCloseModal: this.handleCloseModal,
onOpenModal: this.handleOpenModal
})}
</Fragment>
);
}
// these proptype declarations are to ensure that passed down props are
// consistent and are defined as expected
Modal.propTypes = {
children: PropTypes.func.isRequired // children must be a function
};
export default Modal;
components/Example.js (child component accepting isOpen, onCloseModal and onOpenModal from the parent -- with this approach, as you'll notice, there's duplicate isOpen logic. While this approach gives you full control over the parent, it's repetitive. However, you can simplify your components by moving the "Open Modal" button logic to the parent, and passing in a prop like <Modal btnTitle="Open Modal"> to make it somewhat flexible, BUT you'll still lose some control of what's being initially rendered when isOpen is false.)
import React, { Fragment } from "react";
import Modal from "../Modal";
import "./styles.css";
const Example = () => (
<div className="example">
<h2>Parent Modal Example</h2>
<Modal>
{({ isOpen, onCloseModal, onOpenModal }) =>
isOpen ? (
<Fragment>
<h1 className="title">Hello!</h1>
<p className="subtitle">There are two ways to close this modal</p>
<ul>
<li>Click outside of this modal in the grey overlay area.</li>
<li>Click the close button below.</li>
</ul>
<button
className="uk-button uk-button-danger uk-button-small"
onClick={onCloseModal}
>
Close
</button>
</Fragment>
) : (
<button
className="uk-button uk-button-primary uk-button-small"
onClick={onOpenModal}
>
Open Modal
</button>
)
}
</Modal>
</div>
);
export default Example;

React constructor called only once for same component rendered twice

I expected this toggle to work but somehow the constructor of component <A/> is called only once. https://codesandbox.io/s/jvr720mz75
import React, { Component } from "react";
import ReactDOM from "react-dom";
class App extends Component {
state = { toggle: false };
render() {
const { toggle } = this.state;
return (
<div>
{toggle ? <A prop={"A"} /> : <A prop={"B"} />}
<button onClick={() => this.setState({ toggle: !toggle })}>
toggle
</button>
</div>
);
}
}
class A extends Component {
constructor(props) {
super(props);
console.log("INIT");
this.state = { content: props.prop };
}
render() {
const { content } = this.state;
return <div>{content}</div>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
I already found a workaround https://codesandbox.io/s/0qmnjow1jw.
<div style={{ display: toggle ? "none" : "block" }}>
<A prop={"A"} />
</div>
<div style={{ display: toggle ? "block" : "none" }}>
<A prop={"B"} />
</div>
I want to understand why the above code is not working
In react if you want to render same component multiple times and treat them as different then you need to provide them a unique key. Try the below code.
{toggle ? <A key="A" prop={"A"} /> : <A key="B" prop={"B"} />}
Since that ternary statement renders results in an <A> component in either case, when the <App>'s state updates and changes toggle, React sees that there is still an <A> in the same place as before, but with a different prop prop. When React re-renders it does so by making as few changes as possible. So since this is the same class of element in the same place, React doesn't need to create a new element when toggle changes, only update the props of that <A> element.
Essentially, the line
{toggle ? <A prop="A"/> : <A prop="B"/> }
is equivalent to
<A prop={ toggle ? "A" : "B" }/>
which perhaps more clearly does not need to create a new <A> component, only update the existing one.
The problem then becomes that you set the state.content of the <A> using props.prop in the constructor, so the state.content is never updated. The cleanest way to fix this would be to use props.prop in the render method of the <A> component instead of state.content. So your A class would look like this:
class A extends Component {
render() {
const { prop } = this.props;
return <div>{ prop }</div>;
}
}
If you must take the prop prop and use it in the <A> component's state, you can use componentDidUpdate. Here's an example:
class A extends Component {
constructor(props) {
super(props);
this.state = {content: props.prop};
}
componentDidUpdate(prevProps) {
if (prevProps.prop !== this.props.prop) {
this.setState({content: this.props.prop});
}
}
render() {
const { content } = this.state;
return <div>{ content }</div>
}
}
React will only call the constructor once. That's the expected outcome.
Looks like you're trying to update the state of the component A based on the props.
You could either use the prop directly or use the componentDidUpdate lifecycle method, as Henry suggested. Another way is using the static method getDerivedStateFromProps to update the state based on the prop passed.
static getDerivedStateFromProps(props, state) {
return ({
content: props.prop
});
}

Open a <Modal> by clicking on an element rendered in another component

I am using a group of Semantic UI <Item> components to list a bunch of products. I want to be able to edit the the details of a product when the <Item> is clicked, and I thought the best way to achieve this would be using a <Modal> component.
I want to have everything split into reusable components where possible.
(Note: I've purposefully left out some of the import statements to keep things easy to read.)
App.js
import { ProductList } from 'components';
const App = () => (
<Segment>
<Item.Group divided>
<ProductList/>
</Item.Group>
</Segment>
)
export default App;
components/ProductList.js
import { ProductListItem } from '../ProductListItem';
export default class ProductList extends Component {
constructor() {
super()
this.state = { contents: [] }
}
componentDidMount() {
var myRequest = new Request('http://localhost:3000/contents.json');
let contents = [];
fetch(myRequest)
.then(response => response.json())
.then(data => {
this.setState({ contents: data.contents });
});
this.setState({ contents: contents });
}
render() {
return (
this.state.contents.map(content => {
return (
<ProductListItem
prod_id={content.prod_id}
prod_description={content.prod_description}
category_description={content.category_description}
/>
);
})
)
}
}
components/ProductListItem.js
export default class ProductListItem extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Item key={`product-${this.props.prod_id}`} as='a'>
<Item.Content>
<Item.Header>{this.props.prod_description}</Item.Header>
<Item.Description>
<p>{this.props.prod_description}</p>
</Item.Description>
</Item.Content>
</Item>
)
}
}
All of this works nicely and the list of products displays as it should.
I've also created a basic modal component using one of the examples in the Modal docs:
components/ModalExampleControlled.js
export default class ModalExampleControlled extends Component {
state = { modalOpen: false }
handleOpen = () => this.setState({ modalOpen: true })
handleClose = () => this.setState({ modalOpen: false })
render() {
return (
<Modal
trigger={<Button onClick={this.handleOpen}>Show Modal</Button>}
open={this.state.modalOpen}
onClose={this.handleClose}
size='small'
>
<Header icon='browser' content='Cookies policy' />
<Modal.Content>
<h3>This website uses cookies etc ...</h3>
</Modal.Content>
<Modal.Actions>
<Button color='green' onClick={this.handleClose}>Got it</Button>
</Modal.Actions>
</Modal>
)
}
}
So this will create a button that reads Got it wherever <ModalExampleControlled /> is rendered, and the button causes the modal to appear - great.
How do I instead get the modal to appear when one of the <Item> elements in the product list is clicked (thus getting rid of the button)?
Thanks so much for your time.
Chris
Your problem is that currently the modal manages its own state internally. As long as this is the case and no other component has access to that state, you can not trigger the modal component from outside.
There are various ways to solve this. The best way depends on how your app is set up. It sounds like the best way to go is to replace the internal modal state with a prop that is passed to the modal from a higher order component that also passes open/close functions to the relevant children:
// Modal.js
export default class ModalExampleControlled extends Component {
render() {
return (
{ this.props.open ?
<Modal
open={this.props.open}
onClose={this.props.handleClose}
size='small'
>
<Header icon='browser' content='Cookies policy' />
<Modal.Content>
<h3>This website uses cookies etc ...</h3>
</Modal.Content>
<Modal.Actions>
<Button color='green' onClick={this.props.handleClose}>Got it</Button>
</Modal.Actions>
</Modal>
: null }
)
}
}
// App.js
import { ProductList } from 'components';
class App extends Component {
handleOpen = () => this.setState({ open: true })
handleClose = () => this.setState({ open: false })
render(){
return(
<Segment>
<Item.Group divided>
<ProductList/>
</Item.Group>
<Modal open={this.state.open} closeModal={() => this.handleClose()}}
</Segment>
)
}
}
export default App;
Keep in mind that this code is rather exemplary and not finished. The basic idea is: You need to give control to the highest parent component that is above all other components that need access to it. This way you can pass the open/close functions to the children where needed and control the modal state.
This can get unwieldy if there is a lot of this passing. If your app gets very complex it will become a matter of state management. When there is a lot going on a pattern like Redux might help to manage changing states (e.g. modals) from everywhere. In your case this might be finde, though.

Get another component input value React js

I have two components one is app component and other one is sidebar component i have been using input field in side bar and i want to get the value of that input field in my app component on click how this could be possible ?
You can try lifting the state up.
Create a new component that will contain your two components. In that new component, create a function that you will pass as props inside the sidebar component.
class ContainerComponent extends React.Component {
constructor() {
super();
this.state = {
valueThatNeedsToBeShared: ''
}
}
handleChange(e) {
this.setState({valueThatNeedsToBeShared: e.target.value})
}
render() {
return (
<div>
<AppComponent value={this.state.valueThatNeedsToBeShared} />
<SidebarComponent handleChange={this.handleClick.bind(this)} value={this.state.valueThatNeedsToBeShared} />
</div>
)
}
}
const SidebarComponent = ({handleChange, value}) => <aside>
<input value={value} onChange={handleChange} />
</aside>
const AppComponent = ({value}) => <div>
value from sidebar: {value}
</div>
In pure react it is possible by adding callback from one component and use it into parent component to change their state and then send input value from parent's state to props of your second component

Resources