Reactjs component does not call componentWillMount method when route is matched - reactjs

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
}
}

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 ------------------------------^
}
}

Props don't update when state updates in React

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>
);
}

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

Meteor and React: Correct way to render according to login state

I have a Meteor + React application with login functionality. The main navigation bar displays different items depending on the login state of the user.
My problem is that when a user logs in it doesn't automatically update the navigation bar like expected. It only shows after the page is reloaded or the route is changed.
This obviously goes against the greatness of using React.
This is what I have:
AppContainer
export class AppContainer extends Component {
constructor(props) {
super(props)
this.state = this.getMeteorData()
}
getMeteorData() {
return { isAuthenticated: Meteor.userId() !== null}
}
render() {
return(
<div className="hold-transition skin-green sidebar-mini">
<div className="wrapper">
<Header isAuthenticated={this.state.isAuthenticated} />
<MainSideBar />
{this.props.content}
<Footer />
<ControlSideBar />
<div className="control-sidebar-bg"></div>
</div>
</div>
)
}
}
Header
export class Header extends Component {
render() {
return (
<header className="main-header">
...
<nav className="navbar navbar-static-top" role="navigation">
<a href="#" className="sidebar-toggle" data-toggle="offcanvas" role="button">
<span className="sr-only">Toggle navigation</span>
</a>
<CustomNav isAuthenticated={this.props.isAuthenticated} />
</nav>
</header>
)
}
}
CustomNav
export class CustomNav extends Component {
render() {
if(this.props.isAuthenticated) {
navigation = <NavLoggedIn />
} else {
navigation = <NavLoggedOut />
}
return (
<div className="navbar-custom-menu">
{navigation}
</div>
)
}
}
I think that's all the relevant code needed to help solve this but let me know if I should be doing something else. Not sure if passing the props down through the components like I am is the correct thing to do either so give me a heads up if I'm messing up there too please.
Help is much appreciated!
You need to wrap your Component to use Meteor's reactive data.
AppContainer.propTypes = {
user: PropTypes.object,
};
export default createContainer(() => {
return {
user: Meteor.user(),
};
}, AppContainer);
The user object will be stored in this.props.user. You can use it to conditionally render views.

Resources