Passing state to a child component of another child component - javascript

I'm building a KanBan app with ReactJS, and I'm trying to pass state from a parent component to the furthest component in the parent-child tree. I have a Column component within my main App component, and within this Column component there is another component called 'Card'. I want to pass the data the gets added/updated in the state of the App component & successfully display it in the Card component. As its obvious by now, the Card component is the child of the Column component.
I tried doing so with this.props but this only works one level down - with the column component. I thought about declaring a variable and equating to this.props.details.cards, and then setting it as the new state of the column component so that I could pass it again as props to the card component, but I assume this is not best practice.
This is my App Component:
class App extends Component {
constructor(props) {
super(props)
this.state = {
columns: [
{
name: 'Todos',
cards: []
},
{
name: 'Onprogress',
cards: []
},
{
name: 'Done',
cards: []
},
]
};
};
addCard = card => {
console.log("Adding a Card");
const cards = { ...this.state.columns.cards };
const keyDate = `card${Date.now()}`
cards[keyDate] = card;
this.setState({
columns: [
{
name: 'Todos',
cards: cards
}
]
});
};
render() {
return (
<div className="App">
{Object.keys(this.state.columns).map(key => (
<Column key={key} details={this.state.columns[key]} />
))}
<AddCardForm addCard={this.addCard} />
</div>
);
}
}
export default App;
This is my Column Component:
import React, {Component} from "react";
import Card from "./Card"
class Column extends Component {
render() {
return (
<div className="column">
<h1 className="Title">{this.props.details.name}</h1>
<Card />
</div>
);
}
}
export default Column;
And this is my Card Component:
import React, {Component} from "react";
class Card extends Component {
render() {
return (
<div className="card">
<span className="title">I'm a Card!</span>
</div>
);
}
}
export default Card;

React will only get the properties that are injected into the child component.
Considering we have three component A , B and C.
If we want to pass a prop from A to C while B is in the middle, we do this:
The A component returns the B component and passes someProperty as follows:
return <B someProperty={someValue} />
Now, in the B component we can access the property by calling it this.props.someProperty however, it won't be available at the C component, if we want to do so, we do the following.
return <C someProperty={this.props.someProperty} />
What we did in component B is pass the someProperty that came from component A which we can access in B as this.props.someProperty, we pass it again the same way to the component C..
Read more about this: Passing props between react component
Full example as follows:
Component A:
render() {
return (
<B someProperty={'someString'} />
);
}
Component B:
render() {
return (
<C someProperty={this.props.someProperty} />
);
}
Compoent C: to access the property someProperty for example in the render method
render() {
return (
<p>{this.props.someProperty}</p>
);
}

Related

Why doesn't my React child component get value (re-render) when I change the parent state?

Background
I wrote an exact, short yet complete example of a Parent component with a nested Child component which simply attempts:
Alter a string in the Parent's state
See the Child component updated when the Parent's state value is altered (this.state.name)
Here's What It Looks Like
When the app loads a default value is passed from Parent state to child props.
Change The Name
All I want to do is allow the change of the name after the user adds a new name in the Parent's <input> and clicks the Parent's <button>
However, as you can see, when the user clicks the button only the Parent is rendered again.
Questions
Is it possible to get the Child to render the new value?
What am i doing wrong in this example -- why isn't it updating or
rendering the new value?
All Source Code
Here is all of the source code and you can view it and try it in my StackBlitz project.
I've kept it as simple as possible.
Parent component (DataLoader)
import * as React from 'react';
import { useState } from 'react';
import { Grid } from './Grid.tsx';
interface LoaderProps {
name: string;
}
export class DataLoader extends React.Component<LoaderProps, {}> {
state: any = {};
constructor(props: LoaderProps) {
super(props);
this.state.name = this.props.name;
this.changeName = this.changeName.bind(this);
}
render() {
const { name } = this.state;
let parentOutput = <span>{name}</span>;
return (
<div>
<button onClick={this.changeName}>Change Name</button>
<input id="mapvalue" type="text" placeholder="name" />
<hr id="parent" />
<div>### Parent ###</div>
<strong>Name</strong>: {parentOutput}
<hr id="child" />
<Grid childName={name} />
</div>
);
}
changeName() {
let newValue = document.querySelector('#mapvalue').value.toString();
console.log(newValue);
this.setState({
name: newValue,
});
}
}
Child component (Grid)
import * as React from 'react';
interface PropsParams {
childName: string;
}
export class Grid extends React.Component<PropsParams, {}> {
state: any = {};
constructor(props: PropsParams) {
super(props);
let counter = 0;
this.state = { childName: this.props.childName };
console.log(`CHILD -> this.state.name : ${this.state.childName}`);
}
render() {
const { childName } = this.state;
let mainChildOutput = <span>{childName}</span>;
return (
<div>
<div>### Child ####</div>
<strong>Name</strong>: {mainChildOutput}
</div>
);
}
}
App.tsx is set up like the following -- this is where default value comes in on props
import * as React from 'react';
import { DataLoader } from './DataLoader.tsx';
import './style.css';
export default function App() {
return (
<div>
<DataLoader name={'default value'} />
</div>
);
}
You're seeing two different values because you're tracking two different states. One in the parent component and one in the child component.
Don't duplicate data.
If the child component should always display the prop that's passed to it then don't track state in the child component, just display the prop that's passed to it. For example:
export class Grid extends React.Component<PropsParams, {}> {
render() {
const { childName } = this.props; // <--- read the value from props, not local state
let mainChildOutput = <span>{childName}</span>;
return (
<div>
<div>### Child ####</div>
<strong>Name</strong>: {mainChildOutput}
</div>
);
}
}
In the Child component, you set the prop childName value to state in the contructor ONLY. The constructor is executed ONLY WHEN THE COMPONENT IS MOUNTED. So, it doesn't know if the childName prop is changed later.
There are 2 solutions for this.
(1) Directly use this.props.childName without setting it to a state.
(2) Add a useEffect that updates the state value on prop change.
React.useEffect(() => {
this.state = {
childName: this.props.childName;
};
}, [this.props.childName]);
However, I recommend 1st solution since it's not a good practice to duplicate data.

How to properly render Component after this.setState in React

I have this React component
import React, { Component } from "react";
export default class ResourceForField extends Component {
constructor() {
super();
this.state = {
resources: [],
};
}
componentDidMount() {
// get the resources from the Link props and save it into the state
this.setState({
resources: this.props.location.resources,
});
}
// This component gets the id of current learningField from the url
// and the rest(like the resources) from the Link component
render() {
return (
<div>
{this.state.resources.map(res => (
<div>test</div>
))}
</div>
);
}
}
It gets the resources from the Link component, and that works fine. If I check out the state of the Component from the dev tools, the state looks right. And I thought with my logic this should work. So firstly, the state is empty, the component gets rendered, since the state is empty it doesn't render any components. Then, setState gets called, it gets all the resources and saves them into the state, and then the component would re-render, and it should work, but it doesn't. I'm getting a TypeError: Cannot read property 'map' of undefined error. What is the correct way to do this and how do I fix this?
Try this code:
import React, { Component } from "react";
export default class ResourceForField extends Component {
constructor() {
super();
this.state = {
resources: this.props && this.props.location && this.props.location.resources?this.props.location.resources:[],
};
}
componentDidMount() {
}
// This component gets the id of current learningField from the url
// and the rest(like the resources) from the Link component
render() {
return (
<div>
{this.state.resources.map(res => (
<div>test</div>
))}
</div>
);
}
}
Or use directly props
import React, { Component } from "react";
export default class ResourceForField extends Component {
constructor() {
super();
}
// This component gets the id of current learningField from the url
// and the rest(like the resources) from the Link component
render() {
return (
<div>
{
this.props && this.props.location &&
this.props.location.resources
?this.props.location.resources.map(res => (
<div>test</div>
))
:null
}
</div>
);
}
}
Or use componentWillReceiveProps or getDerivedStateFromProps life cycle methods.
Check this.props.location.resources is array.
See more: https://hackernoon.com/replacing-componentwillreceiveprops-with-getderivedstatefromprops-c3956f7ce607
For first check is this.props.location.resources array, or if data type changes you can add checking, you can use lodash isArray or with js like this:
import React, { Component } from "react";
export default class ResourceForField extends Component {
constructor() {
super();
this.state = {
resources: [],
};
}
componentDidMount() {
// get the resources from the Link props and save it into the state
Array.isArray(this.props.location.resources) {
this.setState({
resources: this.props.location.resources,
});
}
}
// This component gets the id of current learningField from the url
// and the rest(like the resources) from the Link component
render() {
return (
<div>
{this.state.resources.map(res => (
<div>test</div>
))}
</div>
);
}
}
Or you can just use hooks like this:
import React, { useState, useEffect } from "react";
export default function ResourceForField({location}) {
const [ resources, setResources ] = useState([]);
useEffect(() => {
if (location && Array.isArray(location.resources)) {
setResources(location.resources)
}
}, [location]);
return (
<div>
{resources.map(res => (
<div>test</div>
))}
</div>
);
}
If the internal state of ResourceForField doesn't change and always equals to its prop, you shouldn't save the prop in the state. You can instead create a pure functional component.
Also note that there's nothing preventing you from initializing the state from the props in constructor method. i.e. you're not required to wait for the component to mount in order to access the props.
So, I'd write the following component for ResourceForField:
function ResourceForField({resources = []}) {
return (
<div>
{
resources.map(res => (<div>test</div>))
}
</div>
);
}

React - change props at child1 with callback and pass new value to child2

In my app I have a child component, 'Menu', where a 'select' state is updated by a click event, like so:
Menus.jsx (child):
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import Brewing from './Brewing.jsx';
import { withRouter } from 'react-router-dom';
class Menus extends Component{
constructor (props) {
super(props);
this.state = {
select: '',
isLoading: false,
redirect: false
};
};
(...)
gotoCoffee = (index) => {
this.setState({isLoading:true, select:this.state.coffees[index]})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
console.log(this.state.coffees[index])
}
renderCoffee = () => {
if (this.state.redirect) {
return (<Redirect to={'/coffee/'+this.state.select} />)
}
}
render(){
const coffees = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{coffees.map((coffee, index) =>
<span key={coffee}>
<div>
{this.state.isLoading && <Brewing/>}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee(index)}
style={{textDecoration:'underline',cursor:'pointer'}}>
<strong><font color="#C86428">{coffee}</font></strong></div>
<div>
</div>
</div>
</span>)
}
</div>
);
}
}
export default withRouter(Menus);
the above works.
However, let's say I have another child component, 'Coffee', which should inherit this changed state.
I have learned that passing this event change, and state, from child to another child component, is an anti-pattern. Considering the ways of React, data can only flow from top-to-bottom i.e., from parent-to-child.
So have I tried to manage 'select' state from top to bottom, like so:
App.jsx (parent)
class App extends Component {
constructor() {
super();
this.state = {
select: '',
};
this.onSelectChange = this.onSelectChange.bind(this);
};
then I would use a callback here at 'App.jsx', like so:
onSelectChange(newSelect){
this.setState({ select: newSelect });
}
and pass it to 'Menus' component, like so:
<Route exact path='/menus' render={() => (
<Menus
onSelectChange={this.onSelectChange}
/>
)} />
finally, at child 'Menus', I would user event change to change props, which could be passed to other childs etc:
gotoCoffee = (index) => {
this.setState({isLoading:true})
this.props.onSelectChange(index)
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
console.log(this.props.select)
}
but I'm getting console.log(this.props.select) 'undefined'.
what am I missing?
You are only passing onSelectChange method as a prop to Menu component right now, to access this.props.select, you need to pass select as prop to Menu.
<Route exact path='/menus' render={() => (
<Menus
onSelectChange={this.onSelectChange}
select={this.state.select}
/>
)} />
Whenever this.onSelectChange method gets called and state changes in your App.jsx, your Menu component will be rendered. You can use the updated this.props.select in your render method or in any non static method of your Menu component.
class Menu extends Component {
render() {
console.log(this.props.select);
return (
...
);
}
}

react-router Route component constructor is not called when props are updated in BrowserRouter component

I have a React app. I'm using react and react-router. Here's the sandbox link.
I have an App.js file like this:
import React, { Component } from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import Items from './Items';
class App extends Component {
constructor(props) {
super(props);
this.state = {
items: []
}
}
componentDidMount() {
this.setState({ items: ['a', 'b', 'c'] });
}
render() {
const { items } = this.state;
return (
<BrowserRouter>
<div>
<Route exact path="/" render={(props) => <Items {...props} items={items} />} />
</div>
</BrowserRouter>
)
}
}
export default App;
In this file, in the componentDidMount, I'm getting data from an API, then passing it to the Items component. On the initial page load, of course items will be an empty array, and then it will eventually have content.
In my Items.js file, I have:
import React, { Component } from 'react';
class Items extends Component {
constructor(props) {
super(props);
this.items = this.props.items;
}
render() {
return (
<div>
{this.items.length}
</div>
)
}
}
export default Items;
As you can see, this.items is retrieved from the props. On initial page load, again, this is an empty array. But after the componentDidMount fires in App.js, the constructor in Items.js is not fired, so this.items is never re-populated with the items.
How can I instead fire the constructor in Items.js? I know this is a simple example, and therefore could technically be solved by simply accessing the props in the render method, but I really need the constructor to fire, because in my actual app, I have more complex logic in there.
You can use this.props directly in the render method of Items to extract the data you want.
class Items extends Component {
constructor(props) {
super(props);
}
render() {
const { items } = this.props;
return (
<div>
{items.length}
</div>
)
}
}
Since the constructor of a component is only called once, I will instead move the logic that relies on the props, to the parent component.

React: parent component props in child without passing explicitly

Is it possible to have the props of the parent component to be available in child component without passing them down?
I am trying to implement a provider pattern, so that to access all the provider props in its child components.
EX:
Suppose the below provider comp FetchProvider will fetch the data and theme props on its own, and when any child component is enclosed by it, I want to access both props "data" and "theme" in the child component as well. How can we achieve it?
class FetchProvider
{
proptypes= {
data: PropTypes.shape({}),
theme: PropTypes.shape({})
}
render()
{
// do some
}
mapStateToProps()
{
return {data, theme};
}
}
class ChildComponent
{
proptypes= {
name: PropTypes.shape({})
}
render()
{
const{data, them} = this.props; // is this possible here?
// do some
}
}
and if I try to above components as below.
<FetchProvider>
<ChildComponent name="some value"/> //how can we access parent component props here? without passing them down
<FetchProvider/>
This is exactly what react context is all about.
A Consumer can access data the a Provider exposes no matter how deeply nested it is.
// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton(props) {
// Use a Consumer to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
Here is a small running example:
Note This is the react v16 context API.
Your use case can be solved with the usage of React context. With the help of Context, any child that is wrapped by a provided can be a consumer for the data that is provided by the Provider
In your case, you can use it like
context.js
export const FetchContext = React.createContext();
Provider.js
import { FetchContext } from 'path/to/context.js';
class FetchProvider extends React.Component
{
proptypes= {
data: PropTypes.shape({}),
theme: PropTypes.shape({})
}
render()
{
const { data, theme, children } = this.props;
return (
<FetchContext.Provider value={{ data, theme}}>
{children}
</FetchContext.Provider>
)
}
mapStateToProps()
{
return {data, theme};
}
}
ChildComponent.js
class ChildComponent extends React.Component
{
proptypes= {
name: PropTypes.shape({})
}
render()
{
const{data, them} = this.props; // use it from props here
// do some
}
}
export default (props) => (
<FetchContext.Consumer>
{({ data, theme }) => <ChildComponent {...props} data={data} theme={theme} />}
</FetchContext.Consumer>
)
However given the fact that you are already using Redux, which is build on the concept of Context, you might as well use redux and access the values within the child component since they are the same values that are supplied from the Redux store to the child by parent.
class ChildComponent extends React.Component
{
proptypes= {
name: PropTypes.shape({})
}
render()
{
const{data, them} = this.props; // use it from props here
// do some
}
}
const mapStateToProps = (state) => {
return {
data: state.data,
theme: state.theme
}
}
You can use React.Children to iterate over the children and pass whatever props you want to send to the new cloned elements using React.cloneElement.
EX:
class Parent extends React.Component {
constructor(props) {
super(props);
}
render() {
const { children } = this.props;
const newChildren = React.Children.map(children, child =>
React.cloneElement(child, { myProp: 'test' }));
return(
<View>
{newChildren}
</View>
)
}
}
Are you looking for:
class MyParent extends Component {
render() {
return <MyChild {...this.props}>
// child components
</MyChild>
}
}
This would pass all of the props passed into MyParent to the MyChild being rendered.

Categories

Resources