Why do 3 different component instances seem to be sharing state? - reactjs

I have a mind boggling issue where all three of these <RecordAdmin> component instances seem to be using the state from whichever component is loaded first on page load.
I have no clue how it's happening or why, and weirdly, it was working before.
<Switch>
<Route path="/admin/books">
<RecordAdmin singular="book" plural="books" table={BookTable} form={BookForm} />
</Route>
<Route path="/admin/authors">
<RecordAdmin singular="author" plural="authors" table={AuthorTable} form={AuthorForm} />
</Route>
<Route path="/admin/branches">
<RecordAdmin singular="branch" plural="branches" table={BranchTable} form={BranchForm} />
</Route>
</Switch>
Using console.log, it seems as though all 3 of these components will have the same this.state.records object. Shouldn't each component instance have its own state?
Here is the source for the <RecordAdmin> component:
import React from "react";
import Axios from "axios";
import {
Switch,
Route,
NavLink,
Redirect
} from "react-router-dom";
class NewRecordForm extends React.Component {
constructor(props) {
super(props);
this.state = {
redirect: false,
};
}
handleSubmit = (event, formFields, multipart = false) => {
event.preventDefault();
let formData = null;
let config = null;
if (multipart) {
formData = new FormData();
for (let [key, value] of Object.entries(formFields)) {
formData.append(key, value)
}
config = {
headers: {
'Content-Type': 'multipart/form-data'
}
}
} else {
formData = formFields;
}
Axios.post(`${process.env.REACT_APP_API_URL}/${this.props.plural}`, formData, config)
.then(response => {
this.setState({redirect: true})
}).catch(error => {
console.log(error)
})
}
render() {
if (this.state.redirect) {
this.props.redirectCallback();
}
const Form = this.props.form
return (
<div>
{this.state.redirect ? <Redirect to={`/admin/${this.props.plural}`} /> : null}
<Form handleSubmit={this.handleSubmit} />
</div>
)
}
}
function errorMessage(props) {
return (
<div class="alert alert-danger" role="alert">
{props.msg}
</div>
)
}
export default class RecordAdmin extends React.Component {
constructor(props) {
super(props)
this.state = {
records: []
}
}
componentDidMount() {
this.loadRecords();
}
loadRecords = () => {
Axios.get(process.env.REACT_APP_API_URL + '/' + this.props.plural)
.then(response => {
this.setState({records: response.data})
}).catch(error => {
console.log(error)
})
}
deleteRecord = (event, recordId) => {
event.preventDefault();
Axios.delete(process.env.REACT_APP_API_URL + '/' + this.props.plural + '/' + recordId).then(response => {
this.loadRecords();
})
}
render() {
// this allows us to pass props to children that are loaded via {this.props.children}
// more on that here: https://medium.com/better-programming/passing-data-to-props-children-in-react-5399baea0356
const TableComponent = this.props.table
return (
<div className="admin-body">
{this.state.errorMessage ? <errorMessage msg={this.state.errorMessage} /> : null}
<Switch>
<Route exact path={`/admin/${this.props.plural}`}>
<div className="admin-menu">
<NavLink className="btn btn-primary" to={`/admin/${this.props.plural}/new`}>New {this.props.singular.charAt(0).toUpperCase() + this.props.singular.slice(1)}</NavLink>
</div>
<TableComponent records={this.state.records} deleteRecord={this.deleteRecord} />
</Route>
<Route exact path={`/admin/${this.props.plural}/new`}>
<NewRecordForm plural={this.props.plural} form={this.props.form} redirectCallback={this.loadRecords}/>
</Route>
</Switch>
</div>
);
}
}
EDIT:
When I throw in a console.log I see that the first <RecordAdmin> that is loaded on page load, is having its records output to the console no matter which <RecordAdmin> instance is currently selected.
render() {
// this allows us to pass props to children that are loaded via {this.props.children}
// more on that here: https://medium.com/better-programming/passing-data-to-props-children-in-react-5399baea0356
const TableComponent = this.props.table
console.log(this.records) // No matter which <RecordAdmin> is currently being displayed, the records will be the records from whichever <RecordComponent was first loaded on page load.
return (
<div className="admin-body">
{this.state.errorMessage ? <errorMessage msg={this.state.errorMessage} /> : null}
<Switch>
<Route exact path={`/admin/${this.props.plural}`}>
<div className="admin-menu">
<NavLink className="btn btn-primary" to={`/admin/${this.props.plural}/new`}>New {this.props.singular.charAt(0).toUpperCase() + this.props.singular.slice(1)}</NavLink>
</div>
{console.log(this.state.records)}
<TableComponent records={this.state.records} deleteRecord={this.deleteRecord} />
</Route>
<Route exact path={`/admin/${this.props.plural}/new`}>
<NewRecordForm plural={this.props.plural} form={this.props.form} redirectCallback={this.loadRecords}/>
</Route>
</Switch>
</div>
);
}
No matter which <RecordAdmin> instance is being displayed, using console.log shows that state is being shared between all 3 <RecordAdmin> instances.

You can use different key for each instance of RecordAdmin and maybe pass exact={true} just to be sure.

Related

Updated state not rendering in child component

I am trying to pull a picture from NASA's API to get their astronomy picture of the day. When I update the state in my API call and console.log this.state.picture, I can see that picture has been set to the data object. But when I try to log picture in render, it logs an empty array. I used a similar approach in previous assignments and never had this issue, so I really can't figure out what I'm doing wrong. I'm new to React, so sorry if the answer is really obvious.
Parent component:
class App extends Component {
state = {
picture: [],
asteroids: [],
}
render() {
return (
<div className="App">
<Route exact path="/" component={Homepage}/>
<Route path="/apod" render={() =>
<APOD picture={this.state.picture}/>}/>
<Route path="/asteroids" render={() =>
<Asteroids asteroids={this.state.asteroids} />} />
</div>
);
}
}
export default App;
Child component:
class Apod extends React.Component{
getApodPicture = e => {
e.preventDefault();
let url = `https://api.nasa.gov/planetary/apod?api_key=v1sEi9chxFYzrf1uXei0J1GvXaemhnQXiDjEcnK2`;
fetch(url)
.then(res => {
if (!res.ok){
throw new Error (res.status)
}
return res.json();
})
.then(data => {
this.setState({picture: data})
console.log(this.state.picture) // logs the data object from NASA
})
.catch(err => console.log(err))
}
render(){
console.log(this.props.picture) // logs []
// console.log(this.state.picture) // returns "Cannot read property 'picture' of null"
return(
<div>
<h1>Astronomy picture of the day!</h1>
<Link to="/">Back to homepage</Link>
<section>
<button onClick={e => this.getApodPicture(e)}>Click to see picture</button>
</section>
</div>
)
}
}
export default Apod;
This.setState will set the state of the component, not the parent component.
you will need a changeHandler to get the desired result.
something along the lines of
class App extends Component {
state = {
picture: '',
asteroids: [],
}
pictureChangeHandler = value => {
this.setState(value);
}
render() {
return (
<div className="App">
<Route exact path="/" component={Homepage}/>
<Route path="/apod" render={() =>
<APOD pictureChangeHandler={this.pictureChangeHandler}
picture={this.state.picture}/>}/>
<Route path="/asteroids" render={() =>
<Asteroids asteroids={this.state.asteroids} />} />
</div>
);
}
}
export default App;
class Apod extends React.Component{
getApodPicture = e => {
e.preventDefault();
let url = `https://api.nasa.gov/planetary/apod?api_key=v1sEi9chxFYzrf1uXei0J1GvXaemhnQXiDjEcnK2`;
fetch(url)
.then(res => {
if (!res.ok){
throw new Error (res.status)
}
return res.json();
})
.then(data => {
this.props.pictureChangeHandler(data)
})
.catch(err => console.log(err))
}
render(){
console.log(this.props.picture) // logs []
// console.log(this.state.picture) // returns "Cannot read property 'picture' of null"
return(
<div>
<h1>Astronomy picture of the day!</h1>
<Link to="/">Back to homepage</Link>
<section>
<button onClick={e => this.getApodPicture(e)}>Click to see picture</button>
</section>
</div>
)
}
}

Undefined prop value in child component

I'm attempting to read an array item in a child component via props. Logging the array in the child component works. But if I try to access a property of one of the array items by indexing it with the :id from match.params, it tells me that I can't access a property of 'undefined'.
Any guidance would be greatly appreciated.
tours.js
import React, { Component } from "react";
import { Route, Switch } from "react-router-dom";
// Page Imports
import Summary from "../pages/summary";
import Details from "../pages/details";
// Component Imports
import Homebutton from "../components/homebutton";
class Tours extends Component {
state = {
tours: []
};
componentDidMount() {
window.scrollTo(0, 0);
fetch("/tours")
.then(res => res.json())
.then(res => this.setState({ tours: res }));
}
render() {
const tours = this.state.tours;
return (
<section className="tours-page">
<div className="center-box">
<h2>Tours</h2>
</div>
<Switch>
<Route
exact
path={this.props.match.url}
render={props => <Summary {...props} tours={tours} />}
/>
<Route
path={this.props.match.url + "/:id"}
render={props => <Details {...props} tours={tours} />}
/>
</Switch>
<Homebutton />
</section>
);
}
}
export default Tours;
details.js
import React from "react";
const Details = ({
tours,
match: {
params: { id }
}
}) => (
<section className="details">
<h2>{tours[id]["name"]}</h2>
</section>
);
export default Details;
To be sure that tours[id] is not undefined you should check it first
<section className="details">
<h2>{tours[id] && tours[id]["name"]}</h2>
</section>
As componentDidMountalways gets called after first render, you must validate your props to avoid app crashes:
const Details = ({
tours,
match: {
params: { id }
}
}) => (
<section className="details">
<h2>{tours.length && tours[id]["name"]}</h2>
</section>
);

search component with react/redux/router flow

I'm trying to complete my app, have learned react, redux, react router all in one, now I'm just confused a bit when it comes to putting it all together.
Say I have a Nav component that's included in a header that's included globally on all pages and it calls a redux action which then runs a reducer and returns some search results.
When one searches from the navigation bar, how do I get it to redirect a search page that then returns the search results?
Nav component
class Nav extends React.Component {
render() {
const { search } = this.props;
return (
<header>
<SearchBox search={search} />
</header>
)
}
}
that includes a search component
class SearchBox extends React.Component {
constructor() {
super();
this.state = {
name: ''
}
}
handleChange = event => {
this.setState({
[event.target.id]: event.target.value
});
}
handleSubmit = event => {
event.preventDefault();
this.props.search(JSON.stringify({name: this.state.name}))
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" id="name" onChange={this.handleChange} placeholder="Search" />
<button type="submit">Search</button>
</form>
)
}
}
my layouts are like
index.js
const Index = () => {
return (
<Nav />
... Home Content
)
}
profile.js
const Profile = () => {
return (
<Nav />
... Profile Content
)
}
search.js
const Users = (props) => {
let list = null;
list = props.users.map((user, index)=> {
const { name } = user.profile;
const { username } = user;
return (
<li key={index}>
<h3><a href={'/'+username}>{name}</a></h3>
</li>
)
});
return <section id="search"><ul>{list}</ul></section>;
}
class Search extends React.Component {
render() {
const { searchResults } = this.props;
return (
<Nav />
<div>
{
/* show 'No results when no results found' */
searchResults !== ''
? seachResults.length == 0
? 'No results found'
: <Users users={searchResults} />
: null
}
</div>
)
}
}
const mapStateToProps = state => ({
searchResults: state.store.searchResults,
});
the user action is
export const search = (name) => dispatch => {
...
dispatch({
type: SEARCH_USER,
payload: res
})
the reducer is
const initialState = {
searchResults: ''
};
case SEARCH_USER:
return {
...state,
searchResults: action.payload.search
}
}
index.js
class App extends React.Component {
render() {
return (
<Router>
<Switch>
<Route path="/" exact={true} component={Index} />
<Route path="/profile" component={Profile} />
<Route path="/search" component={Search} />
</Switch>
</Router>
)
}
}

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

React-router same url not remounting, trying to figure out componentWillReceiveProps

I'm working on my first React app and having some troubles with routing, just looking for some guidance. I see that this is a specific problem for a lot of people, but having trouble following along with other answers.
From what I can tell of other answers, people are assigning keys to specific routes, and checking the key in componentWillReceiveProps(nextProps). I kinda get that, although not sure where to go from there in terms of re-rendering/mounting.
I'm simply trying to transition between the URLs /catalog/genre/:genre and /catalog/genres. Sorry in advance for the messy code, just trying to get it working!
App.js contains main routes, more specifically for this problem:
<Route path="/catalog/genres" component={Genres}/>
Genres.js:
import React, { Component } from 'react';
import {
Link,
BrowserRouter as Router,
Route,
} from 'react-router-dom';
class Genre extends Component {
constructor(props) {
super(props);
console.log(props);
this.state = {
data: [],
};
}
componentDidMount() {
fetch('http://localhost:3000/catalog/genre/58eacca74a0d2c105c68fbe9')
.then(response => response.json())
.then(json => {
this.setState({
data: json.genre_books
});
});
}
componentWillReceiveProps(newProps) {
console.log(newProps.params);
}
render() {
return (
<div>
<div>
Genre:
<label>
<ul>
{
this.state.data.map((piece) =>
<Link key={piece._id} to={`${piece.url}`}>
<li>
{piece.title}
</li>
</Link>
)
}
</ul>
</label>
</div>
</div>
);
}
}
class AllGenres extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
}
componentDidMount() {
fetch('http://localhost:3000/catalog/genres')
.then(response => response.json())
.then(json => {
this.setState({
data: json.genres_list
});
});
}
render() {
return (
<div>
All Genres:
<label>
<ul>
{
this.state.data.map((piece) =>
<Link key={piece._id} to={`${piece.url}`}>
<li>
{piece.name}
</li>
</Link>
)
}
</ul>
</label>
</div>
);
}
}
class Genres extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
}
componentDidMount() {
fetch('http://localhost:3000/catalog/genres')
.then(response => response.json())
.then(json => {
this.setState({
data: json.genres_list
});
});
}
render() {
return (
<div>
<Router>
<div>
<Route path="/catalog/genre/:genre" render={() => (
<Genre testKey='1'/>
)}/>
<Route exact path="/catalog/genres" render={() => (
<AllGenres testKey='2' />
)}/>
</div>
</Router>
</div>
);
}
}
export default Genres;
For people coming back to this problem, although componentWillReceiveProps(nextProps) is useful for other situations, I found a way around this by following this
guide.
Now I'm able to render based on the URL parameter genreId.
Here's the code:
App.js Nav component
class Nav extends Component {
constructor(props){
super(props);
this.state = {
tap: 0,
}
}
render() {
return (
<div >
<Router>
<div style={styles.mainContent}>
<Drawer open="TRUE" title="My App">
<MenuItem primaryText="Home" containerElement={<Link to="/catalog/home" />} />
<MenuItem primaryText="All books" containerElement={<Link to="/catalog/books" />} />
<MenuItem primaryText="All authors" containerElement={<Link to="/catalog/authors" />} />
<MenuItem primaryText="All genres" containerElement={<Link to="/catalog/genres" />} />
<MenuItem primaryText="All book-instances" containerElement={<Link to="/catalog/bookinstances" />} />
<hr></hr>
<MenuItem primaryText="Create new author" containerElement={<Link to="/catalog/author/create" />} />
<MenuItem primaryText="Create new genre" containerElement={<Link to="/catalog/genre/create" />} />
<MenuItem primaryText="Create new book" containerElement={<Link to="/catalog/book/create" />} />
<MenuItem primaryText="Create new book instance (copy)" containerElement={<Link to="/catalog/bookinstance/create" />} />
<hr></hr>
<MenuItem primaryText="About" containerElement={<Link to="/about" />} />
<MenuItem primaryText="Topics" containerElement={<Link to="/topics" />} />
</Drawer>
<Route path="/catalog/home" component={Home}/>
<Route path="/catalog/books" component={Books}/>
<Route path="/catalog/authors" component={Authors}/>
<Route path="/catalog/genres" component={Genres}/>
<Route path="/catalog/bookInstances" component={BookInstances}/>
<Route path="/about" component={About}/>
<Route path="/topics" component={Topics}/>
</div>
</Router>
</div>
);
}
}
Genres.js
import React, { Component } from 'react';
import {
BrowserRouter as Router,
Route,
Link,
} from 'react-router-dom';
class GenreList extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
}
componentDidMount() {
fetch('http://localhost:3000/catalog/genres')
.then(response => response.json())
.then(json => {
this.setState({
data: json.genres_list
});
});
}
render() {
return (
<div>
All Genres:
<label>
<ul>
{
this.state.data.map((piece) =>
<Link key={piece._id} to={`${piece.url}`}>
<li>
{piece.name}
</li>
</Link>
)
}
</ul>
</label>
</div>
);
}
}
const Genre = ({match}) => (
<div>
<h3>{match.params.genreId}</h3>
</div>
)
const Genres = ({ match }) => {
return (
<div>
<Route path={`${match.url}/:genreId`} component={Genre}/>
<Route exact path={match.url} render={() => (
<div>
<GenreList url={match.url}/>
</div>
)}/>
</div>
);
}
export default Genres;
genre.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var GenreSchema = Schema({
name: { type: String, required: true, min: 3, max: 100 },
}, {
toObject: {
virtuals: true
},
toJSON: {
virtuals: true
} //reference to the associated book
});
// Virtual for bookinstance's URL
GenreSchema
.virtual('url')
.get(function () {
return '/catalog/genres/' + this._id;
});
//Export model
module.exports = mongoose.model('Genre', GenreSchema);

Resources