Props don't update when state updates in React - reactjs

I'm passing some values from my state in the App component to the GetWordContainer component as props. With these props, I set the state in my child component. However, the state in GetWordContainer only updates once. How can I have the state in the child component continue to update when the state in App.js changes?
App.js
class App extends Component {
state = {
redirect: {},
word: '',
error: '',
info: [],
partOfSpeech: [],
versions: [],
shortdef: "",
pronunciation: "",
}
setRedirect = redirect =>{{
this.setState({redirect})
}
// console.log(this.state.redirect);
console.log(this.state.word);
}
handleUpdate = values =>
this.setState({...values})
render() {
return (
<>
<Route
render={ routeProps => <Redirector redirect={this.state.redirect} setRedirect={this.setRedirect} {...routeProps} />}
/>
<header>
<nav>
<Link to='/'>My Dictionary</Link>
</nav>
</header>
<main>
<Route
render = { routeProps =>
!routeProps.location.state?.alert ? '' :
<div>
{ routeProps.location.state.alert }
</div>
}
/>
<Switch>
<Route exact path="/" render={ routeProps =>
<Home
setRedirect={this.setRedirect}
handleUpdate={this.handleUpdate}
{...routeProps} />}
/>
<Route exact path="/definition/:word" render={routeProps =>
<GetWordContainer
setRedirect={this.setRedirect}
handleUpdate={this.handleUpdate}
word={this.state.word}
partOfSpeech={this.state.partOfSpeech}
versions={this.state.versions}
shortdef={this.state.shortdef}
pronunciation={this.state.pronunciation}
{...routeProps} />}
/>
</Switch>
</main>
</>
);
}
}
GetWordContainer.js
class GetWordContainer extends Component {
//this state only updates once
state = {
word: this.props.word,
info: this.props.info,
partOfSpeech: this.props.parOfSpeech,
versions: this.props.versions,
shortdef: this.props.shortdef,
pronunciation: this.props.pronunciation,
}
render (){
return (
<div>
<Search
handleUpdate={this.props.handleUpdate}
setRedirect={this.props.setRedirect}
/>
<div>
{this.state.word}
</div>
<div>
{this.state.partOfSpeech}
</div>
<div>
{this.state.versions.map((v, i) => <div key={i}>{v}</div>)}
</div>
<div>
{this.state.pronunciation}
</div>
<div>
{this.state.shortdef}
</div>
</div>
);
}
}

The issue you're facing is that the state value in the GetWordContainer component is instantiated on the initial render. There are exceptions but in general, React will reuse the same component across renders when possible. This means the component is not re-instantiated, so the state value does not change across re-renders.
One solution to this problem is to use the appropriate lifecycle method to handle when the component re-renders and update state appropriately: getDerivedStateFromProps
However, since it appears you want to render the props directly, I would recommend avoiding state entirely in GetWordContainer.
For example:
class GetWordContainer extends Component {
render() {
return (
<div>
<Search
handleUpdate={this.props.handleUpdate}
setRedirect={this.props.setRedirect}
/>
<div>{this.props.word}</div>
<div>{this.props.partOfSpeech}</div>
<div>
{this.props.versions.map((v, i) => (
<div key={i}>{v}</div>
))}
</div>
<div>{this.props.pronunciation}</div>
<div>{this.props.shortdef}</div>
</div>
);
}
}

Your constructor lifecycle method runs only once - during the initialisation of the component. If you are expecting new data from parent component, you can re-render your child by using componentDidUpdate() or getDerivedStateFromProps.
componentDidUpdate(prevProps) {
if (prevProps.word || this.props.word) {
this.setState({
word
})
}
}
I notice that your child component is not manipulating the props, it is just a display-only container. Why don't you just pass the props and display it directly rather than taking the longest route? Your child component can be a functional component:
const GetWordContainer = (props) => {
return (
<div>
<Search
handleUpdate={props.handleUpdate}
setRedirect={props.setRedirect}
/>
<div>
{props.word}
</div>
<div>
{props.partOfSpeech}
</div>
<div>
{props.versions.map((v, i) => <div key={i}>{v}</div>)}
</div>
<div>
{props.pronunciation}
</div>
<div>
{props.shortdef}
</div>
</div>
);
}

Related

React updating component when params change with old state

i'm rendering some router links with params that contain the the same url but different id params. The router view updates but the state is always behind 1.
here's my Router setup:
<Router>
<div>
<h1>HEY THERE</h1>
<Link to={'/'}>Home</Link>
<Link to={'/detail/13721'}>Show Number 1 </Link>
<Link to={'/detail/1228'}>Show Number 2</Link>
</div>
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/detail/:id" component={DetailPage} />
</Switch>
</Router>
Here's my detail page setup:
import React, { Component } from 'react'
class DetailPage extends React.Component {
constructor(props){
super(props);
this.state ={
showid: '30318',
showurl: 'http://localhost/podcast/podcastsbyid/?id=',
shows: []
}
}
render() {
return ( <div>
<h1>Detail Page</h1><p>{this.state.showid}</p>
{this.state.shows.map((show, i) => {
return <div key={i}>{show.title}</div>
})}
</div>
)
}
getShow(){
fetch(this.state.showurl + this.state.showid).then(res => res.json()).then(data => {
this.setState({shows: []})
this.setState({shows: this.state.shows.concat(data.items)})
})
}
componentWillReceiveProps(newProps){
if(this.state.showid == newProps.match.params.id){
console.log('they are the same')
}
else{
console.log('they are different')
this.setState({showid: newProps.match.params.id})
this.getShow()
}
}
}
export default DetailPage;
any help would be appreciated!!
The problem seems to be state update being asynchronous. Try updating the code as shown below:
componentWillReceiveProps(newProps){
if(this.state.showid == newProps.match.params.id){
console.log('they are the same')
}
else{
console.log('they are different')
this.setState({showid: newProps.match.params.id}, this.getShow)
// use it as callback ------------------------------^
}
}

React) "this.props" while sharing the "state" using router

Somebody help me :(
I couldn't find "this.props.Epoint" on the result page.
It's just like "Epoint : " "IPoint : ". Empty, Empty.
I do have to receive "Epoint : 0", "IPoint : ", don't I?
Here is the code. Please save me.
<App.js>
class App extends Component {
state = {
EPoint: 0,
IPoint: 0,
};
upEPoint = async () => {
this.setState({
Epoint: this.state.EPoint ++
})
};
upIPoint = async () => {
this.setState({
Ipoint: this.state.IPoint ++
})
};
render() {
return (
<>
<Router>
<Route exact path="/" component={Home} />
<Route path="/question1" component={() => <Question1 EPoint={this.state.EPoint} IPoint={this.state.IPoint} upEPoint={this.upEPoint} upIPoint={this.upIPoint}/>} />
<Route path="/question2" component={() => <Question2 EPoint={this.state.EPoint} IPoint={this.state.IPoint} upEPoint={this.upEPoint} upIPoint={this.upIPoint}/>} />
<Route path="/question3" component={() => <Question3 EPoint={this.state.EPoint} IPoint={this.state.IPoint} upEPoint={this.upEPoint} upIPoint={this.upIPoint}/>} />
<Route path="/result" component={() => <Result EPoint={this.state.EPoint} IPoint={this.state.IPoint}/>} />
<Router/>
</>
export default App;
<Result.js>
class Result extends Component {
render() {
return (
<div>
<header>
<h1> Result </h1>
<h5> Epoint : {this.props.Epoint}</h5>
<h5> Ipoint : {this.props.Ipoint}</h5>
</header>
</div>)
}
}
export default Result;
I think the first issue here is that you are trying to access Epoint from props, but the variable in state that you are passing down in props is actually EPoint (notice the capital P there). Same goes for IPoint.
Your Result.js should look like this:
import React from "react";
class Result extends React.Component {
render() {
return (
<div>
<header>
<h1> Result </h1>
<h5> Epoint : {this.props.EPoint}</h5>
<h5> Ipoint : {this.props.IPoint}</h5>
</header>
</div>
);
}
}
export default Result;
As the other answers have also mentioned, you cannot set your state as you have.
I am not so good with class components, but I believe you must set it something like the following:
constructor(props) {
super(props);
this.state = { EPoint: 0, IPoint: 0 };
}
u cant use this.state inside setState just get prev state from arrow function then assign it to new object and return it into setState
upIPoint = async () => {
this.setState(prev => ({
Ipoint: prev.IPoint + 1
})
};

Reactjs component does not call componentWillMount method when route is matched

I am trying to create an SPA using React.
I have an index.js and App.js, SidebarContentWrap.js, Sidebar.js, Content.js components.
index.js has BrowserRouter and calls App.js Component.
App.js fetches data from API in componentWillMount method and then renders a dynamic route <Route path={/playlist/:slug} component={SidebarContentWrap}/>
According to my understanding whenever route will match, componentWillMount in SidebarContentWrap will be called and I will fetch data in it and then render that data. But it does not happen.
Here is some of my code.
/*App.js*/
class App extends Component {
constructor(props){
super(props);
this.state = {
playLists: [],
dataRoute: `${Config.apiUrl}playlists?per_page=3`
}
}
componentWillMount(){
fetch(this.state.dataRoute)
.then(res => res.json())
.then(playlists => this.setState((prevState, props) => {
return { playLists : playlists.map( playlist => {
return { name: playlist.name, slug: playlist.slug}
}
)};
}));
}
render() {
return (
<div className="App">
<Header />
<switch>
{/*<Route path={`/playlist/:slug`} render={({match})=><SidebarContentWrap match={match} playLists={this.state.playLists}/>}/>*/}
<Route path={`/playlist/:slug`} component={SidebarContentWrap}/>
</switch>
<Footer />
</div>
);
}
}
export default App;
AND
/*SidebarContentWrap.js*/
class SidebarContentWrap extends Component {
constructor(props){
super(props);
}
componentWillMount(){
//FETCH DATA HERE EVERY TIME WHEN URL IS CHANGED
}
render() {
return (
<div className="sidebar-content-wrap">
<div className="wrap clearfix">
<main className="App-content">
{/*<Route path={`/playlist/:slug`} render={()=><Content/>}/>*/}
<Content />
</main>
<aside className="App-sidebar">
<div className="tabs">
{/*{this.props.playLists.map((playlist) =>*/}
{/*<NavLink key={playlist.slug} to={`/playlist/${playlist.slug}`}>{playlist.name}</NavLink>*/}
{/*)}*/}
<NavLink key="playlist-1" to="/playlist/playlist-1">Playlist 1</NavLink>
<NavLink key="playlist-2" to="/playlist/playlist-2">Playlist 2</NavLink>
<NavLink key="playlist-3" to="/playlist/playlist-3">Playlist 3</NavLink>
</div>
<div className="tabs-content">
{this.props.match.params.slug}
{/*<Route path={`/playlist/:slug`} render={()=><Sidebar/>}/>*/}
<Sidebar />
</div>
</aside>
</div>
</div>
);
}
}
export default SidebarContentWrap;
componentWillMount only get's called once when the component is first rendered. When your route changes, you aren't unmounting that component, so that's why componentWillMount never gets called again. What you want is to use componentWillReceiveProps instead. When you change the route, new router props will get passed to the component. So you should use componentWillReceiveProps to react to a url change.
You'll still want your fetch in componentWillMount for the very first time the component is rendered, but after that, the fetching should happen in componentWillReceiveProps.
class SidebarContentWrap extends Component {
constructor(props){
super(props);
}
componentWillMount(){
this.fetchData(this.props);
}
componentWillReceiveProps(nextProps){
this.fetchData(nextProps);
}
fetchData(props) {
//FETCH DATA HERE EVERY TIME WHEN URL IS CHANGED
}
render() {
return (
<div className="sidebar-content-wrap">
<div className="wrap clearfix">
<main className="App-content">
{/*<Route path={`/playlist/:slug`} render={()=><Content/>}/>*/}
<Content />
</main>
<aside className="App-sidebar">
<div className="tabs">
{/*{this.props.playLists.map((playlist) =>*/}
{/*<NavLink key={playlist.slug} to={`/playlist/${playlist.slug}`}>{playlist.name}</NavLink>*/}
{/*)}*/}
<NavLink key="playlist-1" to="/playlist/playlist-1">Playlist 1</NavLink>
<NavLink key="playlist-2" to="/playlist/playlist-2">Playlist 2</NavLink>
<NavLink key="playlist-3" to="/playlist/playlist-3">Playlist 3</NavLink>
</div>
<div className="tabs-content">
{this.props.match.params.slug}
{/*<Route path={`/playlist/:slug`} render={()=><Sidebar/>}/>*/}
<Sidebar />
</div>
</aside>
</div>
</div>
);
}
}
export default SidebarContentWrap;
If you change the route with a Link, the component doesn't get remounted, it re-renders. You need to add the fetching logic to componentWillReceiveProps
class SidebarContentWrap extends Component {
//...
componentWillReceiveProps(nextProps) {
//fetch
}
}

ReactRouter communication between parent component and child component

My parent component:
class Main extends Component {
constructor(props) {
super(props);
this.state = {
docked: false,
open: false,
transitions: true,
touch: true,
shadow: true,
pullRight: false,
touchHandleWidth: 20,
dragToggleDistance: 30,
currentUser: {}
};
this.renderPropCheckbox = this.renderPropCheckbox.bind(this);
this.renderPropNumber = this.renderPropNumber.bind(this);
this.onSetOpen = this.onSetOpen.bind(this);
this.menuButtonClick = this.menuButtonClick.bind(this);
this.updateUserData = this.updateUserData.bind(this);
}
updateUserData(user){
this.setState({
currentUser: user
})
}
render() {
return (
<BrowserRouter>
<div style={styles.content}>
<div className="content">
<Switch>
<Route path="/login/:code/:state" component={Login} updateUserData = {this.updateUserData}/>
<Route path="/dashboard" component={Login}/>
</Switch>
</div>
</div>
</BrowserRouter>
)
}
}
My child (login) component:
class Login extends Component{
constructor(props) {
super(props);
this.state = {
linkedInUrl: ''
};
}
componentWillMount(){
const query = new URLSearchParams(this.props.location.search);
if(query.get('code') && query.get('state')){
const code = query.get('code');
axios.post(Globals.API + '/user/saveUser', {
code: code,
})
.then((response) => {
if(response.data.success == true){
this.props.updateUserData(response.data.user);
}
})
.catch((error) => {
console.log(error);
})
}
}
render() {
const { linkedInUrl } = this.state;
return (
<div className="panel center-block" style={styles.panel}>
<div className="text-center">
<img src="/images/logo.png" alt="logo" style={styles.logo}/>
</div>
<div className="panel-body">
<a href={linkedInUrl} className="btn btn-block btn-social btn-linkedin">
<span className="fa fa-linkedin"></span>
Sign in with LinkedIn
</a>
</div>
<div className="panel-footer">
</div>
</div>
)
}
I am trying to update the currentUser object from Main component when I get a response in Login component and to also be able to access currentUser object from within all child components of Main (basically from my entire app). But this.props is empty in Login component and I cannot do this.props.updateUserData(response.data.user); either. Can anyone tell me why please? Thank you all for your time!
Because you don't pass any props to Login component. So to get it working you shouldn't use component prop on Route component. Instead of it you should use render prop, which takes a function which returns a component or jsx doesnt matter. More about Route component you can find here.
So replace this route
<Route
path="/login/:code/:state"
component={Login}
updateUserData = {this.updateUserData}
/>
To something like this, using render prop:
<Route
path="/login/:code/:state"
render={() => <Login updateUserData={this.updateUserData} currentUser= {this.state.currentUser} />}
/>
Worked example
Here is more example how to pass props into Route components using react-router.
Hope it will help

Pass props from wrapper to one children page

Hello and thank you in advance for your help. I have a problem passing props to components loaded with routes. I have a routes file with a wrapper component that loads the pages regarding the path url. On the wrapper component (Layout) I would like to pass to the children components some props. But as the children components are called with this.props.children I don't know how to pass the props. I tried many things and nothing has worked.
I have the following rotes file:
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import Layout from '../components/pages/Layout.js';
import Search from '../components/pages/Search.js';
import Queue from '../components/pages/Queue.js';
import About from '../components/pages/About.js';
const routes = () =>
<Route path="/" component={Layout}>
<IndexRoute component={Search}></IndexRoute>
<Route path="queue" component={Queue}></Route>
<Route path="about" component={About}></Route>
</Route>
export default routes;
In Layout I have:
import React from "react";
import Footer from "../common/Footer.js";
import Nav from "../common/Nav.js";
import Header from "../common/Header.js";
export default class Layout extends React.Component {
constructor(props) {
super(props);
this.state = {
isSongPlaying: false,
playingTrackId: "",
playingList: []
}
}
handleClickTrack(track) {
this.setState({
isSongPlaying: !this.state.isSongPlaying
});
}
renderTrack(i) {
return (
<Player audio_id={id} />
);
}
render() {
const { location } = this.props;
const { history } = this.props;
const { children } = this.props;
return (
<div>
<Header />
<Nav location={location} history={history}/>
<div className="container">
<div className="row">
<div className="col-lg-12">
{this.props.children}
</div>
</div>
<div className="row">
<div className="col-lg-12">
<div className="song-player">
{this.state.isSongPlaying ? this.renderTrack(this.state.playingTrackId) : null}
</div>
</div>
</div>
<Footer/>
</div>
</div>
);
}
}
on {this.props.children} the component is loading my pages components Search, Queue, and About, but i would like add callback props to my Search and Queue components.
On my wrapper Layout component I want to achieve the following:
import React from "react";
import Footer from "../common/Footer.js";
import Nav from "../common/Nav.js";
import Header from "../common/Header.js";
export default class Layout extends React.Component {
constructor(props) {
super(props);
this.state = {
isSongPlaying: false,
playingTrackId: "",
playingList: []
}
}
handleClickTrack(track) {
this.setState({
isSongPlaying: !this.state.isSongPlaying
});
}
renderTrack(i) {
return (
<Player audio_id={id} />
);
}
render() {
const { location } = this.props;
const { history } = this.props;
const { children } = this.props;
return (
<div>
<Header />
<Nav location={location} history={history}/>
<div className="container">
<div className="row">
<div className="col-lg-12">
{RENDER SEARCH WITH onClick prop}
{RENDER QUEUE WITH onClick prop}
</div>
</div>
<div className="row">
<div className="col-lg-12">
<div className="song-player">
{this.state.isSongPlaying ? this.renderTrack(this.state.playingTrackId) : null}
</div>
</div>
</div>
<Footer/>
</div>
</div>
);
}
}
I'm using render={() => <Component/>} in my React apps to give my Routes props. Don't know if it's the perfect way. There might be other ways. But it's working! :)
Here's an example of one of your Routes:
<Route exact path="/queue" render={() => <Queue prop={something}/>} />
You can pass the props to child component using childContextTypes static object.Define below context in parent Layout component.
static childContextTypes={
isSongPlaying: React.PropTypes.bool,
playingTrackId:React.PropTypes.string,
playingList: React.PropTypes.array
}
Then populate the value using getChildContext() in Layout class
getChildContext=()=>{
return {
isSongPlaying: false,
playingTrackId:"Any Value to child component that you are going to pass",
playingList: [] //Array with value
}
}
Now you can get the value in child component (About.jsx or Search.jsx) by defining context types like below
static contextTypes={
isSongPlaying: React.PropTypes.bool,
playingTrackId:React.PropTypes.string,
playingList: React.PropTypes.array
}
Now you can access the property value in child component using the context like below
let isPlaying= this.context.isSongPlaying //or
let playingTrackId=this.context.playingTrackId

Resources