ReactDOM.render doesn't update component on props change - javascript

I have create a very small app to demonstrate my query.
Below shown code has the functionality where the component is dynamically added to DOM using ReactDOM.render and this component carries a prop called title, but when I update the title of the parent component ( in state ) the DynamicComponent doesn't update.
import React from 'react';
import ReactDOM from 'react-dom';
const DynamicComponent = (props) => {
return (
<div style={{ 'border': '2px dotted green' }} >Dynamic Component : {props.title}</div>
)
}
class App extends React.Component {
state = {
title: 'Iam Title'
}
addBlock = () => {
return ReactDOM.render(<DynamicComponent title={this.state.title} />, document.getElementById('dynamiccomponents'))
}
render() {
return (
<div>
<div>Value in state: <b>{this.state.title}</b></div>
<p><b><DynamicComponent /></b> Added Initially</p>
<DynamicComponent title={this.state.title} />
<br />
<p><b><DynamicComponent /></b> Added By ReactDOM.render will be shown below: </p>
<div id="dynamiccomponents"></div>
<button onClick={this.addBlock} >Click to Dynamic Component</button>
<button onClick={() => this.setState({ title: `Update Title` })} >Update Title</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
The first button is used to added the DynamicComponent, works fine as expected.
The Second button is used to update the title in state, now the title got changed but still DynamicComponent doesn't update.
am I missing anything, how do I solve this issue, any help would be appreciated
Thanks

You could re-render the component after state change using a LifeCycle method componentDidUpdate()
import React from "react";
import ReactDOM from "react-dom";
const DynamicComponent = props => {
return (
<div style={{ border: "2px dotted green" }}>
Dynamic Component : {props.title}
</div>
);
};
class App extends React.Component {
state = {
title: "Iam Title"
};
addBlock = () => {
return ReactDOM.render(
<DynamicComponent title={this.state.title} />,
document.getElementById("dynamiccomponents")
);
};
componentDidUpdate() {
return ReactDOM.render(
<DynamicComponent title={this.state.title} />,
document.getElementById("dynamiccomponents")
);
}
render() {
return (
<div>
<div>
Value in state: <b>{this.state.title}</b>
</div>
<p>
<b><DynamicComponent /></b> Added Initially
</p>
<DynamicComponent title={this.state.title} />
<br />
<p>
<b><DynamicComponent /></b> Added By ReactDOM.render will be
shown below:{" "}
</p>
<div id='dynamiccomponents'></div>
<button onClick={this.addBlock}>Click to Dynamic Component</button>
<button onClick={() => this.setState({ title: `Update Title` })}>
Update Title
</button>
</div>
);
}
}
export default App;

This is because when you call addBlock, you are only rendering <DynamicComponent title={this.state.title} /> once to the <div id="dynamiccomopnents"></div>.
When you update the state of title by clicking the button, it re-runs your App's render function, but this.addBlock does not get run again in your render function and therefore your title does not get updated. You can verify this by clicking the button that calls this.addBlock again. It will render your component again, with the updated title.
I'd suggest you introduce some state to conditionally render your component instead of using ReactDOM.render. That way, your component gets re-rendered everytime your render method is run. Here's an example:
import React from 'react';
import ReactDOM from 'react-dom';
const DynamicComponent = (props) => {
return (
<div style={{ 'border': '2px dotted green' }} >Dynamic Component : {props.title}</div>
)
}
class App extends React.Component {
state = {
title: 'Iam Title',
showBlock: false,
}
addBlock = () => {
// this method now sets `this.state.showBlock` to true
this.setState({ showBlock: true });
}
renderBlock = () => {
// return any component you want here, you can introduce some conditional
// logic or even return nested elements, for example:
return (
<div>
<p>Dynamic Component!</p>
<DynamicComponent title={this.state.title} />
</div>
);
}
render() {
return (
<div>
<div>Value in state: <b>{this.state.title}</b></div>
<p><b><DynamicComponent /></b> Added Initially</p>
<DynamicComponent title={this.state.title} />
<br />
<p><b><DynamicComponent /></b> Added By ReactDOM.render will be shown below: </p>
{/* This will run `this.renderBlock` only if `this.state.showBlock` is true */}
{this.state.showBlock && this.renderBlock()}
<button onClick={this.addBlock} >Click to Dynamic Component</button>
<button onClick={() => this.setState({ title: `Update Title` })} >Update Title</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));

ReactDOM.render renders element only once. It creates a different tree that is not connected to your first tree. That is, React doesn't keep track of all ReactDOM.renders you might have ever called and doesn't update them with data that was used to create them
If you need to render element somewhere in the DOM tree outside of your App component but you want it to be connected with your App component (so that it reacts to state changes), use ReactDOM.createPortal

Related

Updating state from child component to parent component

I'm struggling with an issue of App.js which has a Checkboxes component. When someone ticks a checkbox in that component, I want the state for that checkbox to be updated in the App.js file. The code for my Checkboxes component is below. I am using material ui if that makes any difference.
import Checkbox from '#material-ui/core/Checkbox';
import FormGroup from '#material-ui/core/FormGroup';
import FormControlLabel from '#material-ui/core/FormControlLabel';
export default function CheckboxLabels() {
return (
<FormGroup row>
<FormControlLabel
control={<Checkbox name="checkedD" />}
label="Remove Duplicates"
/>
</FormGroup>
);
}
The code in App.js that is relevant is:
<Grid>
<Checkboxes />
</Grid>
What should I be doing? How do I modify the above code?
If you want to keep the state in the App component, then you need to pass a function to your child component which will be used to send the state to the parent (App) component.
In your parent component, you would create the state so that each state of checkboxes can be stored and pass the function that will handle the updating of the state. I would store the state in Map to avoid having duplications of the same checkbox on each update. Also, pass an id to each of the Checkbox components so that you know which state refers to which checkbox when updating later on.
import React from "react";
import CheckboxLabels from "./CheckboxLabel";
class App extends React.Component {
state = {
checkboxes: new Map()
};
handleClick = (e, id) => {
const { checked } = e.target;
this.setState((prevState) => ({
checkboxes: prevState.checkboxes.set(id, checked)
}));
};
render() {
return (
<div className="App">
<CheckboxLabels id={1} handleClick={this.handleClick} />
<CheckboxLabels id={2} handleClick={this.handleClick} />
<CheckboxLabels id={3} handleClick={this.handleClick} />
<CheckboxLabels id={4} handleClick={this.handleClick} />
</div>
);
}
}
export default App;
In your child component, you would accept the function and the id and then pass that function with the event and id in onChange method of the checkbox.
import React from "react";
import Checkbox from "#material-ui/core/Checkbox";
import FormGroup from "#material-ui/core/FormGroup";
import FormControlLabel from "#material-ui/core/FormControlLabel";
export default function CheckboxLabels({ id, handleClick }) {
return (
<FormGroup row>
<FormControlLabel
control={<Checkbox name="checkedD" />}
label="Remove Duplicates"
onChange={(e) => handleClick(e, id)}
/>
</FormGroup>
);
}
This way, you can store the state of multiple checkboxes and you can easily track which state is which by an id.
You can use regular state mechanism in parent and just pass it down to CheckboxLabels component. So send setChecked and checked values down:
export default function CheckboxLabels({ checked, onChange }) {
return (
<FormGroup row>
<FormControlLabel
control={
<Checkbox
name="checkedD"
checked={checked}
onChange={(e) => {
if (onChange) {
onChange(e.target.checked);
}
}}
/>
}
label="Remove Duplicates"
/>
</FormGroup>
);
}
export default function App() {
const [checked, setChecked] = useState(false);
return (
<Grid>
<CheckboxLabels checked={checked} onChange={setChecked} />
</Grid>
);
}
I am not sure if your code is good naming-wise, but I added that checkbox component is name CheckboxLabels

setState is not defined with reactJS

I'm working with reactJS and I am trying to use setState so that i can use that state to determine which way the graph needs to rotate but I get an error saying 'setState' is not defined no-undef. Do I need to have a constructor to initialize the state?
Code:
class App extends React.Component {
render() {
const {rotate, setRotate} = setState(false)
return (
<div className="custom-container">
<Tree
data={data}
height={600}
width={1000}
svgProps={{transform: 'rotate(${rotate ? "90" : "0")'}}
/>
<button onClick={() => setRotate(!rotate)}>Rotate</button>
</div>
);
}
}
If you are trying to use, "useState" instead of setState,
'useState' is used for having a state in functional components, you will have to convert class component to function component as below and then use 'useState'
export default function App() {
const [rotate, setRotate] = React.useState(false)
return (
<div className="custom-container">
<Tree
data={data}
height={600}
width={1000}
svgProps={{transform: 'rotate(${rotate ? "90" : "0")'}}
/>
<button onClick={() => setRotate(!rotate)}>Rotate</button>
</div>
);
}
You could also try this:
class App extends React.Component {
state = {
rotate: false
};
render() {
return (
<div className="custom-container">
<Tree
data={data}
height={600}
width={1000}
svgProps={{transform: 'rotate(${rotate ? "90" : "0")'}}
/>
<button onClick={() => this.setState({ rotate: !this.state.rotate})}>Rotate</button>
</div>
);
}
}
I think you are trying to use useState();
Make sure you import useState at the top first and also use function component instead of the class component :
import React, {useState} from 'react';
function App() {
const [rotate, setRotate] = useState(false)
return (
<div className="custom-container">
<Tree
data={data}
height={600}
width={1000}
svgProps={{transform: 'rotate(${rotate ? "90" : "0")'}}
/>
<button onClick={() => setRotate(!rotate)}>Rotate</button>
</div>
);

How to add a button using Higher Order Component in reactjs

How can I add a button to a component using higher order component? I tried this but its not adding the button inside the component. Its adding it before the original component.
const withButton = WrappedComponent => {
return class extends Component {
render() {
return (
<Fragment>
<button>BUTTON ADDED USING HOC</button>
<WrappedComponent {...this.props} />
</Fragment>
)
}
}
}
export default withButton
When I call the HOF like this
const ComponentWithButton = withButton(WrappedComponent)
ComponentWithButton has button added but its adding before WrappedComponent whereas I want to add button inside as a child of the WrappedComponent.
Lets say that WrappedComponent is rendering something like
<div className="baseClass">{other divs and layout}</div>
const ComponentWithButton = withButton(WrappedComponent)
ComponentWithButton should render the following
<div className="baseClass">
<button>BUTTON ADDED USING HOC</button>
{other divs and layout}
</div>
Try using props.children, also refer to React.Children API
function ComponentWithButton({ children }) {
return (
<>
<button>BUTTON ADDED USING HOC</button>
{children}
</>
);
}
And then render:
<ComponentWithButton>
<WrappedComponent />
</ComponentWithButton>
With classes:
class ComponentWithButton extends Component {
render() {
const { children } = this.props;
return (
<>
<button>BUTTON ADDED USING HOC</button>
{children}
</>
);
}
}
export default ComponentWithButton;
If you want to dynamically place the button somewhere inside the WrappedComponent, you can try something like this.
const withButton = WrappedComponent => {
return class extends Component {
render() {
return (
<Fragment>
<WrappedComponent {...this.props}>
<button>BUTTON ADDED USING HOC</button>
</WrappedCompnent>
</Fragment>
)
}
}
}
export default withButton
Now in your wrapped component, you can place the button any where you want as the button would be accessible as a property children to WrappedComponent.
const WrappedComponent = ({ children, ...otherProps }) => (
<div className="baseClass">
{children}
{renderOtherDivsAndProps(otherProps)}
</div>
);
Hope this helps you
I tried this and I am getting what I am looking for.
const withButton = WrappedComponent => {
return class extends Component {
render() {
return (
<WrappedComponent {...this.props}>
<button>BUTTON ADDED USING HOC</button>
{this.props.children}
</Wrappedcomponent>
)
}
}
}
export default withButton

How do I clear the the array of a state?

So this is my code :
import React from "react";
import Navigation from './Navigation';
import Foot from './Foot';
import MovieCard from './MovieCard';
class Favorites extends React.Component {
render() {
const { onSearch, favorites, favoriteCallback, totalFavorites, searchKeyUpdate } = this.props;
return (
<div>
<Navigation
onSearch={onSearch}
totalFavorites={totalFavorites}
searchKeyUpdate={searchKeyUpdate} />
<div className="container">
<button onClick={()=> this.clearFavorites(favorites)}> Clear all movies </button>
{(favorites.length < 1) ?
<h1 style={{ fontSize: '13px', textAlign: 'center' }}>Please mark some of the movies as favorites!</h1>
:
<ul
className="movies">
{favorites
.map(movie => (
<MovieCard
movie={movie}
key={movie.imdbID}
toggleFavorite={favoriteCallback}
favorites={favorites}
/>
))}
</ul>
}
<Foot />
</div>
</div>
);
}
}
const clearFavorites = (favorites) => {
this.setState({ favorites: [] });
}
The thing I need for the button to do is that when i click it that it clears the whole state of favorites. The clearFavorites function is used to clear everything but when I try this I get an error:
Why doesn't this clear the state of favorites?
You have two problems:
clearFavorites function is not in your class. So you should put it inside.
You are trying to clear the data inside the favorites array, which is not part of your state, using the function clearFavorites. So, first of all, you should add favorites array to your state and then you can manipulate the information. I suggest you to use the function getDerivedStateFromProps.
As others mentioned, first moving clearFavorites function into Favorites class.
Second, your favorites list is not part of state object, but instead you pull it out from this.props.favorites, so instead of using this.setState, we should just change the props value.
Third, since you're emptying the array, the parameter in your clearFavorites probably not needed? Please refer to below:
First we define a constructor to get the value from props and pass it to state in the constructor as below:
constructor(props) {
super(props);
this.state = {favorites: this.props.favorites}
}
clearFavorites = () => {
this.setState({favorites: []});
};
Then at last in your render method change to following:
const { onSearch, favoriteCallback, totalFavorites, searchKeyUpdate } = this.props;
const favorites = this.state.favorites;// Or in your ul tag, instead of using favorites, change it to this.state.favorites
You can try to move the clearFavorites into your component
import React from "react";
import Navigation from "./Navigation";
import Foot from "./Foot";
import MovieCard from "./MovieCard";
class Favorites extends React.Component {
render() {
const {
onSearch,
favorites,
favoriteCallback,
totalFavorites,
searchKeyUpdate
} = this.props;
return (
<div>
<Navigation
onSearch={onSearch}
totalFavorites={totalFavorites}
searchKeyUpdate={searchKeyUpdate}
/>
<div className="container">
<button onClick={() => this.clearFavorites(favorites)}>
{" "}
Clear all movies{" "}
</button>
{favorites.length < 1 ? (
<h1 style={{ fontSize: "13px", textAlign: "center" }}>
Please mark some of the movies as favorites!
</h1>
) : (
<ul className="movies">
{favorites.map(movie => (
<MovieCard
movie={movie}
key={movie.imdbID}
toggleFavorite={favoriteCallback}
favorites={favorites}
/>
))}
</ul>
)}
<Foot />
</div>
</div>
);
}
clearFavorites = favorites => {
this.setState({ favorites: [] });
};
}
<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>

React DnD drags whole list of cards instead of single card

I am trying to use react DnD in my react Project. In my render method I define a variable named Populate like show below, which returns a list of cards like this
render() {
var isDragging = this.props.isDragging;
var connectDragSource = this.props.connectDragSource;
var Populate = this.props.mediaFiles.map((value) => {
return(
<div>
<MuiThemeProvider>
<Card style= {{marginBottom: 2, opacity: isDragging ? 0 : 1}} id={value.id} key={value.id}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
//onTouchTap={() => {this.handleClick(value.id)}}
zDepth={this.state.shadow}>
<CardHeader
title={value.Episode_Name}
//subtitle={value.description}
actAsExpander={false}
showExpandableButton={false}
/>
</Card>
</MuiThemeProvider>
</div>
)
});
And my return of render method looks like this
return connectDragSource (
<div>
<MuiThemeProvider>
<div className="mediaFilesComponent2">
{Populate}
</div>
</MuiThemeProvider>
</div>
)
Problem is when I try using drag, then the whole list of cards gets selected for drag. I want all the cards having individual drag functionality.
If you want each card to have drag functionality than you'll have to wrap each card in a DragSource, and not the entire list. I would split out the Card into it's own component, wrapped in a DragSource, like this:
import React, { Component, PropTypes } from 'react';
import { ItemTypes } from './Constants';
import { DragSource } from 'react-dnd';
const CardSource = {
beginDrag: function (props) {
return {};
}
};
function collect(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}
}
class CardDragContainer extends React.Component {
render() {
return this.props.connectDragSource(
<div>
<Card style= {{marginBottom: 2, opacity: this.props.isDragging ? 0 : 1}} id={value.id} key={value.id}
onMouseOver={this.props.onMouseOver}
onMouseOut={this.props.onMouseOut}
zDepth={this.props.shadow}>
<CardHeader
title={props.title}
actAsExpander={false}
showExpandableButton={false}
/>
</Card>
</div>
)
}
}
export default DragSource(ItemTypes.<Your Item Type>, CardSource, collect)(CardDragContainer);
Then you would use this DragContainer in render of the higher level component like this:
render() {
var Populate = this.props.mediaFiles.map((value) => {
return(
<div>
<MuiThemeProvider>
<CardDragContainer
value={value}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
shadow={this.state.shadow}
/>
</MuiThemeProvider>
</div>
)
});
return (
<div>
<MuiThemeProvider>
<div className="mediaFilesComponent2">
{Populate}
</div>
</MuiThemeProvider>
</div>
);
}
That should give you a list of Cards, each of which will be individually draggable.

Categories

Resources