Since upgrading to using Gatsby V2 I have been struggling to pass this.state to child components to be used as this.props.
For Example I have a Container that has data1 and data2 added to this.state as
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
data1: '',
data2: ''
};
}
componentDidMount() {
// Loading database
.then(doc =>
this.setState({
data1: doc.data().data1,
data2: doc.data().data2
})
);
}
render() {
const children = this.props;
const stateAsProps = React.Children.map(children, child =>
React.cloneElement(child, {
data1: this.state.data1,
data2: this.state.data2
})
);
return (
<div>{stateAsProps}</div>
);
}
}
and a child component as
class Child extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<h1>{this.props.data1}</h1>
<p>{this.props.data2}</p>
);
}
}
and finally this is brought into the page by
const Page = () => (
<Parent authUserID="01234" campaignID="56789">
<Child />
</Parent>
);
In Gatsby V1 this was working but now with the migration I am receiving an error Uncaught Error: Objects are not valid as a React child (found: object with keys {authUserID, campaignID, children}). If you meant to render a collection of children, use an array instead.
Can anyone advise to why and how this issue can be rectified?
You are using the entire props object as children in your Parent component. Make sure you destructure out the children object from the props instead and it will work as expected.
const { children } = this.props;
Related
I have this class component where I'd like to fetch the response from server to an state array so I can pass further the elements to another component as props, so far I have this:
export default class MainApp extends Component {
state = {
posts: [],
}
constructor(props){
super(props);
const request = new FetchRequest();
request.setAmount(this.props.amount);
request.setUserid(this.props.token);
request.setSeenpostsList(this.props.seenPosts);
var stream = client.fetchPosts(request, {});
stream.on('data', function(response) {
this.setState({
posts: [...this.state.posts, response.array]
})
});
}
render(){
return(
<div className="main-app">
<Navbar />
<Postbox token = {this.props.token}/>
{this.state.posts.map(element =>
<Postcard username = {element[0]}/>
)}
</div>
)
}
With this code I get TypeError: Cannot read properties of undefined (reading 'posts').
What is the correct way to do it?
Your state needs to be initialised within the constructor() {}
constructor(props) {
super(props);
this.state = {
posts: []
}
}
Here's info you might also want to read
So I'm a beginner with react and I was wondering how to re-render the child after setting the state in the parent (from the child). Here's a code sample. I have a function that calls a GET request using Axios and when I press the button in the child component ideally it will update the state in the parent and also re-render the child but it only does the former.
Parent:
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
data: []
}
}
fetchData = () => {
axios
.get(url)
.then(res => this.setState({data: res.data}))
}
Render() {
return (<Child data={this.state.data} fetchData={this.fecthData}/>)
}
// ...
Child:
class Child extends Component {
// ...
render() {
const { data, fetchData } = this.props
// render data
return <button onClick={fetchData}>Change data then fetch</button>
}
}
Also, are you supposed to make a local state in the Child and set it as a copy of the Parent's state or just passing it down as a prop is okay?
Your parent component holds the data and the child uses it. It seems to me you're doing it the right way. Here is a fully working example:
Codesandbox
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
this.updateData = this.updateData.bind(this);
}
async fetchData() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
return response.json();
}
updateData() {
this.setState({ data: [] }) // Creates a flicker, just so you see it does refresh the child
this.fetchData().then((res) => this.setState({ data: res }));
}
render() {
return <Child data={this.state.data} onAction={this.updateData} />;
}
}
Note I renamed your child prop fetchData into onAction (I don't know what's the name of the action that triggers a refresh, could be onRefresh). It's always best to see components props with separation between data attributes and event attributes.
Even standard components have it this way: <input value={user.firstname} onChange={doSomething} />. So, better to prefix events by on, then the parent decides what to do with it. It's not the child's concern.
class Child extends Component {
render() {
const { data, onAction } = this.props;
return (
<>
<button onClick={onAction}>Change data then fetch</button>
{data.map((item) => (
<div key={item.id}>
{item.id} - {item.title}
</div>
))}
</>
);
}
}
I have two components, one parent one child. I am using the fetch method in componentDidMount() callback. Once I do this, I set the state with key items to that data that is pulled from the api. Once I do this it should be able to be console logged in the child component as a prop. However this is not working. What am I doing wrong here?
Parent Component:
import React, {Component} from 'react';
import Map from './maps/Map';
class Main extends Component {
constructor(props){
super(props);
this.state = {
name: "John",
items: []
}
}
componentDidMount() {
fetch('https://hn.algolia.com/api/v1/search?query=')
.then(dat => dat.json())
.then(dat => {
this.setState({
items: dat.hits
})
})
}
render() {
return (
<div>
<Map list={this.state.name} items={this.state.items}></Map>
</div>
)
}
}
export default Main;
Child Component:
import React, {Component} from 'react';
class Map extends Component {
constructor(props) {
super(props);
console.log(props.items)
}
render () {
return (
<h1>{this.props.name}</h1>
)
}
}
export default Map;
First, fetch is asynchronous. So, the fetch statement might be pending by the time you try to console.log the result inside the child constructor.
Putting the console.log inside the render method would work, because the component will be rerendered, if the state items changes.
The constructor for a component only runs one time during a lifecycle. When it does, props.items is undefined because your ajax request is in-flight, so console.log(props.items) doesn't show anything.
If you change your constructor to console.log("constructed");, you'll see one-time output (stack snippets may not show this--look in your browser console). Henceforth, componentDidUpdate() can be used to see the new props that were set when your ajax request finishes.
You could also log the props inside the render method, which will run once before the ajax request resolves and again afterwards when props.items changes.
As a side point, you have <Map list=... but the component tries to render this.props.name, which is undefined.
Also, if you aren't doing anything in the constructor (initializing state or binding functions) as here, you don't need it.
class Map_ /* _ added to avoid name clash */ extends React.Component {
constructor(props) {
super(props);
console.log("constructed");
}
componentDidUpdate(prevProps, prevState) {
const props = JSON.stringify(this.props, null, 2);
console.log("I got new props", props);
}
render() {
return (
<div>
<h1>{this.props.name}</h1>
<pre>
<ul>
{this.props.items.map((e, i) =>
<li key={i}>{JSON.stringify(e, null, 2)}</li>)}
</ul>
</pre>
</div>
);
}
}
class Main extends React.Component {
constructor(props) {
super(props);
this.state = {name: "John", items: []};
}
componentDidMount() {
fetch('https://hn.algolia.com/api/v1/search?query=')
.then(dat => dat.json())
.then(dat => {
this.setState({items: dat.hits})
});
}
render() {
return (
<div>
<Map_
name={this.state.name}
items={this.state.items}
/>
</div>
);
}
}
ReactDOM.createRoot(document.querySelector("#app"))
.render(<Main />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
The only problem you have is that you are trying to use this.props.name and your Map component props are called list and items, so it will return undefined.
If you log your props in the constructor you will get the initial state of Main because the fetch hasn't returned anything yet. Remember that the constructor only runs once. So you are probably getting an empty array when you log props.items in the constructor because that's what you have in your initial state.
{
name: "John",
items: []
}
If you log the props in your render method you will see your array filled with the data you fetched, as you can see here:
https://codesandbox.io/s/stoic-cache-m7d43
If you don't want to show the component until the data is fetched you can include a boolean property in your state that you set to true once you the fetch returns a response and pass it as a prop to your component. Your component can you use that variable to show, for example, a spinner while you are fetching the data. Here's an example:
https://codesandbox.io/s/reverent-edison-in9w4
import CircularProgress from "#material-ui/core/CircularProgress"
class Main extends Component {
constructor(props) {
super(props);
this.state = {
name: "John",
items: [],
fecthed: false
};
}
componentDidMount() {
fetch("https://hn.algolia.com/api/v1/search?query=")
.then(dat => dat.json())
.then(dat => {
this.setState({
items: dat.hits,
fecthed: true
});
});
}
render() {
return (
<Map
fetched={this.state.fecthed}
list={this.state.name}
items={this.state.items}
/>
);
}
}
class Map extends Component {
render() {
return (
<div>
{this.props.fetched ? (
<div>
<h1>{this.props.list}</h1>
{this.props.items.map((item, indx) => (
<div key={indx}>Author: {item.author}</div>
))}
</div>
) : (
<CircularProgress />
)}
</div>
);
}
}
Hope this helps. Cheers!
After I execute a query in the console I can see: {data:{action: [...]}. How can I assign that data to a variable in a React component?
Trying this but it is not working:
class MyGraph extends React.Component {
constructor(props) {
super(props);
this.state = {
nodes: this.data
Have some falsy or empty initial value for state.node. Then fetch the data in componentDidMount - on success, update state.node with your actual data. That data can also be passed down to child components. Example:
class MyGraph extends React.Component {
constructor(props) {
super(props);
this.state = {
nodes: null,
}
}
componentDidMount() {
// w/e your fetch function is. I'm assuming your method uses promises.
fetchDataSomehow().then((response) => {
this.setState({nodes: response.data}) // or whatever property has the nodes
})
}
render() {
const nodes = this.state.nodes;
return (
<LoadingIcon isVisible={!nodes}>
{(nodes || []).map((node) => (
<MyCircle key={node.someUniqueId} node={node} /> // access `node` inside the `<MyCircle />` component using `this.props.node`
))}
</LoadingIcon>
)
}
}
You'll need to handle what happens/renders while the data is still loading (e.g the made-up <LoadingIcon /> component).
Within JSX syntax i can easily set a child prop to follow parent's state, like:
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { attr1: 10 }
}
render() {
return (
<div>
<Child prop1={this.state.attr1} />
</div>
)
}
}
So whenever state.attr1 changes, Children will have it prop1 changed.
How can i get the same behavior with dynamic created childs:
class Parent extends React.Component {
constructor(props) {
super(props);
this.buildChildren = (items) => {
items.map(item =>
React.createElement(
childrenListWithClasses[item['childrenClass']],
{ key: item['id'], data: {} }
)
)
}
this.state = {
children: buildChildren(this.props.childrenList)
}
}
componentWillReceiveProps(nextProps) {
// maybe here i want to do something like
nextProps.data.forEach(item =>
ReactDOM.findDOMNode(item['id']).setProps({ data: item['data']}) )
}
render() {
return (
<div>
<div>{this.state.children}</div>
</div>
)
}
}
This piece of code does not work, also i've read that setProps is deprecated. Another approach would be create the child with props ~binded to this.state.data if data data is structured as obj:
React.createElement(
childrenListWithClasses[item['childrenClass']],
{ key: item['id'], data: this.state.data[item['id']] }
)
But seems like it tries to evaluate this.state.data[item['id']] at execution.
I've found a suggestion to use cloneWithProps, but this doesn't seem right... If JSX can change child's props why cant i do it with code? also it would not make if i want my child elements to smoothly transition when changing props, child's lifecycle would be ignored.