Child component's props not updating when state changes - javascript

I'm using drag and drop to instantiate react classes, but for some reason the state from the parent component is not being passed to the child. The child isn't even being rerendered, tried shouldComponentUpdate and componentWillReceiveProps.
Parents relevant code:
dragEnd(e) {
e.preventDefault();
let me = this;
let el = (
<Pie key={ Date.now() * Math.random() } yAxisData={ me.state.yAxisData } legendData={ me.state.legendData } />
)
this.setState({
cells: this.state.cells.concat(el),
});
}
So, on drop, is created, and then render looks like:
render() {
<div className = { "insights-data" } onDrop={ this.dragEnd } onDragOver={ this.preventDefault }>
{ this.state.cells }
</div>
}
All this works fine, but now when I change the data passed to this.state.yAxisData and/or this.state.legendData, it's not calling render on the child component.
Here's the child components render:
render() {
return (
<div className="insights-cell">
<ReactECharts
option={ this.create() }
style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: 0, height: "100%" }}
theme="chalk"
notMerge={ true }
/>
</div>
)
}
Any ideas? I thought maybe there was a binding issue, but that doesn't seem to be it, as I'm using me = this. It's not even re-rendering the child component.

You are already creating the element in dragEnd function by passing props to it and storing them to an array. Therefore the array this.state.cells contain the array of already declared elements. Therefore it cannot update on state change. You should render a new array of element on every render.
Just push some necessary detail of dragged element in this.state.cells and then iterate through this array on every render.
dragEnd(e) {
e.preventDefault();
let el = draggedElementType
this.setState({
cells: this.state.cells.concat(el),
});
}
And in render, iterate through this array and return the desired element.
render() {
<div className = { "insights-data" } onDrop={ this.dragEnd } onDragOver={ this.preventDefault }>
{ this.state.cells.map((cell, index) => {
if (cell === "pie") {
return (<Pie key={index} yAxisData={ me.state.yAxisData } legendData={ me.state.legendData } />);
}
else if (){...
)}
</div>
}

Related

Map of refs, current is always null

I'm creating a host of a bunch of pages, and those pages are created dynamically. Each page has a function that I'd like to call at a specific time, but when trying to access a ref for the page, the current is always null.
export default class QuizViewPager extends React.Component<QuizViewPagerProps, QuizViewPagerState> {
quizDeck: Deck | undefined;
quizRefMap: Map<number, React.RefObject<Quiz>>;
quizzes: JSX.Element[] = [];
viewPager: React.RefObject<ViewPager>;
constructor(props: QuizViewPagerProps) {
super(props);
this.quizRefMap = new Map<number, React.RefObject<Quiz>>();
this.viewPager = React.createRef<ViewPager>();
this.state = {
currentPage: 0,
}
for (let i = 0; i < this.quizDeck!.litems.length; i++) {
this.addQuiz(i);
}
}
setQuizPage = (page: number) => {
this.viewPager.current?.setPage(page);
this.setState({ currentPage: page })
this.quizRefMap.get(page)?.current?.focusInput();
}
addQuiz(page: number) {
const entry = this.quizDeck!.litems[page];
var ref: React.RefObject<Quiz> = React.createRef<Quiz>();
this.quizRefMap.set(page, ref);
this.quizzes.push(
<Quiz
key={page}
litem={entry}
index={page}
ref={ref}
pagerFocusIndex={this.state.currentPage}
pagerLength={this.quizDeck?.litems.length!}
setQuizPage={this.setQuizPage}
navigation={this.props.navigation}
quizType={this.props.route.params.quizType}
quizManager={this.props.route.params.quizType === EQuizType.Lesson ? GlobalVars.lessonQuizManager : GlobalVars.reviewQuizManager}
/>
)
}
render() {
return (
<View style={{ flex: 1 }}>
<ViewPager
style={styles.viewPager}
initialPage={0}
ref={this.viewPager}
scrollEnabled={false}
>
{this.quizzes}
</ViewPager>
</View >
);
}
};
You can see in addQuiz() I am creating a ref, pushing it into my map, and passing that ref into the Quiz component. However, when attempting to access any of the refs in setQuizPage(), the Map is full of refs with null current properties.
To sum it up, the ViewPager library being used isn't actually rendering the children you are passing it.
If we look at the source of ViewPager (react-native-viewpager), we will see children={childrenWithOverriddenStyle(this.props.children)} (line 182). If we dig into the childrenWithOverriddenStyle method, we will see that it is actually "cloning" the children being passed in via React.createElement.
It is relatively easy to test whether or not the ref passed to these components will be preserved by creating a little demo:
const logRef = (element) => {
console.log("logRef", element);
};
const ChildrenCreator = (props) => {
return (
<div>
{props.children}
{React.Children.map(props.children, (child) => {
console.log("creating new", child);
let newProps = {
...child.props,
created: "true"
};
return React.createElement(child.type, newProps);
})}
</div>
);
};
const App = () => {
return (
<div className="App">
<ChildrenCreator>
<h1 ref={logRef}>Hello World</h1>
<p>It's a nice day!</p>
</ChildrenCreator>
</div>
);
}
ReactDOM.render(<App />, '#app');
(codesandbox)
If we look at the console when running this, we will be able to see that the output from logRef only appears for the first, uncopied h1 tag, and not the 2nd one that was copied.
While this doesn't fix the problem, this at least answers the question of why the refs are null in your Map. It actually may be worth creating an issue for the library in order to swap it to React.cloneElement, since cloneElement will preserve the ref.

Render unique divs for each hovered element

minimum reproducible example: https://codesandbox.io/s/react-hover-example-tu1eu?file=/index.js
I currently have a new element being rendered when either of 2 other elements are hovered over. But i would like to render different things based upon which element is hovered.
In the example below and in the codepen, there are 2 hoverable divs that are rendered; when they are hovered over, it changes the state and another div is rendered. I would like for the HoverMe2 div to render text "hello2". Currently, whether i hover hoverme1 or 2, they both just render the text "hello".
import React, { Component } from "react";
import { render } from "react-dom";
class HoverExample extends Component {
constructor(props) {
super(props);
this.handleMouseHover = this.handleMouseHover.bind(this);
this.state = {
isHovering: false
};
}
handleMouseHover() {
this.setState(this.toggleHoverState);
}
toggleHoverState(state) {
return {
isHovering: !state.isHovering
};
}
render() {
return (
<div>
<div
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me
</div>
<div
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me2
</div>
{this.state.isHovering && <div>hello</div>}
</div>
);
}
}
render(<HoverExample />, document.getElementById("root"));
You need to keep the state of item which you have hovered that's for sure
const { Component, useState, useEffect } = React;
class HoverExample extends Component {
constructor(props) {
super(props);
this.handleMouseHover = this.handleMouseHover.bind(this);
this.state = {
isHovering: false,
values: ['hello', 'hello2'],
value: 'hello'
};
}
handleMouseHover({target: {dataset: {id}}}) {
this.setState(state => {
return {
...state,
isHovering: !state.isHovering,
value: state.values[id]
};
});
}
render() {
return (
<div>
<div
data-id="0"
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me
</div>
<div
data-id="1"
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me2
</div>
{this.state.isHovering && <div>{this.state.value}</div>}
</div>
);
}
}
ReactDOM.render(
<HoverExample />,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
You can pass the context text as shown in example. This is working code:
import React, { Component } from "react";
import { render } from "react-dom";
// Drive this using some configuration. You can set based on your requirement.
export const HOVER_Hello1 = "Hello1";
export const HOVER_Hello2 = "Hello2";
class HoverExample extends Component {
constructor(props) {
super(props);
this.handleMouseHover = this.handleMouseHover.bind(this);
this.state = {
isHovering: false,
contextText: ""
};
}
handleMouseHover = (e, currentText) => {
this.setState({
isHovering: !this.state.isHovering,
contextText: currentText
});
}
toggleHoverState(state) {
//
}
render() {
return (
<div>
<div
onMouseEnter={e => this.handleMouseHover(e, HOVER_Hello1)}
onMouseLeave={e => this.handleMouseHover(e, HOVER_Hello1)}
>
Hover Me
</div>
<div
onMouseEnter={e => this.handleMouseHover(e, HOVER_Hello2)}
onMouseLeave={e => this.handleMouseHover(e, HOVER_Hello2)}
>
Hover Me2
</div>
{this.state.isHovering && <div>{this.state.contextText}</div>}
</div>
);
}
}
export default HoverExample;
If the whole point is about linking dynamically messages to JSX-element you're hovering, you may store that binding (e.g. within an object).
Upon rendering, you simply pass some anchor (e.g. id property of corresponding object) within a custom attribute (data-*), so that later on you may retrieve that, look up for the matching object, put linked message into state and render the message.
Following is a quick demo:
const { Component } = React,
{ render } = ReactDOM,
rootNode = document.getElementById('root')
const data = [
{id:0, text: 'Hover me', message: 'Thanks for hovering'},
{id:1, text: 'Hover me too', message: 'Great job'}
]
class HoverableDivs extends Component {
state = {
messageToShow: null
}
enterHandler = ({target:{dataset:{id:recordId}}}) => {
const {message} = this.props.data.find(({id}) => id == recordId)
this.setState({messageToShow: message})
}
leaveHandler = () => this.setState({messageToShow: null})
render(){
return (
<div>
{
this.props.data.map(({text,id}) => (
<div
key={id}
data-id={id}
onMouseEnter={this.enterHandler}
onMouseLeave={this.leaveHandler}
>
{text}
</div>
))
}
{
this.state.messageToShow && <div>{this.state.messageToShow}</div>
}
</div>
)
}
}
render (
<HoverableDivs {...{data}} />,
rootNode
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
As #CevaComic pointed out, you can do this with CSS. But if you want to use React, for example, because your actual problem is more complex, here is the answer.
You will need a way to tell apart the two elements. It could be done with some neat tricks, like setting an unique id to each element, passing a custom argument, or something else.
But I would advise against "cool tricks" as it's more difficult to understand what is going on, and the code is more prone to errors. I think the best way it to use a dumb approach of unique functions for unique elements.
Each onMouseEnter and onMouseLeave has to be an unique function (e.g. handleMouseHover1 and handleMouseHover2), and each of those functions need to control unique state (for example, isHovering1 and isHovering2). Then you have to render the element you want based on the state. Of course, for a real-world code, you will probably want to use more descriptive names to make the code more comprehensible. The full code would look something like this.
class HoverExample extends Component {
state = {
isHovering1: false,
isHovering2: false
};
handleMouseHover1 = () => {
this.setState(({ isHovering1 }) => ({ isHovering1: !isHovering1 }));
};
handleMouseHover2 = () => {
this.setState(({ isHovering2 }) => ({ isHovering2: !isHovering2 }));
};
render() {
const { isHovering1, isHovering2 } = this.state;
return (
<div>
<div
onMouseEnter={this.handleMouseHover1}
onMouseLeave={this.handleMouseHover1}
>
Hover Me1
</div>
<div
onMouseEnter={this.handleMouseHover2}
onMouseLeave={this.handleMouseHover2}
>
Hover Me2
</div>
{isHovering1 && <div>hello1</div>}
{isHovering2 && <div>hello2</div>}
</div>
);
}
}
Also, updated example: https://codesandbox.io/s/react-hover-example-rc3h0
Note: I have also edited the code to add some syntax sugar which exists with newer ECMAScript versions. Instead of binding the function, you can use the arrow function format, e.g. fn = () => { ... }. The arrow function means the this context is automatically bound to the function, so you don't have to do it manually. Also, you don't have to initialize this.state inside the constructor, you can define it as a class instance property. With those two things together, you do not need the constructor at all, and it makes the code a bit cleaner.

React not reordering array of components on render

I am trying to render a list of components in order with react, the component is updating this array of elements but is not re-ordering them.
Pseudo code;
class Form extends Component {
//
// .... other initialization code and logic
//
updatePositions() {
//
// re-order this.state.page.page_contents
//
this.setState({ page: this.state.page });
}
renderContents() {
return this.state.page.page_content.map((c, i) => {
return (<ContentItem
key={ i }
content={ c }
/>);
});
}
render() {
return (
<div className="row">
<div className="medium-12 columns">
{ this.renderContents() }
</div>
</div>
);
}
}
If i log out the results of page.page_content the items are being reordered in the array, however the form render is not re-rendering the contents in its new order
You shouldn't be using array indices as keys if your array order is subject to change. Keys should be permanent, because React uses the keys to identify the components, and if a component receives a key that previously belonged to a different component, React thinks it is the same component as before.
Create unique keys for your elements that are permanent to those elements.
you could try to force update
renderContents() {
this.forceUpdate();
return this.state.page.page_content.map((c, i) => {
return (<ContentItem
key={ i }
content={ c }
/>);
});
}
Don't mutate this.state, directly. Bring it into a new variable and then add it back into state.
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
from: https://reactjs.org/docs/react-component.html
Instead, you should try:
updatePositions() {
const page_contents = [...this.state.page.page_contents]
// re order page_contents
this.setState({ page: { page_contents });
}
renderContents() {
return this.state.page.page_content.map((c, i) => {
return (<ContentItem
key={ i }
content={ c }
/>);
});
}
it's your code here - key={i} i is not changing so it will not re-render the component - if you want to re-render the component - please make sure that - key should change.
renderContents() {
return this.state.page.page_content.map(c => {
return (<ContentItem
key={ c }
content={ c }
/>);
});
}
c is content - if it's change then Component will re-render
this.setState({ page: this.state.page }) it's wrong - ur trying to set the same value in same variable again .
class Form extends Component {
//
// .... other initialization code and logic
//
updatePositions() {
//
// re-order this.state.page.page_contents
//
this.setState({ page: newValueFromAPI.page });
}
render() {
const { page: { page_content } } = this.state
return (
<div className="row">
<div className="medium-12 columns">
{ page_content.length > 0 && (
page_content.map(c => <ContentItem key={c} content={c}/>)
)}
</div>
</div>
);
}
}

React - Array of children components not rerendering with props expression set in a callback

I understand that the wording of the question is slightly nebulous, so I will expand. This is a personal project of mine that I have taken up to learn some React basics and familiarize myself with socket.io
I have a CollapsibleList component, and a NestedList component, which renders an array of the CollapsibleList components.
NestedList has an event handler that gets set in componentWillMount of the component. The event is when a menu arrives via socket.io from my server. When the menu arrives, a new CollapsibleList is added to the array, and state is changed to trigger a rerender. The events are triggered by an initial socket.io event that is emitted via componentDidMount (get-menus).
CollapsibleList is collapsed/uncollapsed by its onclick which uses a toggleVisiblity method passed via props from the NestedList, whose state determines whether its child CollapsibleList components are open or not.
Problem: CollapsibleList props (which come from state of the NestedList) don't change on changing state of said NestedList. I have examined the properties in the debugger and I have been stuck for days. In other words, the CollapsibleList element appears on the browser window, but clicking it only changes the state of the NestedList, and the props of the CollapsibleList doesn't change, and thus it doesn't appear/disappear. I think it has something to do with creating the CollapsibleLists in the socket.io callback, bound with 'this', since the 'collapsed' prop of the CollapsibleList depends on this.state[restaurantId].collapsed. Source is below, if it is unclear I can add more explanation.
class CollapsibleList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<List>
<ListItem
button
onClick={() => {
this.props.collapseEventHandler(this.props.restaurantId);
}}
>
<ListItemText primary="Collapse me!" />
</ListItem>
<ListItem>
<Collapse in={!this.props.collapsed} timeout="auto" unmountOnExit>
<ListItemText primary="Hello World!" />
</Collapse>
</ListItem>
</List>
);
}
}
class NestedList extends React.Component {
constructor(props) {
super(props);
//let menuData = props.menuData.map()
this.toggleVisiblity = this.toggleVisiblity.bind(this);
this.arrayOfMenus = [];
}
componentWillMount() {
socket.on(
"menu-arrived",
function(menuJson) {
if (menuJson.response.menu.menus.items) {
let restaurantId = menuJson.restaurant_id;
//let menuId = menuJson.response.menu.menus.items[0].menuId;
this.arrayOfMenus.push(
<CollapsibleList
collapsed={this.state[restaurantId].collapsed}
collapseEventHandler={this.toggleVisiblity}
restaurantId={restaurantId}
key={restaurantId}
/>
);
this.setState(function(prevState, props) {
return {
[restaurantId]: {
collapsed: prevState[restaurantId].collapsed,
updated: true
}
};
});
}
}.bind(this)
);
}
componentDidMount() {
getNearbyRestaurantRawData().then(
function(rawData) {
let restaurantIds = parseOutVenueIds(rawData);
let menusOpen = {};
for (let i = 0; i < restaurantIds.length; i++) {
menusOpen[restaurantIds[i]] = {
collapsed: true
};
}
this.setState(menusOpen, () => {
socket.emit("get-menus", {
ids: restaurantIds
});
});
}.bind(this)
);
}
toggleVisiblity(restaurantId) {
this.setState(function(prevState, props) {
let newState = Object.assign({}, prevState);
newState[restaurantId].collapsed = !prevState[restaurantId].collapsed;
return newState;
});
}
render() {
return (
<List>
<React.Fragment>
<CssBaseline>{this.arrayOfMenus}</CssBaseline>
</React.Fragment>
</List>
);
}
}
You are pushing CollapsibleList React elements to an array on the instance, which means that new React elements will not be created and returned from the render method when state or props change.
You should always derive your UI from state and props in the render method instead.
Example
class NestedList extends React.Component {
state = { restaurantIds: [] };
componentWillMount() {
socket.on("menu-arrived", menuJson => {
if (menuJson.response.menu.menus.items) {
let restaurantId = menuJson.restaurant_id;
this.setState(prevState => {
return {
restaurantIds: [...prevState.restaurantIds, restaurantId],
[restaurantId]: {
collapsed: prevState[restaurantId].collapsed,
updated: true
}
};
});
}
});
}
// ...
render() {
return (
<List>
<React.Fragment>
<CssBaseline>
{this.state.restaurantIds.map(restaurantId => (
<CollapsibleList
collapsed={this.state[restaurantId].collapsed}
collapseEventHandler={this.toggleVisiblity}
restaurantId={restaurantId}
key={restaurantId}
/>
))}
</CssBaseline>
</React.Fragment>
</List>
);
}
}

How to Access styles from React?

I am trying to access the width and height styles of a div in React but I have been running into one problem. This is what I got so far:
componentDidMount() {
console.log(this.refs.container.style);
}
render() {
return (
<div ref={"container"} className={"container"}></div> //set reff
);
}
This works but the output that I get is a CSSStyleDeclaration object and in the all property I can all the CSS selectors for that object but they none of them are set. They are all set to an empty string.
This is the output of the CSSStyleDecleration is: http://pastebin.com/wXRPxz5p
Any help on getting to see the actual styles (event inherrited ones) would be greatly appreciated!
Thanks!
For React v <= 15
console.log( ReactDOM.findDOMNode(this.refs.container).style); //React v > 0.14
console.log( React.findDOMNode(this.refs.container).style);//React v <= 0.13.3
EDIT:
For getting the specific style value
console.log(window.getComputedStyle(ReactDOM.findDOMNode(this.refs.container)).getPropertyValue("border-radius"));// border-radius can be replaced with any other style attributes;
For React v>= 16
assign ref using callback style or by using createRef().
assignRef = element => {
this.container = element;
}
getStyle = () => {
const styles = this.container.style;
console.log(styles);
// for getting computed styles
const computed = window.getComputedStyle(this.container).getPropertyValue("border-radius"));// border-radius can be replaced with any other style attributes;
console.log(computed);
}
Here is an example of computing the CSS property value via React Refs and .getComputedStyle method:
class App extends React.Component {
constructor(props) {
super(props)
this.divRef = React.createRef()
}
componentDidMount() {
const styles = getComputedStyle(this.divRef.current)
console.log(styles.color) // rgb(0, 0, 0)
console.log(styles.width) // 976px
}
render() {
return <div ref={this.divRef}>Some Text</div>
}
}
It's worth noting that while ReactDOM.findDOMNode is usable today, it will be deprecated in the future in place of callback refs.
There is a post here by Dan Abramov which outlines reasons for not using findDOMNode while providing examples of how to replace the use of ReactDOM.findDOMNode with callback refs.
Since I've seen SO users get upset when only a link was included in an answer, so I will pass along one of the examples Dan has kindly provided:
findDOMNode(stringDOMRef)
**Before:**
class MyComponent extends Component {
componentDidMount() {
findDOMNode(this.refs.something).scrollIntoView();
}
render() {
return (
<div>
<div ref='something' />
</div>
)
}
}
**After:**
class MyComponent extends Component {
componentDidMount() {
this.something.scrollIntoView();
}
render() {
return (
<div>
<div ref={node => this.something = node} />
</div>
)
}
}
You should use ReactDOM.findDOMNode method and work from there. Here's the code that does what you need.
var Hello = React.createClass({
componentDidMount: function() {
var elem = ReactDOM.findDOMNode(this.refs.container);
console.log(elem.offsetWidth, elem.offsetHeight);
},
render: function() {
return (
<div ref={"container"} className={"container"}>
Hello world
</div>
);
}
});
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
jsFiddle
In case of function components use the useRef Hook:
const _optionsButton = useRef(null);
const _onSelectText = (event) => {
if (true) {
_optionsButton.current.style["display"] = "block";
} else {
_optionsButton.current.style["display"] = "none";
}
console.log(_optionsButton.current.style); //all styles applied to element
};
add ref attribute to component
<IconButton
style={{
color: "rgba(0, 0, 0, 0.54)",
fill: "rgba(0, 0, 0, 0.54)",
border: "1px solid rgba(0, 0, 0, 0.54)",
position: "absolute",
display: "none",
}}
color="primary"
component="span"
onClick={() => {}}
ref={_optionsButton} //this
>
Check
</IconButton>;
Thank you
You already get the style, the reason why CSSStyleDeclaration object's props have so much empty string value is it's link to the inner style.
See what will happen if you make change like below:
<div ref={"container"} className={"container"} style={{ width: 100 }}></div>

Categories

Resources