React - child-parent relationship with props - reactjs

I want to have a parent component that its children can register inside, so I can use this data somewhere else (generating a menu, for example).
Currently, my code is as follows:
const app = document.getElementById('app');
class Children extends React.Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.add(this.props.name);
}
render() {
return (
<div>Children</div>
)
}
}
class Items extends React.Component {
render() {
return (
<nav>
{this.props.content}
</nav>
)
}
}
class Parent extends React.Component {
constructor() {
super();
this.state = {
sections: []
}
}
add(section) {
const currentSections = this.state.sections;
const id = section.replace(' ', '-').toLowerCase();
const obj = { name: section, id };
this.setState({
sections: currentSections.push(obj)
});
}
render() {
console.log(this.state.sections);
return (
<div>
<Items content={this.state.sections} />
<Children add={this.add.bind(this)} name="Section 1" />
<Children add={this.add.bind(this)} name="Section 2" />
<Children add={this.add.bind(this)} name="Section 3" />
</div>
)
}
}
render(<Parent />, app);
My problem is, this.state.sections returns 3, but when I log it again in componentDidMount, it is an array.
What can I do?
JSBin

add(section) {
const currentSections = this.state.sections;
const id = section.replace(' ', '-').toLowerCase();
const obj = { name: section, id };
currentSections.push(obj)
this.setState({
sections: currentSections
});
}
Reason is you were setting state to currentSections.push(obj) which actually returns the count not array. Push earlier and set the sections as currentSections

The problem appears to be due to using push to append an item, which returns an index of the new element.
Mutating state in-place is not the best option, and I would argue it's better to use concat instead:
{
sections: currentSections.concat([obj])
}
Which will return a new array.
In your specific case, there's also going to be a race condition between the add calls: the three callbacks will be called at approximately the same time, so in all three, currentSections will be []. Then each one will append just on item and set it, and ultimately the state is going to end up containing only one element, not three.
To mitigate this, you can use another way of calling setState that ensures all three will be added sequentially:
this.setState(state => {
return {
sections: state.sections.concat([obj])
};
})

Related

How can I pass state to grandparent so that uncle/aunt can use it?

I've been struggling for hours trying to get some code to work. I'm new with React, but I have spent a lot time looking for a solution to this as well, and updating this code as I understood with no success.
Basically my app is a component that splits into two components, with one of those splitting into 9 buttons. When I click one of those buttons, I want its uncle/aunt to recognize that, and use the id of the button that was pushed to create a message.
I figured I should be passing the button id up to the grandparent so that it can pass the id down to the uncle/aunt. But its the passing the id to the grandparent I'm struggling with.
This is the general set up below:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
"x" : " "
};
getX(x){
this.setState({"x": x})
}
render() {
return (
<div>
<A getX={this.getX}/>
<B x={this.state.x} />
</div>
)
}
}
const A = (props) => {
const getX = (x) => props.getX(x);
a = [];
for (let i=0; i<9; i++) {
a.push(<C id={i} getX={getX}/>);
return <div>{a}</div>
}
class C extends React.Component {
constructor(props) {
super(props);
this.state = {
"id" : props.id,
"getX" : (x) => props.getX(x)
}
this.handleMouseDown = this.handleMouseDown.bind(this);
}
handleMouseDown(e) {
this.state.getX(e.target.id);
}
render() {
<div />
}
}
class B extends React.Component {
constructor(props){
super(props);
this.state = {
"x" : props.x
}
}
render() {
return <div>{this.state.x}</div>
}
}
Firstly, the getX() method of the App component doesn't seem to be working how I expected it to. By that I mean, when I add getX("7"); to the render method of the App component, just before the return statement, the whole thing crashes. But if I replace this.setState({"x": x}) with this.state.x = x in the getX() method, then the state sucessfully passes down to the component B, which is something at least. But, I don't understand why.
Secondly, I can't work out how to modify the App component's state from within component A. The getX() method of the App component doesn't seem to be passed into component A as I expected. Again, if I insert getX("7"); before the return statement of component A, the whole thing crashes again. I expected the getX function of component A to be the same function as the getX method of the App component, and therefore update the state of the App component. But I've had no success with that at all. I've even tried inserting this.getX = this.getX.bind(this) into the constructor of the App component, but that didn't solve everything for me.
Lastly, as you can probably guess, I cant modify the App component's state from any of the C components.
Any ideas? I'm stumped.
I have modified your example so that it works. A few things:
Dont copy props to state, that is an antipattern and creates bugs (as you have seen). Dont copy the id or the function passed from component A to component C, or in component B. Just use the props values.
You had some syntax errors that I fixed.
You didnt return the array created in component A.
(This is my preference, but I will argue that you are setting a value, not getting, so i renamed getX to setX.)
There was nothing returned from component C. I was not sure what you was suppoosed to be returning from that component, so I just created a button with a click-handler.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
x: '',
};
this.setX = this.setX.bind(this);
}
setX(x) {
this.setState({ x: x });
}
render() {
return (
<div>
<A setX={this.setX} />
<B x={this.state.x} />
</div>
);
}
}
const A = (props) => {
let a = [];
for (let i = 0; i < 9; i++) {
a.push(<C id={i} setX={props.setX} />);
}
return <div>{a}</div>;
};
class B extends React.Component {
render() {
return <div>{this.props.x}</div>;
}
}
class C extends React.Component {
constructor() {
super();
this.handleMouseDown = this.handleMouseDown.bind(this);
}
handleMouseDown() {
this.props.setX(this.props.id);
}
render() {
return <button onClick={this.handleMouseDown}>Click me</button>;
}
}

Add component on button click

I am making a front end application using typescript and react. I have a component A which amongst other html elements has a textbox. I want to add this component A on click of a button. So if the user clicks the button multiple times, i want a new component A to be created on every click. Also I want to be able to store the text data so that I can later fetch it and process it.
I tried to make a list of this component but it gives me an error.
interface State {
componentList?: ComponentA[];
}
export class ComponentList extends React.Component<Props, State> {
  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }
public onClick(event) {
const componentList = this.state.componentList;
this.setState({
componentList: componentList.concat(<ComponentA key=
{componentList.length} />)
});
}
  public render() {
    return (
      <React.Fragment>
        <button onClick={this.onClick}>Add component</button>
{this.state.componentList.map(function(component, index)
{
return ComponentA
})}
      </React.Fragment>
    );
  }
}
You might want to make two changes in your code.
First initialise your state in the constructor,
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state = { componentList: [] }
}
So that react can track that data.
Second thing is, you are returning wrong item from the map in the render function.
Try returning component, which is different copies of <ComponentA ../> that you pushed every time you clicked the button,
public render() {
return (
<React.Fragment>
<button onClick={this.onClick}>Add component</button>
{this.state.componentList.map(function(component, index)
{
return component;
})}
</React.Fragment>
);
}
Keep the component count in the state:
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state = {
numComponents: 0
}
}
Add a new function which creates an array of the component for rendering later:
clickedComponents = () => {
let componentArray = [];
for (let i=0; i<this.state.numComponents; i++) {
componentArrays.push(<ComponentA />);
}
return componentArray;
}
Increment the component count with your onClick function:
public onClick(event) {
this.setState({numComponents: this.state.numComponents + 1});
}
Render the component array:
public render() {
return (
<React.Fragment>
<button onClick={this.onClick}>Add component</button>
{this.clickedComponents()}
</React.Fragment>
);
}

Change in Context doesn't show in Consumer element

I have set a context which has fields updated by an API call:
export const CharacterContext = React.createContext()
export class CharacterProvider extends Component {
constructor(props) {
super(props);
this.state = {
text: "test, test",
name: "no-name",
id: "999",
currStamina: 0,
abilities: {
"bra": 0,
"agi": 0,
"int": 0,
"cun": 0,
"will": 0,
"pre": 0
},
skills: [],
characters: [],
getCharacter: this.getCharacter,
}
}
getCharacter = (id) => {
CharacterDataService.getCharacterById(id)
.then(
response => {
console.log(response);
this.setState({
name: response.data.username,
maxStamina: response.data.stamina,
currStamina: response.data.stamina,
id: response.data.id,
abilities: response.data.abilities,
skills: response.data.skills
});
}
);
}
render() {
return (
<CharacterContext.Provider
value={this.state}>
{
this.props.children
}
</CharacterContext.Provider>
)
}
}
export const CharacterConsumer = CharacterContext.Consumer
Another element has set Provider and Consumer inside for the purpose of testing. It receives the state from the provider, but even though the element is rendered AFTER context update (through a Router), it shows original values (abilities set to 0 etc...).
export default class Edit extends React.Component {
static contextType = CharacterContext;
constructor(props) {
super(props);
}
render() {
return (
<CharacterProvider>
<CharacterConsumer>
{({text, currStamina}) => (
<p>{text} : {currStamina}</p>
)}
</CharacterConsumer>
<Abilities/>
</CharacterProvider>
)
}
}
What am I missing? Why does the values in context update by the API call but the consumer element still shows the original values?
In my opinion it's a router problem.
Can you check if you <Link /> components are rendered outside of the router context. If is rendered outside the Router, meaning the Links will fallback to the default value passed to createContext.
For some reason the problem was in nesting consumer element directly under the provider. What didn't work:
render() {
return (
<CharacterProvider>
<CharacterConsumer>
{({text, currStamina}) => (
<p>{text} : {currStamina}</p>
)}
</CharacterConsumer>
<Abilities/>
</CharacterProvider>
)
}
This didn't work (change in the Provider didn't propagate to the consumer). As soon as I moved the Provider to parent element, the consumer was update with every change of the provider.
render() {
return (
<div>
<CharacterConsumer>
{({text, currStamina}) => (
<p>{text} : {currStamina}</p>
)}
</CharacterConsumer>
</div>
)
}

Fetch data periodically from server and re-render view in reactjs

I want to fetch data from server periodically and refresh rows when data is fetched by setState() but the rows doesn't re-render after setState().
constructor(props) {
super(props);
this.state = {
rows: []
}
this.refreshList = this.refreshList.bind(this);
}
refreshList() {
req.get('/data').end(function (error, res) {
// type of res is array of objects
this.setState({
rows: res
});
});
}
// call this method on button clicked
handleClick() {
this.refreshList();
}
render() {
return(
<div>
<button onClick={this.handleClick}>Refresh List</button>
<Table rows={this.state.rows}/>
</div>
);
}
when call refreshList() new feteched data doesn't render.
My table component is:
// Table component
export class Table extends Component {
constructor(props) {
super(props);
this.state = {
rows: props.rows
}
}
render() {
return (
<div>
{this.state.rows.map((row, i) => (
<div>{row.title}</div>
))}
</div>
)
}
}
Thanks a lot for your help. How can I refresh list on click button?
Your table component never changes its state after the construction. You can fix it easily by updating the state from new props:
export class Table extends Component {
constructor(props) {
super(props);
this.state = {
rows: props.rows
}
}
componentWillReceiveProps(newProps) {
this.setState({
rows: newProps.rows
});
}
render() {
return (
<div>
{this.state.rows.map((row, i) => (
<div>{row.title}</div>
))}
</div>
)
}
}
However, if your table component is so simple, you can make it stateless and use props directly without setState():
export class Table extends Component {
render() {
return (
<div>
{this.props.rows.map((row, i) => (
<div>{row.title}</div>
))}
</div>
)
}
}
Note there is no need for constructor now. We could actually make it a functional component.
Use an arrow function:
req.get('/data').end((error, res)=> {
// type of res is array of objects
this.setState({
rows: res
});
});
With the ES5 style callback function, the context of this gets lost.
You could also bind this directly to a local variable, i.e., var that = this and stick with the function syntax, but I think most would agree what the ES6 arrow syntax is nicer.

React child component can't get props.object

My parent component is like this:
export default class MobileCompo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: null,
datasets: {}
};
this.get_data = this.get_data.bind(this);
}
componentWillMount() {
this.get_data();
}
async get_data() {
const ret = post_api_and_return_data();
const content={};
ret.result.gsm.forEach((val, index) => {
content[val.city].push()
});
this.setState({data: ret.result.gsm, datasets: content});
}
render() {
console.log(this.state)
// I can see the value of `datasets` object
return (
<div>
<TableElement dict={d} content={this.state.data} />
<BubbleGraph maindata={this.state.datasets} labels="something"/>
</div>
)
}
}
child component:
export default class BubbleGraph extends React.Component {
constructor(props) {
super(props);
this.state = {
finalData: {datasets: []}
};
console.log(this.props);
// here I can't get this.props.maindata,it's always null,but I can get labels.It's confusing me!
}
componentWillMount() {
sortDict(this.props.maindata).forEach((val, index) => {
let tmpModel = {
label: '',
data: null
};
this.state.finalData.datasets.push(tmpModel)
});
}
render() {
return (
<div>
<h2>{this.props.labels}</h2>
<Bubble data={this.state.finalData}/>
</div>
);
}
}
I tried many times,but still don't work,I thought the reason is about await/async,but TableElement works well,also BubbleGraph can get labels.
I also tried to give a constant to datasets but the child component still can't get it.And I used this:
this.setState({ datasets: a});
BubbleGraph works.So I can't set two states at async method?
It is weird,am I missing something?
Any help would be great appreciate!
Add componentWillReceiveProps inside child componenet, and check do you get data.
componentWillReceiveProps(newProps)
{
console.log(newProps.maindata)
}
If yes, the reason is constructor methos is called only one time. On next setState on parent component,componentWillReceiveProps () method of child component receives new props. This method is not called on initial render.
Few Changes in Child component:
*As per DOC, Never mutate state variable directly by this.state.a='' or this.state.a.push(), always use setState to update the state values.
*use componentwillrecieveprops it will get called on whenever any change happen to props values, so you can avoid the asyn also, whenever you do the changes in state of parent component all the child component will get the updates values.
Use this child component:
export default class BubbleGraph extends React.Component {
constructor(props) {
super(props);
this.state = {
finalData: {datasets: []}
};
}
componentWillReceiveProps(newData) {
let data = sortDict(newData.maindata).map((val, index) => {
return {
label: '',
data: null
};
});
let finalData = JSON.parse(JSON.stringify(this.state.finalData));
finalData.datasets = finalData.datasets.concat(data);
this.setState({finalData});
}
render() {
return (
<div>
<h2>{this.props.labels}</h2>
<Bubble data={this.state.finalData}/>
</div>
);
}
}

Resources