Access parent component state from child component - reactjs

I can't figure out how to access parent component state from child component. When I try my code below I get an error in my parent components saying:
Stateless function components cannot have refs.
Can someone point in the right direction?
This is my parent Component (Podcast.js)
import React, { PureComponent } from 'react';
import PodcastItem from './PodcastItem';
class Podcast extends PureComponent { // PureComponent is preferred here instead of Component
constructor(props) {
super(props);
this.state = {
podcast: []
};
}
// Fetches podID from props.match
fetchPodcast () {
fetch(`https://itunes.apple.com/search?term=podcast&country=se&media=podcast&entity=podcast&limit=20`)
.then(response => response.json())
.then(data => this.setState({ podcast: data.results }));
}
componentDidMount () {
this.fetchPodcast()
}
// Check if new props is not the same as prevProps
componentDidUpdate (prevProps) {
// respond to parameter change
let oldId = prevProps.match.params.podID
let newId = this.props.match.params.podID
if (newId !== oldId)
this.fetchPodcast()
}
render() {
return (
<div>
<h2>Country ({this.props.match.params.podID}) </h2>
<ul>
{this.state.podcast.map(podcast => (
<PodcastItem key={podcast.collectionId} podcast={podcast} />
))}
</ul>
</div>
)
}
}
export default Podcast;
And this is the child component (PodcastItem.js)
import React from 'react';
// Here Stateless function is enough
const PodcastItem = ({ podcast }) => (
<li key={podcast.collectionId}>
<a ref={podcast.collectionId}>{podcast.collectionName}</a>
</li>
);
export default PodcastItem;

Related

Child component not triggering rendering when parent injects props with differnt values

Here are my components:
App component:
import logo from './logo.svg';
import {Component} from 'react';
import './App.css';
import {MonsterCardList} from './components/monster-list/monster-card-list.component'
import {Search} from './components/search/search.component'
class App extends Component
{
constructor()
{
super();
this.state = {searchText:""}
}
render()
{
console.log("repainting App component");
return (
<div className="App">
<main>
<h1 className="app-title">Monster List</h1>
<Search callback={this._searchChanged}></Search>
<MonsterCardList filter={this.state.searchText}></MonsterCardList>
</main>
</div>
);
}
_searchChanged(newText)
{
console.log("Setting state. new text: "+newText);
this.setState({searchText:newText}, () => console.log(this.state));
}
}
export default App;
Card List component:
export class MonsterCardList extends Component
{
constructor(props)
{
super(props);
this.state = {data:[]};
}
componentDidMount()
{
console.log("Component mounted");
this._loadData();
}
_loadData(monsterCardCount)
{
fetch("https://jsonplaceholder.typicode.com/users", {
method: 'GET',
}).then( response =>{
if(response.ok)
{
console.log(response.status);
response.json().then(data => {
let convertedData = data.map( ( el, index) => {
return {url:`https://robohash.org/${index}.png?size=100x100`, name:el.name, email:el.email}
});
console.log(convertedData);
this.setState({data:convertedData});
});
}
else
console.log("Error: "+response.status+" -> "+response.statusText);
/*let data = response.json().value;
*/
}).catch(e => {
console.log("Error: "+e);
});
}
render()
{
console.log("filter:" + this.props.filter);
return (
<div className="monster-card-list">
{this.state.data.map((element,index) => {
if(!this.props.filter || element.email.includes(this.props.filter))
return <MonsterCard cardData={element} key={index}></MonsterCard>;
})}
</div>
);
}
}
Card component:
import {Component} from "react"
import './monster-card.component.css'
export class MonsterCard extends Component
{
constructor(props)
{
super(props);
}
render()
{
return (
<div className="monster-card">
<img className="monster-card-img" src={this.props.cardData.url}></img>
<h3 className="monster-card-name">{this.props.cardData.name}</h3>
<h3 className="monster-card-email">{this.props.cardData.email}</h3>
</div>
);
}
}
Search component:
import {Component} from "react"
export class Search extends Component
{
_searchChangedCallback = null;
constructor(props)
{
super();
this._searchChangedCallback = props.callback;
}
render()
{
return (
<input type="search" onChange={e=>this._searchChangedCallback(e.target.value)} placeholder="Search monsters"></input>
);
}
}
The problem is that I see how the text typed in the input flows to the App component correctly and the callback is called but, when the state is changed in the _searchChanged, the MonsterCardList seems not to re-render.
I saw you are using state filter in MonsterCardList component: filter:this.props.searchText.But you only pass a prop filter (filter={this.state.searchText}) in this component. So props searchTextis undefined.
I saw you don't need to use state filter. Replace this.state.filter by this.props.filter
_loadData will get called only once when the component is mounted for the first time in below code,
componentDidMount()
{
console.log("Component mounted");
this._loadData();
}
when you set state inside the constructor means it also sets this.state.filter for once. And state does not change when searchText props change and due to that no rerendering.
constructor(props)
{
super(props);
this.state = {data:[], filter:this.props.searchText};
}
If you need to rerender when props changes, use componentDidUpdate lifecycle hook
componentDidUpdate(prevProps)
{
if (this.props.searchText !== prevProps.searchText)
{
this._loadData();
}
}
Well, in the end I found what was happening. It wasn't a react related problem but a javascript one and it was related to this not been bound to App class inside the _searchChanged function.
I we bind it like this in the constructor:
this._searchChanged = this._searchChanged.bind(this);
or we just use and arrow function:
_searchChanged = (newText) =>
{
console.log("Setting state. new text: "+newText);
this.setState({filter:newText}, () => console.log(this.state));
}
Everything works as expected.

Pass additional props through hookrouter

I'm trying to pass additional data through to a component using hookrouter. I'm not sure what I'm doing wrong. I get the error Functions are not valid as React child. This may happen if you return a Component instead of <Component /> from render.
Path: Routes
const routes = {
"/modules/unit/:id": ({ id }) => (page_url) => (
<UnitPage unit_id={id} page_url={page_url} />
)
};
Path: Link within page
<A href={`modules/unit/${id}`} page_url={page_url}>
Unit link
</A>
** UPDATE **
Path UnitPage.jsx
class UnitPage extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidUpdate() {
console.log("props", this.props);
}
render() {
const { id, page_url } = this.props;
return (
<div>
<p>{id}</p>
<p>{page_url}</p>
</div>
);
}
}

How can I update a single React Component from multiple different components?

I'm learning React and still trying to figure out how to plan out and implement some things. I have an app that makes three different API calls and thus three different return values. I'd like to have a global status component that tells me if all three loaded or not. Here's my psuedo code since I haven't found the proper way to do this yet, but this is effectively my train of thought at the moment. I have the main app:
const App = () => {
return (
<div>
<GenericAPICallerA />
<GenericAPICallerB />
<GenericAPICallerC />
<div>
<APIStatus/>
</div>
</div>
);
}
This is the APIStatus which just returns if all A, B, and C API calls have loaded or not:
class APIStatus extends React.Component{
constructor(props){
super(props);
this.state = {
aLoaded: false,
bLoaded: false,
cLoaded: false,
};
}
render(){
if (this.state.aLoaded && this.state.bLoaded && this.state.cLoaded){
return <div>Everything has loaded!</div>
}
else{
return <div>Nothing has loaded!</div>
}
}
}
And finally one of the APICaller components. The others are essentially the same:
class GenericAPICallerA extends React.Component{
constructor(props){
super(props);
this.state = {
error: null,
isLoaded: false,
};
}
componentDidMount(){
fetch("example.com/api",{
method: 'GET',
})
.then(res => res.json())
.then(
(result) =>{
this.setState({
isLoaded: true,
});
},
(error) =>{
this.setState({
isLoaded: false,
error
});
}
)
}
render(){
const { error, isLoaded, profile } = this.state;
if (error){
return <div>Errored!</div>
} else if (!isLoaded){
// APIStatus.state.aLoaded=false
} else {
// APIStatus.state.aLoaded=true
return(
<div>Generic Caller A is done!</div>
);
}
}
}
The comments in the render section are what I don't know how to do. I feel like I should pass in the APIStatus as a prop to the GenericAPICaller but I'm still unsure how I would update that property from inside the GenericAPICaller.
Thanks!
You can create a function in parent component and pass it to the child will be triggered and pass a state variable to the child where it will be used
For example:
import React from 'react'
import ComponentA from './ComponentA'
import ComponentB from './ComponentB'
class App extends React.Component
{
constructor()
{
super()
this.state = { my_state_variable:`some value` }
}
my_function()
{
this.setState({ my_state_variable:`new value` })
}
render = () => <>
<ComponentA my_function={my_function} />
<ComponentB my_state_variable={my_state_variable} />
</>
}
export default App
ComponentA
import React from 'react'
const ComponentA = ({ my_function }) => <>
<button onClick={() => my_function() }>Click Me </button>
</>
export default ComponentA
ComponentB
import React from 'react'
const ComponentB = ({ my_state_variable }) => <>
<p>{my_state_variable}</p>
{my_state_variable === `some value` && <p>if some value this will render </p>}
{my_state_variable === `new value` && <p>if new value this will render </p>}
</>
export default ComponentA
You can use context to accomplish this. By using context, you are able to access the value you provide to it as long as the component you attempt to access it through is a child of a provider.
The example below illustrates how you can access a shared state between multiple components at different levels without having to pass props down.
const {
useState,
useEffect,
createContext,
useContext,
Fragment
} = React;
const MainContext = createContext({});
function Parent(props) {
const [state, setState] = useState({child1:false,child2:false,child3:false});
return <MainContext.Provider value={{state,setState}}>
<Child1/> {state.child1? 'loaded':'not loaded'}
<br/>
<Child2/>
</MainContext.Provider>;
}
function Child1(){
const {state, setState} = useContext(MainContext);
return <button onClick={()=>setState({...state, child1:!state.child1})}>Load Child 1</button>;
}
function Child2(){
const {state, setState} = useContext(MainContext);
return <Fragment>
<button onClick={()=>setState({...state, child2:!state.child2})}>Load Child 2</button> {state.child2? 'loaded':'not loaded'}
<br/>
<Child3/> {state.child3? 'loaded':'not loaded'}
</Fragment>;
}
function Child3(){
const {state, setState} = useContext(MainContext);
return <button onClick={()=>setState({...state, child3:!state.child3})}>Load Child 3</button>;
}
const el = document.querySelector("#root");
ReactDOM.render(<Parent/>, el);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>

How to pass the changed state from child component to its parent in ReactJS

I am trying to understand how to pass a changed state from child component to its parent in ReactJS? so far the following code changes the child state but not the parents state, any clue what I am doing wrong?
I am using redux to get product array from mongodb.
Product array example:
[
{
“_id”: “2331”,
“department”: “Shoes”,
“category”: “Shoes/Women/Pumps”,
“name”: “Calvin Klein”,
“title”: “Evening Platform Pumps”,
“description”: “Perfect for a casual night out or a formal event.”,
“style”: “Designer”,
"colors": ["red","yellow","red","black"]
},
{
“_id”: “30671”,
“department”: “Shoes”,
“category”: “Shoes/Women/Pumps”,
“name”: “zara”,
“title”: “Evening Platform Pumps”,
“description”: “Perfect for a casual night out or a formal event.”,
“style”: “Designer”,
"colors": ["red","yellow","red","black"]
}
]
Parent Component
import React, { Component } from 'react'
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
products: [],
};
}
componentDidMount() {
this.props.getProducts();
}
componentDidUpdate(prevProps, prevState) {
if (this.props.product.products !== prevState.products) {
this.setState({ products: this.props.product.products });
}
}
onUpdateProducts = (e) => {
const newProducts = this.state.products;
this.props.updateProductName(newProducts);
};
render() {
const { products } = this.state;
if (isEmpty(products)) {
productContent = (
<div>
<p className="lead text-muted">Error Empty Products </p>
</div>
);
} else {
const productArr = products.map((product) => (
<Child key={product._id} product={product} />
));
productContent = (
<div>
{productArr}
</div>
);
}
return (
<div className="container">
{productContent}
<div className="row">
<div className="col-md-12">
<button className="btn " onClick={this.onUpdateProducts}>
Submit
</button>
</div>
</div>
</div>
)
}
}
const mapStateToProps = (state) => ({
product: state.product
});
export default connect(mapStateToProps, {
getProducts,updateProductName
})(Parent);
Child Component
import React, { Component } from 'react'
export default class Child extends Component {
constructor(props) {
super(props);
this.state = {
product: this.props.product,
};
}
componentDidUpdate(prevProps, prevState) {
if (this.props.product !== prevProps.product) {
this.setState({
product: this.props.product
});
}
}
onChangeProductName = (e) => {
const newProduct = Object.assign({}, this.state.product, {
name: e.target.value
});
this.setState({ product: newProduct }, function() {
console.log('onChangeProductName: ', this.state.product);
});
};
render() {
const { product } = this.state;
return (
<div>
<TextInput
placeholder="Product Name"
name="prd_name"
value={product.name}
onChange={this.onChangeProductName}
/>
</div>
)
}
}
There are two ways for a child component to update the parent component:
Without using Redux, you can pass a function as a prop of the child component, so the child component can call this function to update the parent component.
Store the data in the Redux store. The child component dispatches an action which updates the Redux state, where the parent component gets data.
A simple example would explain the concept of passing the changed state from child to the parent.
Component A:
export default class A extends Component{
//This is a callback
handleStateChange = (value) ={
console.log("value", value);//you get the value here when state changes in B(Child) component
}
render(){
return(
<div>
<B handleStateChange={this.handleStateChange} />
</div>
)
}
}
Component B:
export Class B extends Component{
constructor(props){
super(props);
this.state={
value: "01"
}
}
handleButton = () => {
const value = "02";
this.setState({
value: "02"
});
this.props.handleStateChange(value);
}
render(){
return(
<div>
<button onClick={this.handleButton} />
</div>
)
}
}
Or you can directly pass the state if you call this.props.handleStateChange(this.state.value); this in render directly on any event handler if you want to pass updated state
As #Ying zuo mentioned you need to use redux to get the changed state value of child component in parent component.
When state changes in child component, you make a redux action call by passing the value as param and set that in the state in reducer and get the state in your parent component
Hope that explains the concept.
You have to pass the child a function.
In the child component you are setting state to be equal to props value, and then you are updating state. This has no connection to parent class - you also shouldn't modify props just as an aside.
The solution is to pass a function from the parent to child. This function will update the parent state, and because you are passing the parent state to the child, it will also be updated.
So in your parent class you could do something like:
onChangeProductName = (value, i) => {
const new_product_array = [...this.state.products];
new_product_array[i].name = value;
this.setState({ products: new_product_array});
};
You would need to pass this to the child
const productArr = products.map((product, i) => (
<Child
key={product._id}
product={product} onChangeName={this.onChangeProductName.bind(this)}
index={i} />
));
And then call it in the child
<TextInput
placeholder="Product Name"
name="prd_name"
value={product.name}
onChange={() => this.props.onChangeName(product, this.props.index)}
/>
The child component then doesn't need all the state tracking.

React passing onClick value from one class to another

In Tournaments.js I have a list of Tournament names each with unique ID's which are fetched from an API. Now whenever I click on one of these tournament names, I get it's ID but I need to pass this ID to Template.js where I can fetch tournament data based on the Tournament ID that was clicked. I am trying to do something with passing props from child to parent but I am completely lost right now.
Tournament.js:
import React, { Component } from "react";
import Template from './template';
const API = 'http://localhost:8080/api/tournaments';
class Tournaments extends Component {
constructor() {
super();
this.state = {
data: []
}
}
componentDidMount() {
fetch(API)
.then((Response) => Response.json())
.then((findresponse) => {
console.log(findresponse)
this.setState({
data:findresponse,
})
})
}
reply_click(event) {
var targetId = event.target.getAttribute('id');
console.log(targetId);
}
render() {
return(
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="jumbotron text-center">
{
this.state.data.map((dynamicData, key) =>
<div>
<a href={"/#/template"} id={dynamicData.id} onClick={this.reply_click}>{dynamicData.name}</a>
Edit</button>
<Template name={dynamicData.id}></Template>
</div>
)
}
</div>
</div>
</div>
</div>
)
}
}
export default Tournaments;
Template.js:
import React, { Component } from "react";
import Parser from 'html-react-parser';
import Tournaments from "./Tournaments";
import './template.css';
import './index.css';
const tournyAPI = 'http://localhost:8080/api/tournaments';
const teamAPI = 'http://localhost:8080/api/teams'
class template extends Component {
constructor() {
super();
this.state = {
data: [],
}
}
componentDidMount() {
fetch(tournyAPI)
.then((Response) => Response.json())
.then((findresponse) => {
this.setState({
tournydata:findresponse.filter(res => res.id === 18),
})
})
So basically my goal is to use targetID from Tournament.js in place of the '18' in the ComponentDidMount in Template.js
You should keep this value in the parent's component state and pass it as a prop to child.
When your onClick is fired you should update parents state so updated props will be passed to child.
Here is the code:
Tournament.js
import React, { Component } from "react";
import Template from './template';
const API = 'http://localhost:8080/api/tournaments';
class Tournaments extends Component {
constructor() {
super();
this.state = {
data: [],
targetId: null,
}
}
componentDidMount() {
fetch(API)
.then((Response) => Response.json())
.then((findresponse) => {
console.log(findresponse)
this.setState({
data:findresponse,
})
})
}
reply_click = id => {
return () => {
this.setState({ targetId: id })
}
}
render() {
return(
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="jumbotron text-center">
{
this.state.data.map((dynamicData, key) =>
<div>
<a href={"/#/template"} onClick={this.reply_click(dynamicData.id)}>{dynamicData.name}</a>
Edit</button>
<Template name={dynamicData.id} targetId={this.state.targetId}></Template>
</div>
)
}
</div>
</div>
</div>
</div>
)
}
}
export default Tournaments;
Template.js
import React, { Component } from "react";
import Parser from 'html-react-parser';
import Tournaments from "./Tournaments";
import './template.css';
import './index.css';
const tournyAPI = 'http://localhost:8080/api/tournaments';
const teamAPI = 'http://localhost:8080/api/teams'
class template extends Component {
constructor() {
super();
this.state = {
data: [],
}
}
componentDidMount() {
fetch(tournyAPI)
.then((Response) => Response.json())
.then((findresponse) => {
this.setState({
tournydata: findresponse.filter(res => res.id === this.props.targetId),
})
})
But do it using componentDidUpdate instead of componentDidMount if you want keep updated your Template component after every changing of targetId.
Like this:
Template.js
componentDidUpdate(prevProps) {
if (prevProps.targetId !== this.props.targetId) {
fetch(tournyAPI)
.then((Response) => Response.json())
.then((findresponse) => {
this.setState({
tournydata:findresponse.filter(res => res.id === this.props.targetId),
})
})
}
}
If you need to do it at once during first rendering just add check if targetId is not null in your Tournament component.
Something like this:
Tournament.js
render() {
...
{this.state.targetId ? <Template name={dynamicData.id} targetId={this.state.targetId}></Template> : null }
...
}
Add targetID to your parent state and pass as prop to child:
reply_click(event) {
var targetId = event.target.getAttribute('id');
this.setState({ targetID });
}
<Template targetID={this.state.targetID}
In Template you can access it with this.props.targetID.
You need to use componentDidUpdate though, see React Child component don't update when parent component's state update
In your example it's very easy as you are rendering <Template> in loop you can pass it as param
Problem 1
React purpose is not to manipulate dom like this var targetId = event.target.getAttribute('id') You have this id, why you are fetching it from DOM? Instead of that accept it as param
<a href={"/#/template"} id={dynamicData.id} onClick={(e) => this.reply_click(e, dynamicData.id)}>{dynamicData.name}</a>
and
reply_click(event, targetId) {
console.log(targetId);
}
Never ever query DOM in React. You will get mess and errors because of unmounted elements and etc. React uses JSDom (in memory) not Real DOM
Problem 2
Just pass it as param
and call setState inside reply_click
reply_click(event, targetId) {
console.log(targetId);
this.setState({targetID: targetId})
}

Resources