I have a React component that's created dynamically, with data loaded into its state. This state is being cleared between click events, resulting in the visible info being lost (undefined). Where has it gone?
const React = require('react');
const ReactDOM = require('react-dom');
class App extends React.Component {
constructor(props) {
super(props);
this.nodesQuantity = 0;
this.state = {roots: []};
}
nextId(me) {
return me.nodesQuantity++;
}
componentDidMount() {
var roots = [1,2,3].map(node => {
let nextId = this.nextId(this);
return <Node key={nextId} data={node} nextId={this.nextId} depth={0} god={this} />
});
this.setState({roots: roots});
}
render() {
return (
<ul>{this.state.roots}</ul>
)
}
}
class Node extends React.Component{
constructor(props) {
super(props);
this.state = {data: this.props.data, childNodes: [], visible: true};
this.toggleExpanded = this.toggleExpanded.bind(this);
}
toggleExpanded(event) {
console.log(this.state.childNodes.length)
let val={id:"asdf",label:"asdf"}
if (this.state.childNodes.length > 0) {
this.state.childNodes.forEach((childNode) => {
childNode.setState({visible: !childNode.state.visible})
});
} else {
this.setState((oldState, props) => {
let nextId = props.nextId(props.god);
console.log(nextId);
oldState.childNodes.push(<Node key={nextId} data={val} nextId={props.nextId} depth={props.depth+1} god={props.god} />);
});
}
event.stopPropagation();
}
render() {
return (
<li onClick={this.toggleExpanded}>
{this.state.data.id} ({this.state.data.label})
<ul>{this.state.childNodes}</ul>
</li>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('react')
)
(oldState, props) => {
let nextId = props.nextId(props.god);
console.log(nextId);
oldState.childNodes.push(<Node key={nextId} data={val} nextId={props.nextId} depth={props.depth+1} god={props.god} />);
}
doesn't return anything... possibly the source of your undefined. You're also modifying previous state via push which is forbidden in the react docs:
prevState is a reference to the previous state. It should not be
directly mutated. Instead, changes should be represented by building a
new object based on the input from prevState and props.
You ought to be doing something like:
return {childNodes:[...(oldState.childNodes)]}
I solved it. My main problem was a misunderstanding of react lifecycle.
This answer, albeit a bit outdated, was quite useful in understanding my problem.
Also a bit switch was to store the Node as json and only build the nodes during render step. Updates are triggered with setState which pass through the componentWillReceiveProps method that updates the state of the node json and proceeds to render
Here's my solution: https://jsfiddle.net/n3ygz7uk/2/
Related
If I return jsx of a Component from some method and call that method in another component's render, then is a new object created on every render?
class MyComponent extends React.Component {
render = () =>(<h3>My Component</h3>)
}
function getEl(){ return (<MyComponent />) }
class Comp extends React.Component{
componentDidMount = () =>{
let ct = 100;
const update = () => {
if(ct-- < 0) return;
this.setState({}, update);
}
update();
}
render(){
return (<div> {getEl()} </div>)
}
}
If Comp renders 100 times, is a new instance of MyComponent created 100 times? And what if the props passed to MyComponent changes. Then?
Looking for some good in-depth explanation of why this happens
This stuff can be really confusing at first. Or even after you've done your first few bits of work. :-)
The JSX <MyComponent /> is shorthand for React.createElement(MyComponent). What that does is create an object (a "React element") giving React the information it needs to create or update an instance of the component. It doesn't always create a new component instance; if an instance already exists for where that object is used to render something, the previous instance is updated. The object from the JSX itself isn't the component (and in fact, you can reuse them).
For completeness: Even when the component is re-rendered, that doesn't necessarily mean all of the DOM nodes for it are recreated; they may be updated or, if it's an unnecessary render, left completely alone.
Let's expand that code to include a prop on MyComponent and throw some instrumenting calls at it (fancy talk for console.log and a MutationObserver in this case):
class MyComponent extends React.Component {
constructor(props) {
super(props);
console.log(`MyComponent constructor, counter = ${this.props.counter}`);
}
render() {
const { counter } = this.props;
console.log(`MyComponent render, counter = ${counter}`);
return <h3>My Component: {counter}</h3>;
}
}
function getEl(counter) {
console.log(`getEl, counter = ${counter}`);
return <MyComponent counter={counter} />;
}
class Comp extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
};
}
componentDidMount() {
let counter = 0;
const handle = setInterval(() => {
++counter;
this.setState({counter});
if (counter === 5) {
clearInterval(handle);
}
}, 250);
}
render() {
const { counter } = this.state;
console.log(`Comp render, counter = ${counter}`);
return <div>{getEl(counter)}</div>;
}
}
const root = document.getElementById("root");
const ob = new MutationObserver(records => {
for (const record of records) {
console.log(`DOM modification type: ${record.type} on ${record.target.nodeName}:`);
if (record.type === "characterData") {
console.log(`* Value: Changed from ${record.oldValue} to ${record.target.nodeValue}`);
} else if (record.type === "attributes") {
// We aren't listening for these
} else {
for (const node of record.removedNodes) {
console.log(`* Removed: ${node.nodeName} with ${node.innerHTML || node.nodeValue || "(no value)"}`);
}
for (const node of record.addedNodes) {
console.log(`* Added: ${node.nodeName} with ${node.innerHTML || node.nodeValue || "(no value)"}`);
}
}
}
});
ob.observe(root, {
childList: true,
subtree: true,
characterData: true,
characterDataOldValue: true
});
ReactDOM.render(<Comp/>, root);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
As you can see, the MyComponent constructor was only called once to create a single instance of MyComponent, initially with counter = 0. After that, React updated that existing component instance five times with the counter values 1 through 5 (I used 5 instead of 100).
You can also see the DOM modifications thanks to the MutationObserver: The root component gets a new H3 with a couple of text nodes with the initial text, and then just the text node with the counter is updated as the counter changes.
I took over a React Native project from somebody else, this project is quite old (2 years) and I came across the following:
Home.js Component: (I simplified it)
export let customersData = null;
export default class Home extends Component {
render() {
return (
<JumboButton
onPress={() => {
this.props.navigator.push({
component: CustomerSearch
});
}}
>
);
}
_getAllCustomers(limit, sortAttr, order) {
apiCall.(apiUrl, {
...
}).then((responseData) => {
const customersDataAll = responseData.data;
customersData = customersDataAll.filter((f) => {
return f.lastname !== ''
});
});
}
}
So within the Home-Component, customersData is filled with data. Also the component CustomerSearch is called and within CustomerSearch I found this:
CustomerSearch.js:
import {customersData} from './Home';
export default class CustomerSearch extends Component {
constructor(props) {
super(props);
this.ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
dataSource: this.ds.cloneWithRows(customersData),
};
}
}
Two things are a bit weird for me:
Is it correct to set customersData and not this.customersData inside the callback of the api call?
Currently I get this error https://d.pr/i/IUzxdf "Cannot convert undefined or null to object" and I assume that this is because of the data import of customersData in CustomerSearch.js. Is the place where I need to look? Btw is there any chance that react tells me the exact line and file where this error occurs?
Thanks for your help!
Short answer is that it is definitely an unusual React pattern, and not one that many people would recommend. Importing a let variable into another file is not a reliable way to share information between components.
It would be far more sensible to attach customersData to your parent component's state and pass it to CustomersSearch through a prop - i.e.
export default class Home extends Component {
constructor (props) {
super(props);
this.state = { customersData: null };
this._getAllCustomers = this._getAllCustomers.bind(this)
}
render() {
return (
<JumboButton
onPress={() => {
this.props.navigator.push({
component: props =>
<CustomerSearch customersData={this.state.customersData} {...props} />
});
}}
>
);
}
_getAllCustomers(limit, sortAttr, order) {
apiCall.(apiUrl, {
...
}).then((responseData) => {
const customersDataAll = responseData.data;
const customersData = customersDataAll.filter((f) => {
return f.lastname !== ''
});
this.setState({ customersData });
});
}
}
Not sure how your JumboButton's onPress prop works exactly, but you should get the idea?
And in answer to 2. - Yes I would imagine this is the problem!
How do I filter data in a component based on a parent prop?
My data to compare to contains something like:
{
"id": "5a7847508f9337cf77712128",
"index": 0,
"projectName": "Ovolo",
"location": "Stoddard Place, Riverton, New York, 3571"
},
{
"id": "5a7847503101a8ef7f7d3c30",
"index": 1,
"projectName": "Isonus",
"location": "Elliott Walk, Magnolia, Minnesota, 4488"
}...
My component looks something like this:
import React, {Component} from 'react';
import data from "../data/projects.json";
const ProjectTitle = data.filter((projects, props) => {
if (projects.id === props.project_id )
var title = projects.projectName;
return title;
});
class ProjectName extends Component {
constructor() {
super();
this.state = {
projectTitle: ProjectTitle
}
}
componentDidMount() {
this.setState({
projectTitle: ProjectTitle
});
}
render() {
return (
<h4 className="projectName">
{ this.state.projectTitle }
</h4>
);
}
}
export default ProjectName;
Basically, I want to match props.project_id from the parent component with the first project from my data that matches the id, so I can display the projectName using the component. Currently, the one that I tried does not return the name of the project. I am new to this so I need help and guidance, maybe an example or a reference may help.
Basically you wants to watch for different project_id sent from the parent and depending on which want to filter the data, as far as i understood.
One thing you can do is use componentWillReceiveProps lifecycle to watch for new props. and can also componentDidMount lifecycle to set the projecttitle.
you need to change the projectTitle function also
const ProjectTitle = (projectId) => {
const project = data.filter((projects) => {
if (projects.id === projectId ) {
return true;
}
return false
});
if(project) {
return projects.projectName;
}
return project
}
componentWillReceiveProps(nextProps) {
const {project_id} = nextProps
const newProjectTitle = ProjectTitle(project_id)
this.setState({
projectTitle: newProjectTitle,
})
}
componentDidMount() {
const {project_id} = this.props
const newProjectTitle = ProjectTitle(project_id)
this.setState({
projectTitle: newProjectTitle,
})
}
By adding componentWIllReceiveprops and componentDidMount you can
dynamically change the project name whenever new project_id is passed
as a props. you can also use the constructor function to initialize the state like this
constructor(props) {
super(props)
this.state = {
projectTitle: ProjectTitle(props.project_id),
}
}
Refer this lifecycle methods in react for more reference
Not sure why it isn't working exactly but that ProjectTitle that you create outside the class doesn't ring well with me. I think that you're assigning it to the state and that's not working as expected. You should do the filter (or find) operation in the componentWillMount function of the React lifecycle . After you find the project you do the setState and it should work.
class ProjectName extends Component {
constructor() {
super();
this.state = {
projectTitle: ''
}
}
componentWillMount() {
const project = data.find(project => project.id === this.props.project_id);
if (project) {
this.setState({
projectTitle: project.projectName
});
}
}
render() {
return (
<h4 className="projectName">
{this.state.projectTitle}
</h4>
);
}
}
export default ProjectName;
Assuming props.project_id is a ProjectName's prop the easiest solution is to move the filter to the render function. Also, if you want to find one record use find not filter.
render() {
const project = data.find(( {id} ) => id === this.props.project_id)
return (
<h4 className="projectName">
{ project && project.title }
</h4>
);
}
You could do as other suggested and set the state during the component's creation, but then you'd have to do the same in componentWillReceiveProps, this creates a lot of unnecessary complexity.
If performance is an issue (it generally isn't) and you want to find the project only once per id, use a memoized function and keep the logic in the render function. This is a lot simpler and has the same effect.
My node.js server sends with socket.io new data each 10s. In my web application I update this.state each time that my server sends data and force to update with forceUpdate()
However, my react component doesn't refresh, I don't know why. I followed the doc but I missed something...
Parent :
class DataAnalytics extends React.Component {
constructor(props) {
super(props);
socket = this.props.socket;
this.state = {data: []};
socket.on('dataCharts', (res) => {
console.log("new data charts : "+res);
var data = JSON.parse(res);
this.setState({data: data});
this.forceUpdate();
});
}
componentWillUnmount() {
socket.off('dataCharts');
}
render() {
return (
<div id="dataAnalytics">
<Stats data={this.state.data}></Stats>
</div>
);
}
}
export default DataAnalytics;
Child :
class Stats extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="stats" style={{textAlign:'center'}}>
<h4>Number: </h4>
</div>
);
}
componentDidUpdate() {
var data = this.props.data;
if(!jQuery.isEmptyObject(data)) {
$( ".stats" ).html("<h4>Number : data['nb']['counterIn']</h4>");
}
}
}
export default Stats;
Anyone know how to refresh automatically my React component.
The React component doesn't update because it doesn't realize that it's state changes. You can force an update on a React component by creating it each time with a different key attribute.
render() {
return (
<div id="dataAnalytics">
<Stats key={this.uniqueId()} data={this.state.data}></Stats>
</div>
);
}
// Example of a function that generates a unique ID each time
uniqueId: function () {
return new Date().getTime();
}
I usually do it like -
function MyComponent() {
const [_, refresh] = useState()
useEffect(() => {
// Code that's supposed to run on refresh
}, [refresh])
return
<>
{/* Rest of the code */}
<button onclick={() => refresh(true)}>Refresh</button>
</>
}
The idea is to define a state and use it as a dependency of useEffects (or useMemos and useCallbacks).
If there are multiple effect hooks, add refresh to all of them as a dependency.
I'm having trouble figuring out how to short circuit rendering a branch
of a tree of React components using Immutable.js cursors.
Take the following example:
import React from 'react';
import Immutable from 'immutable';
import Cursor from 'immutable/contrib/cursor';
let data = Immutable.fromJS({
things: [
{title: '', key: 1},
{title: '', key: 2}
]
});
class Thing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.thing.deref() !== nextProps.thing.deref();
}
handleChangeTitle(e) {
this.props.thing.set('title', e.target.value);
}
render() {
return <div>
<input value={this.props.thing.get('title')}
onChange={this.handleChangeTitle.bind(this)} />
</div>;
}
}
class Container extends React.Component {
render() {
const cursor = Cursor.from(this.props.data, 'things', newThings => {
data.set('things', newThings);
renderContainer();
});
const things = cursor.map(thing => (
<Thing thing={thing} key={thing.get('key')} />
));
return <div>
{things}
</div>;
}
}
const renderContainer = () => {
React.render(<Container data={data} />, document.getElementById('someDiv'));
};
Say I change the first Thing's title. Only the first Thing will render with
the new title and the second Thing will not re-render due to
shouldComponentUpdate. However, if I change the second Thing's title, the
first Thing's title will go back to '' since the second Thing's cursor
is still pointing at an older version of the root data.
We update the cursors on each render of Container but the ones that don't
render due to shouldComponentUpdate also don't get the new cursor with the updated
root data. The only way I can see keeping the cursors up to date is to remove
shouldComponentUpdate in the Thing component in this example.
Is there a way to change this example to use shouldComponentUpdate using fast referential
equality checks but also keep the cursors updated?
Or, if that's not possible, could you provide an overview of how you would generally work with cursors + React components and rendering only components with updated data?
I updated your code, see comments inline:
class Thing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.thing.deref() !== nextProps.thing.deref();
}
handleChangeTitle(e) {
// trigger method on Container to handle update
this.props.onTitleChange(this.props.thing.get('key'), e.target.value);
}
render() {
return <div>
<input value={this.props.thing.get('title')}
onChange={this.handleChangeTitle.bind(this)} />
</div>;
}
}
class Container extends React.Component {
constructor() {
super();
this.initCursor();
}
initCursor() {
// store cursor as instance variable to get access from methods
this.cursor = Cursor.from(data, 'things', newThings => {
data = data.set('things', newThings);
// trigger re-render
this.forceUpdate();
});
}
render() {
const things = this.cursor.map(thing => (
<Thing thing={thing} key={thing.get('key')} onTitleChange={this.onTitleChange.bind(this)} />
));
return <div>
{things}
</div>;
}
onTitleChange(key, title){
// update cursor to store changed things
this.cursor = this.cursor.update(x => {
// update single thing
var thing = x.get(key - 1).set('title', title);
// return updated things
return x.set(key - 1,thing);
});
}
}
const renderContainer = () => {
React.render(<Container data={data} />, document.getElementById('someDiv'));
};