Mutating child components based on parent component's state, without re-rendering - javascript

Goal
I'm trying to manage mouseenter and mouseleave events from a parent component, for a collection of child components that keep getting re-rendered.
I'm building a reusable component for the collection of listings, that does several things like pagination, and a few other things when a listing is hovered.
So to make this reusable, I have to maintain the state of the hovered listing from the parent CollectionComponent, and mutate each individual listing component based on the state of the parent.
Code
Here are the components I'm using (I stripped them all down to their most basic forms):
Listings Component:
import React from 'react'
import $ from 'jquery'
import CollectionComponent from './CollectionComponent'
import Listing from './Listing'
export default class Listings extends React.Component {
constructor(props) {
super(props)
this.state = {
listings: this.props.listings,
}
}
render() {
return (<section className="listing-results">
{this.state.listings.map( listing =>
<CollectionComponent results={this.state.listings} IndividualResult={Listing} perPage={this.props.perPage} options={options}/>
)}
</section>)
}
}
Collection Component:
import React from 'react'
export default class CollectionComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
results: this.props.results,
hoveredId: null
}
}
componentDidMount() {
this.$listings = $('.result-card')
$(this.$listings).mouseenter(this.toggleInfoIn).mouseleave(this.toggleInfoOut)
}
toggleInfoIn = e => {
var { target } = e
var infoId = $(target).data('id')
this.setState({hoveredId: infoId})
}
toggleInfoOut = e => {
this.setState({hoveredId: null})
}
render() {
const {results, IndividualResult, perPage, options} = this.props
return (
<div className="paginated-results">
{this.state.results.map( result =>
<IndividualResult key={result.id} result={result} options={options}/>
)}
</div>
)
}
}
Individual Listing Component:
import React from 'react'
export default class Listing extends React.Component {
constructor(props) {
super(props)
}
render() {
const { listing, hoveredId } = this.props
return (
<div className="result-card" data-id={listing.id}>
<div className={hoveredId === listing.id ? 'hovered' : ''}>
Listing Content
</div>
</div>
)
}
}
I know I can probably structure the CollectionComponent a little cleaner with a higher order component, but I'll leave that for refactoring later once I get it working properly with this basic setup.
Problem
My problem is that every time I hover and change the state of the parent component, it re-renders the child components, because their props are dependent on the parent's state. Once this happens, the reference to my jQuery collection of listings is no longer valid. So the mouse events are attached to old DOM elements that no longer exist.
How can I structure this differently, so that either:
the child elements' props update without re-rendering, or
the jQuery collection reference doesn't change
I'd really like to avoid getting a new the jQuery collection every time the component updates.

The behavior of hover should be confined to the individual listing component and not the Collections component.
As the Collections component maintains the state of currently hovered item, it is good idea to pass an handler as part of props and then render the list again based on the change in state set by the Collections component.
Use react based event handlers where ever necessary which makes it for a controlled component. It is not a good idea to put state in the DOM where react can take care of it for you.
Listings
import React from 'react'
export default class Listing extends React.Component {
constructor(props) {
super(props);
this.onMouseEnter = this.onMouseEnter.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
}
onMouseEnter() {
this.props.onMouseEnter({ listingId: this.props.listing.id });
}
onMouseLeave() {
this.props.onMouseLeave();
}
render() {
const { listing, hoveredId } = this.props
const listingId = listing.id;
const isHovered = this.props.hoveredId === listing.id;
return (
<div className="result-card" onMouseEnter={this.onMouseEnter} onMouseLeave={onMouseLeave}>
<div className={isHovered ? 'hovered' : ''}>
Listing Content
</div>
</div>
)
}
}
Collections
import React from 'react'
export default class CollectionComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
results: this.props.results,
hoveredId: null
}
}
onMouseEnter({ listingId }) {
this.setState({ listingId });
}
onMouseLeave() {
this.setState({ listingId: null });
}
render() {
const {results, IndividualResult, perPage, options} = this.props
return (
<div className="paginated-results">
{this.state.results.map( result =>
<IndividualResult key={result.id} hoveredId={this.state.hoveredId} result={result} options={options} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}/>
)}
</div>
)
}
}

Related

How to properly render Component after this.setState in React

I have this React component
import React, { Component } from "react";
export default class ResourceForField extends Component {
constructor() {
super();
this.state = {
resources: [],
};
}
componentDidMount() {
// get the resources from the Link props and save it into the state
this.setState({
resources: this.props.location.resources,
});
}
// This component gets the id of current learningField from the url
// and the rest(like the resources) from the Link component
render() {
return (
<div>
{this.state.resources.map(res => (
<div>test</div>
))}
</div>
);
}
}
It gets the resources from the Link component, and that works fine. If I check out the state of the Component from the dev tools, the state looks right. And I thought with my logic this should work. So firstly, the state is empty, the component gets rendered, since the state is empty it doesn't render any components. Then, setState gets called, it gets all the resources and saves them into the state, and then the component would re-render, and it should work, but it doesn't. I'm getting a TypeError: Cannot read property 'map' of undefined error. What is the correct way to do this and how do I fix this?
Try this code:
import React, { Component } from "react";
export default class ResourceForField extends Component {
constructor() {
super();
this.state = {
resources: this.props && this.props.location && this.props.location.resources?this.props.location.resources:[],
};
}
componentDidMount() {
}
// This component gets the id of current learningField from the url
// and the rest(like the resources) from the Link component
render() {
return (
<div>
{this.state.resources.map(res => (
<div>test</div>
))}
</div>
);
}
}
Or use directly props
import React, { Component } from "react";
export default class ResourceForField extends Component {
constructor() {
super();
}
// This component gets the id of current learningField from the url
// and the rest(like the resources) from the Link component
render() {
return (
<div>
{
this.props && this.props.location &&
this.props.location.resources
?this.props.location.resources.map(res => (
<div>test</div>
))
:null
}
</div>
);
}
}
Or use componentWillReceiveProps or getDerivedStateFromProps life cycle methods.
Check this.props.location.resources is array.
See more: https://hackernoon.com/replacing-componentwillreceiveprops-with-getderivedstatefromprops-c3956f7ce607
For first check is this.props.location.resources array, or if data type changes you can add checking, you can use lodash isArray or with js like this:
import React, { Component } from "react";
export default class ResourceForField extends Component {
constructor() {
super();
this.state = {
resources: [],
};
}
componentDidMount() {
// get the resources from the Link props and save it into the state
Array.isArray(this.props.location.resources) {
this.setState({
resources: this.props.location.resources,
});
}
}
// This component gets the id of current learningField from the url
// and the rest(like the resources) from the Link component
render() {
return (
<div>
{this.state.resources.map(res => (
<div>test</div>
))}
</div>
);
}
}
Or you can just use hooks like this:
import React, { useState, useEffect } from "react";
export default function ResourceForField({location}) {
const [ resources, setResources ] = useState([]);
useEffect(() => {
if (location && Array.isArray(location.resources)) {
setResources(location.resources)
}
}, [location]);
return (
<div>
{resources.map(res => (
<div>test</div>
))}
</div>
);
}
If the internal state of ResourceForField doesn't change and always equals to its prop, you shouldn't save the prop in the state. You can instead create a pure functional component.
Also note that there's nothing preventing you from initializing the state from the props in constructor method. i.e. you're not required to wait for the component to mount in order to access the props.
So, I'd write the following component for ResourceForField:
function ResourceForField({resources = []}) {
return (
<div>
{
resources.map(res => (<div>test</div>))
}
</div>
);
}

How to update state of a component through a button click in another component?

I have 2 components in my react application. On first time page load, the first component is supposed to make a query and display data(buttons) accordingly. The state of second component till now is empty. When the user clicks on any of the button, another request should be made to the sever and state of the second component should be changed and should be reflected on the web page.
These are my files..
Apps.js
import React, { Component } from 'react';
import './App.css';
import OrgList from "./orgList"
import OrgDetails from "./orgDetails"
class App extends Component {
render() {
return [
<OrgList/>,
<OrgDetails/>
];
}
}
export default App;
orgList.js
import React, { Component } from 'react'
import OrgDetails from "./orgDetails"
var posts =[]
class OrgList extends Component {
constructor(props){
super(props);
this.state={
mainpost: [],
devices:[],
}
}
componentDidMount(){
fetch(someURL)
.then(res => res.json())
.then(function (data){
for (let i = 0; i < 3; i++){
posts.push(data.orgs[i].name)
}
}).then(mainpost => this.setState({mainpost:posts}));
}
render() {
var token =new OrgDetails();
const postItems =this.state.mainpost.map((post) => (
console.log(post),
<button
data-tech={post}
key={post}
className="org-btn"
onClick={() => token.dispatchBtnAction(post)}
>
<h3>{post}</h3>
</button>
)
)
return (
<div>
<h3> Organisations!!!! </h3>
<h5>{postItems}</h5>
</div>
)
}
}
export default OrgList;
orgDetails.js
import React, { Component } from 'react'
var list =[]
const orgname = org =>
`someURL/${org}`
class OrgDetails extends Component {
state={
devices:[],
}
constructor(props){
super(props);
this.state={
devices: [],
}
this.dispatchBtnAction=this.dispatchBtnAction.bind(this)
}
dispatchBtnAction=(str) => {
list =[]
fetch(orgname(str))
.then(res => res.json())
.then(function (data){
for (let i = 0; i < 3; i++){
//console.log("123")
list.push(data.devices[i].location)
console.log(list)
}
}).then(devices => this.setState({
devices : list,
}));
}
render() {
const devices=this.state.devices.map((dev,i)=>(
<div key={dev}>
<li>{dev}</li>
</div>
))
return (
<div>
<p>{devices}</p>
</div>
)
}
}
export default OrgDetails;
But I am getting this warning...
Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to this.state directly or define a state = {}; class property with the desired state in the OrgDetails component.
Because of this, the state is not getting changed and the component is not rerendering.
How to eliminate this warning and if any better method is there please do suggest.
As these 2 component are not parent-child components, perhaps you should implement all the logic in the App and than pass state-handlers as props to each component.
Then your components will look something like this:
class App extends Component {
state = { clicks: 0 }
incrementState = () {
const prev = this.state.clicks;
this.setState({ clicks: prev + 1 })
}
render() {
return [
<DisplayComponent counter={this.state.clicks} />,
<ControlComponent onIncrement={this.incrementState} />
];
}
}
Component that displays state
class DisplayComponent extends Component{
render() {
return (<h3>this.props.counter</h3>);
}
}
Component that handles state
class ControlComponent extends Component {
render() {
return (<button onClick={this.props.onIncrement}>click me</button>)
}
}
Well the whole issue is this line var token =new OrgDetails(); This just creates the object. But doesn't mount it in the DOM. It also doesn't reference to the component <OrgDetails/> created in App. So when you try to use token.dispatchBtnAction(post), you are trying to setState on a component that is not mounted in the DOM, hence the error.
This is a really questionable way of making communication in between two components. You are better off using a Parent-Child relationship in between component. Also you can have a look at making Presentational Component and Container components differentiation to make the workflow easy. Have a read at the this link.

passing data between children in react-router

I have a problem. The problem is I need somehow to get product id from the ReadProducts component and pass it to ReadOne components, because I can't get an id in function and can't show the product. How can I do this?
The structure is below:
This component is parent for products, to get all and to get one
class Product extends Component {
render() {
return (
<Switch>
<Route path='/products' component={ReadProducts} />
<Route path='/products/:id' component={ReadOne} />
</Switch>
)
}
};
This one is getting all the products from api
class ReadProducts extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
this.getProduct = this.getProduct.bind(this);
}
render() {
const { products } = this.state;
return(
<ul>
{
products.map(product => (
<li key={product.id}>
<Link to={`/products/${product.id}`}><button onClick={???}>{product.name}</button></Link>
</li>
))
}
</ul>
)
}
}
This is for reading one product
class ReadOne extends Component {
constructor(props) {
super(props);
this.state = {
id: 0,
...
}
}
render() {
return(
<div className='pew'>
{this.state.name}, {this.state.id}...
</div>
)
}
}
The problem is I need somehow to get product id from the ReadProducts component and pass it to ReadOne components, because I can't get an id in function and can't show the product. How can I do this?
Add this in Product.js
We set the initial state of id.
We add setProductId in Product.js because this is where the state of id has to be in order for you to use it in ReadOne.js.
constructor(props) {
super(props);
this.state={
id:' ',
}
}
setProductId(id) => {
this.setState({ id:id })
}
Then pass id down as a prop to ReadOne component.
<Route path='/products/:id' render={()=> <ReadOne id={this.state.id}/>}>
This is the getter for the id, add this inside of ReadProducts.js
showProduct = (id) => {
getProduct(id);
}
<Link to={`/products/${product.id}`}><button onClick={this.showProduct(product.id)}>{product.name}</button></Link>
Now you can use id in any component within Product.js
Quick note from the react-router docs.
When you use component (instead of render or children, below) the
router uses React.createElement to create a new React element from the
given component. That means if you provide an inline function to the
component prop, you would create a new component every render. This
results in the existing component unmounting and the new component
mounting instead of just updating the existing component.

Saving incoming multiple data in list in React.js

I would really appreciate if you could help me with that one. Namely, I have a parent component and it gets new data from a child component. In child component array-data is mapped so incoming data into parent component is not one but multiple. I would like to save it inside the list tags to get as many list items as incoming values from child component.
Here's the code:
import React, { Component } from 'react';
import TextEnter from './TextEnter.jsx';
class Terminal extends Component {
constructor(props) {
super(props);
this.state = {
incomingData: '',
};
}
updateParent(val) {
this.setState({
incomingData: <li> {val} </li> // here I would like to save every incoming mapped data
});
}
render() {
return (
<div className="terminal">
<TextEnter
afterCommand={this.props.afterCommand}
triggerParent={(val) => this.updateParent(val)}
/>
<ul className="listContainer">
{this.state.incomingData}
</ul>
</div>
);
}
}
export default Terminal;
I don't really like to store React Elements (created with <li> in a jsx file) as state. I'd rather store the list, and leave how it is rendered to the render() function. I would change it to something like:
import React, { Component } from 'react';
import TextEnter from './TextEnter.jsx';
class Terminal extends Component {
constructor(props) {
super(props);
this.state = {
incomingData: [],
};
}
updateParent(val) {
this.setState({
incomingData: this.state.incomingData.concat(val)
});
}
render() {
const listItems = this.state.incomingData.map(item => {
return <li>{ item }</li>
});
return (
<div className="terminal">
<TextEnter
afterCommand={this.props.afterCommand}
triggerParent={this.updateParent}
/>
<ul className="listContainer">
{listItems}
</ul>
</div>
);
}
}
export default Terminal;
Notice that I also pass this.updateParent directly instead of wrapping it in an anonymous function which should give the same result. You'll get a key warning from react, but I don't know what unique identifier you have access to.

Is there a React lifecycle method to do something only when component receive props the first time?

I'm new to React so thank you for your patience in advance. Also using Redux.
I have a list of content pulled from the API, I display the text and a hidden text box and on a state change associated that alternates the visibility of the two. Essentially user can click on the text and edit the text, achieved by inverting the boolean and swapping the display. They can then save it and PUT to server etc.
Since my list length varies, I must initialize a number of state.isVisible[n]. equivalent to the number of content being displayed each time. This number must be counted, after the props come in. I am using Redux so the content is retrieved, stored, then given to props. It's done as the following:
constructor(props){
super(props);
this.state = {
isVisibleObj: {}
}
}
componentWillReceiveProps(){
const { isVisibleObj } = this.state
// set visibility of text box
let obj = {}
Object.keys(this.props.questions).forEach(key => obj[key] = false)
this.setState({isVisibleObj: obj})
}
My initial implementation was that in componentWillReceiveProps I do all the setState() to initialize the isVisible properties to a boolean.
The challenge I am having with this implementation is that, if a user open up multiple items for edit, and if she saves one of them, the PUT request on success would send back the edited content, now updating the store and props. This will trigger componentWillReceiveProps and reset all the visibilities, effectively closing all the other edits that are open.
Any suggestion on how to proceed?
I think you should make two components
List (NamesList.react)
import React, {PropTypes} from 'react';
import NameForm from './NameForm.react';
import Faker from 'Faker'
export default class NamesList extends React.Component {
constructor(){
super();
this.addItem = this.addItem.bind(this);
}
addItem(){
var randomName = Faker.name.findName();
this.props.addName(randomName);
}
render() {
let forms = this.props.names.map((name,i) => {
return <NameForm updateName={this.props.updateName} index={i} key={i} name={name} />
});
return (<div>
<div>{forms}</div>
<button onClick={this.addItem}>Add</button>
</div>);
}
}
NamesList.propTypes = {
names: PropTypes.arrayOf(PropTypes.string).isRequired
};
Form (NameForm.react)
import React, {PropTypes} from 'react';
export default class NameForm extends React.Component {
constructor(props) {
super(props);
this.updateName = this.updateName.bind(this);
this.state = {
showTextBox:false
}
}
updateName(){
this.setState({showTextBox:false});
this.props.updateName(this.props.index,this.refs.name.value);
}
render() {
if(this.state.showTextBox){
return (<div>
<input ref="name" defaultValue={this.props.name} />
<button onClick={this.updateName}>Save</button>
</div>);
}
return (<div onClick={() => {this.setState({showTextBox: !this.state.showTextBox})}}>
{this.props.name}
</div>);
}
}
NameForm.propTypes = {
name:PropTypes.string.isRequired
};
Invoke (App.js)
import React, { Component } from 'react';
import NamesList from './NamesList.react';
class App extends Component {
constructor(){
super();
this.addName = this.addName.bind(this);
this.updateName = this.updateName.bind(this);
this.state = {
names:['Praveen','Vartika']
}
}
addName(name){
let names = this.state.names.concat(name);
this.setState({
names: names
});
}
updateName(index,newName){
let names = this.state.names.map((name,i) => {
if(i==index){
return newName
}
return name;
});
this.setState({names:names});
}
render() {
return (
<NamesList names={this.state.names} updateName={this.updateName} addName={this.addName} />
);
}
}
export default App;
Now if your store changes after user saves something. React wont re-render Child component that didn't change

Categories

Resources