I am learning React.
I am working on a Myreads book app project. My app renders fine at the start but the moment I click on the select tag, i get the above error message thrown up.
I thought it was a 'propTypes' issue at first and went ahead and installed prop-types but it is still not working.
Here is my cmp where there error occurs and my parent cmp where my method is defined:
// child component
import React from "react";
import Icon from "./icons/add.svg";
import SearchBar from "./SearchBar";
class Book extends React.Component {
render() {
return (
<div className="book">
<div className="book-top">
<div
className="book-cover"
style={{
width: 128,
height: 193,
backgroundImage: `${
this.props.book.imageLinks
? "url(this.props.book.imageLinks.thumbnail)"
: "Icon"
}`
}}
/>
<div className="book-shelf-changer">
<select
onChange={event =>
this.props.moveShelf(this.props.book, event.target.value)
}
value={this.props.book.shelf}
>
<option value="move" disabled>
Move to...
</option>
<option value="currentlyReading">Currently Reading</option>
<option value="wantToRead">Want to Read</option>
<option value="read">Read</option>
<option value="remove">Remove</option>
</select>
</div>
</div>
<div className="book-title">{this.props.book.title}</div>
<div className="book-authors">{this.props.book.authors}</div>
</div>
);
}
}
export default Book;
And the parent component, where method is defined:
// parent component
import React from "react";
import SearchBar from "./SearchBar";
import MainPage from "./MainPage";
import { Route } from "react-router-dom";
import "./App.css";
import * as BooksAPI from "./BooksAPI";
class BooksApp extends React.Component {
constructor(props) {
super(props);
this.moveShelf = this.moveShelf.bind(this);
this.state = {
books: [],
query: "",
searchedBooks: []
};
}
componentDidMount() {
BooksAPI.getAll().then(books => {
this.setState({ books: books });
});
}
moveShelf = (book, shelf) => {
BooksAPI.update(book, shelf); //call update method here to stack books
BooksAPI.getAll().then(books => {
//to have books update dynamically -->
this.setState({ books: books });
});
};
updateQuery = query => {
this.setState({
query: query
});
this.updateSearchedBooks(query);
};
updateSearchedBooks = query => {
query
? BooksAPI.search(query).then(searchedBooks => {
/*fetch searchedbooks using the method defined in BooksAPI */
searchedBooks.error
? this.setState({ searchedBooks: [] })
: this.setState({ searchedBooks });
})
: this.setState({ searchedBooks: [] });
};
render() {
return (
<div className="app">
<Route
exact
path="/"
render={() => (
<MainPage
books={this.state.books} //calling all my books
moveShelf={this.moveShelf}
/>
)}
/>
<Route
path="/search"
render={() => (
<SearchBar
searchedBooks={this.state.searchedBooks}
moveShelf={this.moveShelf}
/>
)}
/>
</div>
);
}
}
export default BooksApp;
I guess that you do not pass moveShelf to Book inside MainPage and/or SearchBar components.
You do not need this.moveShelf = this.moveShelf.bind(this); when you define it as an arrow function moveShelf = (book, shelf) => {...}
Related
I am refactoring a small app that organises books onto respective shelves. I have extracted the JSX into a Book.js file to handle rendering each book object and call the updateShelf method to move books between shelves.
To that end, I have exported the updateShelf function from App.js and imported it into the Book.js component which is embedded in the ListBooks which uses the map function to pass each book object to Book to be rendered.
The issue I have is that when the onChange handler in Book fires I get the below exception:
TypeError: Object(...) is not a function
17 | <div className="book-top">
18 | <div className="book-cover" style={{ width: 128, height: 193, backgroundImage: `url(${book.imageLinks.thumbnail})` }}></div>
19 | <div className="book-shelf-changer">
> 20 | <select value={book.shelf} onChange={(e) => updateShelf(book, e.target.value)}>
21 | <option value="none" disabled >Move to...</option>
22 | <option value="currentlyReading" >Currently Reading</option>
23 | <option value="wantToRead" >Want to Read</option>
I am not sure what it is that I am doing wrong.
I've included quite a lot because I'm not sure exactly where the issue issue's source is. Let me know if I need to trim down the post.
App.js
import React, { Component } from 'react'
import ListBooks from './ListBooks'
import SearchBooks from './SearchBooks'
import * as BooksAPI from './utils/BooksAPI'
import { Route } from 'react-router-dom'
class BooksApp extends Component {
state = {
books: []
}
componentDidMount() {
BooksAPI.getAll()
.then((books) => {
this.setState(() => ({
books
}))
})
}
updateShelf(book, shelf) {
this.state.books.forEach(b => {
if(b.id === book.id && shelf !== '' && shelf !== 'none') {
b.shelf = shelf
this.setState((currentState) => ({
books: currentState.books
}))
BooksAPI.update(book, shelf)
}
});
}
render() {
return (
<div>
<Route exact path='/' render={() => (
<ListBooks
books={this.state.books}
onUpdateShelf={this.updateShelf}
/>
)} />
<Route exact path='/search' render={() => (
<SearchBooks
books={this.state.books}
/>
)} />
</div>
)
}
}
export default BooksApp
export const updateShelf = updateShelf;
ListBooks.js
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import './App.css'
import { Link } from 'react-router-dom'
import Book from './Book'
const shelves = [
{
key: 'currentlyReading',
name: 'Currently Reading'
},
{
key: 'wantToRead',
name: 'Want To Read'
},
{
key: 'read',
name: 'Read'
}
];
class ListBooks extends Component {
static propTypes = {
books: PropTypes.array.isRequired
}
render() {
const { books } = this.props
function getBooksForShelf(shelfKey) {
return books.filter(book => book.shelf === shelfKey);
}
return(
<div className="app">
<div className="list-books">
<div className="list-books-title">
<h1>My Reads</h1>
</div>
<div className="list-books-content">
<div>
{ shelves.map((shelf) => (
<div key={shelf.key} className="bookshelf">
<h2 className="bookshelf-title">{shelf.name}</h2>
<div className="bookshelf-books">
<ol className="books-grid">
<li>
{ getBooksForShelf(shelf.key).map((book) => (
<Book key={book.id} book={book}/>
))}
</li>
</ol>
</div>
</div>
)) }
</div>
</div>
<Link
to='/search'
className="open-search">
Find a Book
</Link>
</div>
}
</div>
)
}
}
export default ListBooks
Book.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { updateShelf } from './App'
class Book extends Component {
static propTypes = {
book: PropTypes.object.isRequired
}
render() {
const { book } = this.props
return(
<div key={book.id} className="book">
<div className="book-top">
<div className="book-cover" style={{ width: 128, height: 193, backgroundImage: `url(${book.imageLinks.thumbnail})` }}></div>
<div className="book-shelf-changer">
<select value={book.shelf} onChange={(e) => updateShelf(book, e.target.value)}>
<option value="none" disabled >Move to...</option>
<option value="currentlyReading" >Currently Reading</option>
<option value="wantToRead" >Want to Read</option>
<option value="read" >Read</option>
<option value="none" >None</option>
</select>
</div>
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
)
}
}
export default Book
You're exporting export const updateShelf = updateShelf. But updateShelf is a part of the Component, so it's most likely undefined. You should move the method outside of the class if you plan to export it, or pass it down as a prop if you expect it to change the component.
I have a problem. I'm trying do the method Render Prop but it not is working.
My project is: It has to render some names of ComponentDidMount, and I can get it to do the filter and to filter the names. But I passed the function filter for a component, and do the Render Prop.
I pass it here:
import React from 'react';
import './Body.css';
import { Link } from "react-router-dom";
import axios from 'axios';
import Filter from './Filter';
class Body extends React.Component {
constructor(props) {
super(props);
this.state = {
employee: []
}
}
componentDidMount() {
axios
.get("http://127.0.0.1:3004/employee")
.then(response => this.setState({ employee: response.data }));
}
getName = (filter) => {
const { employee, add } = this.state;
return employee.filter(employee => employee.name.includes(filter)).map(name => (
<div className='item' key={name.id}>
<Link className="link" to={`/user/${name.id}`}>
<div key={name.id}>
<img className="img" alt="imgstatic"
src={`https://picsum.photos/${name.id}`}
/>
</div>
<h1 className="name2"> {name.name} </h1>
</Link>
</div>
));
};
getValueInput = (evt) => {
const inputValue = evt.target.value;
this.setState({ input: inputValue });
}
render() {
return (
<div>
<h4 className="manager"> Hello {this.props.currentManager}, here be all employees available for change. </h4>
<div className="body">
{this.getName()}
</div>
<div className='input'>
<Filter render={this.getName} />
</div>
</div>
)
}
}
export default Body;
And here I get him:
import React from 'react';
class Filter extends React.Component {
constructor() {
super();
this.state = {
input: ''
}
}
getValueInput = (evt) => {
const inputValue = evt.target.value;
this.setState({ input: inputValue });
console.log();
console.log(this.state.input)
}
render() {
return (
<div>
<input placeholder='Search name here' type="text" onChange={this.getValueInput} />
</div>
)
}
}
export default Filter
But something's not working...
Can someone help me?
You are not at all using the render prop being supplied to the Filter component. Also the objective of render prop is to render the data, go using this.getName() inside the render Body Component isn't correct either(for one you are not passing the filter value to the getName). You would use it like
import React from 'react';
import './Body.css';
import { Link } from "react-router-dom";
import axios from 'axios';
import Filter from './Filter';
class Body extends React.Component {
constructor(props) {
super(props);
this.state = {
employee: []
}
}
componentDidMount() {
axios
.get("http://127.0.0.1:3004/employee")
.then(response => this.setState({ employee: response.data }));
}
getName = (filter) => {
const { employee, add } = this.state;
return employee.filter(employee => employee.name.includes(filter)).map(name => (
<div className='item' key={name.id}>
<Link className="link" to={`/user/${name.id}`}>
<div key={name.id}>
<img className="img" alt="imgstatic"
src={`https://picsum.photos/${name.id}`}
/>
</div>
<h1 className="name2"> {name.name} </h1>
</Link>
</div>
));
};
getValueInput = (evt) => {
const inputValue = evt.target.value;
this.setState({ input: inputValue });
}
render() {
return (
<div>
<h4 className="manager"> Hello {this.props.currentManager}, here be all employees available for change. </h4>
<div className='body'>
<Filter render={this.getName} />
</div>
</div>
)
}
}
export default Body;
and Filter as
import React from 'react';
class Filter extends React.Component {
constructor() {
super();
this.state = {
input: ''
}
}
getValueInput = (evt) => {
const inputValue = evt.target.value;
this.setState({ input: inputValue });
console.log();
console.log(this.state.input)
}
render() {
return (
<React.Fragment>
{this.props.render(this.state.input)}
<div className='input'>
<input placeholder='Search name here' type="text" onChange={this.getValueInput} />
</div>
</React.Fragment>
)
}
}
Note React.Fragment is available from v16.2.0 onwards and if you are not using the relevant version replace React.Fragment with <div>
I have a basic React app with a function in the App.js component to pass down as a prop for subsequent components to handle moving a book between shelves.
When I pass the function down one level, from App.js to ListBooks.js to be passed on the Books.js component which is where the user will select the new shelf and trigger the API call and state update, this works fine.
However when I then try to do the same from App.js > SearchBooks.js > Book.js it does not seem to work.
What I expect is for the updateShelf function to be called to update the book and the state.
Apologies for 'wall of code' if there's too much there, just not sure exactly where the issue is.
EDIT:
As suggested in comments, here is a CodeSandbox version:
https://codesandbox.io/s/github/richardcurteis/myreads-udacity
App.js
import React, { Component } from 'react'
import ListBooks from './ListBooks'
import SearchBooks from './SearchBooks'
import * as BooksAPI from './utils/BooksAPI'
import { Route } from 'react-router-dom'
class BooksApp extends Component {
state = {
books: []
}
componentDidMount() {
BooksAPI.getAll()
.then((books) => {
this.setState(() => ({
books
}))
})
}
updateShelf = (book, shelf) => {
this.state.books.forEach(b => {
if(b.id === book.id) {
b.shelf = shelf
this.setState((currentState) => ({
books: currentState.books
}))
BooksAPI.update(book, shelf)
}
});
}
render() {
return (
<div>
<Route exact path='/' render={() => (
<ListBooks
books={this.state.books}
onUpdateShelf={this.updateShelf}
/>
)} />
<Route exact path='/search' render={() => (
<SearchBooks
onUpdateShelf={this.updateShelf}
/>
)} />
</div>
)
}
}
export default BooksApp
SearchBooks.js
import React, { Component } from 'react'
import * as BooksAPI from './utils/BooksAPI'
import Book from './Book';
export default class SearchBooks extends Component {
state = {
query: '',
books: []
}
updateQuery(query) {
this.setState(() => ({
books: [],
query: query
}))
this.bookSearch(query)
}
bookSearch(e) {
if (e.length > 0) BooksAPI.search(e)
.then(books => this.setState(currentState => ({
books: books
})));
}
render() {
const { query, books } = this.state
const { onUpdateShelf } = this.props
return(
<div className="search-books">
<div className="search-books-bar">
<a className="close-search" >Close</a>
<div className="search-books-input-wrapper">
<input
type="text"
placeholder="Search by title, author or subject"
value={query}
onChange={(event) => this.updateQuery(event.target.value)}
/>
</div>
</div>
<div className="search-books-results">
<ol className="books-grid">
<li>
{ !books.error ? (
books.map((book) => (
<Book
key={book.id}
book={book}
updateShelf={onUpdateShelf}
/>
))
) : (
<h4>"{query}", is not a valid search</h4>
)}
</li>
</ol>
</div>
</div>
)
}
}
Book.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class Book extends Component {
static propTypes = {
book: PropTypes.object.isRequired
}
render() {
const { book, updateShelf } = this.props
return(
<div key={book.id} className="book">
<div className="book-top">
<div className="book-cover" style={{ width: 128, height: 193, backgroundImage: `url(${book.imageLinks.thumbnail})` }}></div>
<div className="book-shelf-changer">
<select value={book.shelf ? book.shelf : 'none'} onChange={(e) => updateShelf(book, e.target.value)}>
<option disabled >Move to...</option>
<option value="currentlyReading" >Currently Reading</option>
<option value="wantToRead" >Want to Read</option>
<option value="read" >Read</option>
<option value="none" >None</option>
</select>
</div>
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
)
}
}
export default Book
Just for reference:
ListBooks.js
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import './App.css'
import { Link } from 'react-router-dom'
import Book from './Book'
const shelves = [
{
key: 'currentlyReading',
name: 'Currently Reading'
},
{
key: 'wantToRead',
name: 'Want To Read'
},
{
key: 'read',
name: 'Read'
}
];
class ListBooks extends Component {
static propTypes = {
books: PropTypes.array.isRequired,
onUpdateShelf: PropTypes.func.isRequired
}
render() {
const { books, onUpdateShelf } = this.props
function getBooksForShelf(shelfKey) {
return books.filter(book => book.shelf === shelfKey);
}
return(
<div className="app">
<div className="list-books">
<div className="list-books-title">
<h1>My Reads</h1>
</div>
<div className="list-books-content">
<div>
{ shelves.map((shelf) => (
<div key={shelf.key} className="bookshelf">
<h2 className="bookshelf-title">{shelf.name}</h2>
{ getBooksForShelf(shelf.key).length === 0 ? (
<div>
<h4>No books in this shelf</h4>
</div>
) : (
<div className="bookshelf-books">
<ol className="books-grid">
<li>
{ getBooksForShelf(shelf.key).map((book) => (
<Book key={book.id}
book={book}
updateShelf={onUpdateShelf}/>
))}
</li>
</ol>
</div>
)}
</div>
)) }
</div>
</div>
<Link
to='/search'
className="open-search">
Find a Book
</Link>
</div>
</div>
)
}
}
export default ListBooks
As we discussed it's not a problem with the prop passing but rather with updating the state. Here is the modified code that seems to work:
updateShelf = (book, shelf) => {
const bookFromState = this.state.books.find(b => b.id === book.id);
if (bookFromState) {
// update existing
bookFromState.shelf = shelf;
this.setState(currentState => ({
books: currentState.books
}));
BooksAPI.update(book, shelf);
} else {
// add new one
this.setState(prevState => ({ books: [...prevState.books, book] }));
BooksAPI.update(book, shelf);
}
};
Or better without mutating the state:
updateShelf = (book, shelf) => {
this.setState(prevState => {
const booksCopy = prevState.books.filter(b => b.id !== book.id);
booksCopy.push({ ...book, shelf });
return { books: booksCopy }
});
BooksAPI.update(book, shelf);
};
could anyone tell me why is that won't work? Proper data is displaying in the console (console.log(this.state);), but it won't be transfered to MainContainer.
Same data initialized in the constructor>state>users working without issues. Where's the problem?
App
import React, {Component} from 'react';
import logo from './logo.svg';
import './App.css';
import Header from './components/header/Header';
import MainContainer from './containers/main-container/MainContainer';
class App extends Component {
constructor(props) {
super(props);
this.state = {
users: []
}
}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(users => {
let u = users.map((user) => {
return {id: user.id, name: user.name, email: user.email}
})
return u;
})
.then(u => {
this.setState({users: u});
console.log(this.state);
});
}
render() {
return (
<div className="App">
<Header/>
<MainContainer users={this.state.users}/>
</div>
)
}
}
export default App;
MainContainer
import React from 'react';
import ActionBar from '../../components/action-bar/ActionBar'
import ListHeader from '../../components/list-header/ListHeader'
import ListItem from '../../components/list-item/ListItem'
import ListItemPlaceholder from '../../components/list-item-placeholder/ListItemPlaceholder'
class MainContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
users : props.users
}
}
render() {
const list = this.state.users.map(
(user) =>
{
const liStyle = {
'background-color': user % 2 == 0 ? '#fbfcfc' : 'transparent',
};
return <ListItem key={user.id} style={liStyle} id={user.id} name={user.name} email={user.email}/>
}
);
return (
<div className={'main-container'}>
<ActionBar />
<ListHeader />
{list}
</div>
)
}
}
export default MainContainer;
.................................................................................................................
Best Regards!
crova
In your <MainContainer> component you store the users in its state in the constructor but you never alter it. You only need to use state when the component needs to alter it during its lifetime. But the users come from it's parent via the users prop which you never render. So just render that prop instead:
const MainContainer = props => (
<div className="main-container">
<ActionBar />
<ListHeader />
{props.users.map(({id, name, email}) => (
<ListItem
key={id}
style={{
backgroundColor: id % 2 === 0 ? '#fbfcfc' : 'transparent'
}}
id={id}
name={name}
email={email}
/>
))}
</div>
);
When the users change in the parent it will re-render and pass the new users array to the <MainContainer>.
Also note that if your component only renders props and has no own state it can be written as a stateless functional component.
I'm having trouble understanding how to pass state as props to other child components in React. In my code, you can see I've got a component that takes input and maps it to my state array, displaying part of that data in another component, that's working just fine.
But the overall goal is that when a user clicks on an item they've added to the list, React Router kicks in and changes the view to the MovieDetails component, which will have extra information they've entered, like title, date and description.
I haven't even gotten to setting up react router because I can't seem to properly access state within the MovieDetails component. And then I'm not quite sure how to display the correct MovieDetails component with router.
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
import './App.css';
class App extends Component {
constructor() {
super();
this.addMovie = this.addMovie.bind(this);
this.state = {
movies : []
};
}
addMovie(movie) {
let movies = this.state.movies;
movies.push(movie);
this.setState({ movies });
}
render() {
return (
<div className="wrapper">
<div className="container">
<div>
<h3 className="heading">Favorite Movies</h3>
</div>
</div>
<div>
<AddMovie addMovie={ this.addMovie }/>
<MovieList movies={ this.state.movies }/>
</div>
</div>
)
}
}
class AddMovie extends Component {
addMovie(event) {
event.preventDefault();
const movie = {
title : this.title.value,
year : this.year.value,
image : this.image.value,
desc : this.desc.value
}
this.props.addMovie(movie);
this.movieForm.reset();
}
render() {
return (
<div className="container">
<form ref={(input) => this.movieForm = input} onSubmit={(e) => this.addMovie(e)}>
<input ref={(input) => this.title = input} className="Input" type="text" placeholder="Title"/>
<input ref={(input) => this.year = input} className="Input" type="text" placeholder="Year"/>
<textarea ref={(input) => this.desc = input} className="Input" type="text" placeholder="Description"></textarea>
<input ref={(input) => this.image = input} className="Input" type="text" placeholder="Poster URL"/>
<button type="submit">Add</button>
</form>
</div>
)
}
}
class MovieList extends Component {
render() {
return (
<div>
{ this.props.movies.map( (movie, i) => <MovieListItem key={i} details={ movie }/> )}
</div>
);
}
}
class MovieListItem extends Component {
constructor(props) {
super(props);
this.toggleClass = this.toggleClass.bind(this);
this.state = {
active: false
};
}
toggleClass() {
const currentState = this.state.active;
this.setState({ active: !currentState });
}
render() {
const { details } = this.props;
return (
<div
className={this.state.active ? "red": null}
onClick={this.toggleClass}
>
<img src={details.image} alt=""/>
<hr/>
</div>
)
}
}
class MovieDetails extends Component {
render() {
return (
<div>
<p>title here</p>
<p>year here</p>
<p>description here</p>
<img src="image" alt=""/>
</div>
)
}
}
export default App;
The problem come from the way you try to access the input values. When you use ref, you get a React wrapper, not the real DOM element, so you can't access directly to .value or .reset(). You have to use the getDOMNode() method to get the DOM element. This worked for me :
const movie = {
title : this.title.getDOMNode().value,
year : this.year.getDOMNode().value,
image : this.image.getDOMNode().value,
desc : this.desc.getDOMNode().value
};
...
this.movieForm.getDOMNode().reset();
An other thing, when you setState something that uses the current state, you should use the callback instead :
addMovie(newMovie) {
this.setState(({movies: prevMovies})=> ({
movies: [...prevMovies, newMovie]
}));
}
See complete setState API from official doc
If I got it right, do you want to push to a new component (where the details should be accessible) when you're clicking on an item created from MovieList? If so, here are the steps you have to do:
If you want to push a new view you have to use something like browserHistory or hashHistory from 'react-router'. In this case I'll use browserHistory.
To access the state in MovieDetails component simply pass it through browserHistory.
Here is the way I used your code to push to a new view when an item from MovieList component is clicked:
import {Router, Route, browserHistory} from "react-router";
class Routes extends Component {
render() {
let props = this.props;
return (
<Router history={browserHistory}>
<Route path="/" component={App}/>
<Route path="/movie-details" component={MovieDetails}/>
</Router>
)
}
}
// Here is your App component
class App extends Component {
// ... your code
}
// ... your other classes
class MovieListItem extends Component {
// ... Constructor
// Here I'm pushing the new route for MovieDetails view
toggleClass(details) {
browserHistory.push({
pathname: '/movie-details',
state: details // pass the state to MovieDetails
});
// ... your code
}
render() {
const {details} = this.props;
return (
<div
// ... your code
onClick={this.toggleClass.bind(this, details)} // pass details to toggleClass()
>
// ... your code
</div>
)
}
}
// Here is your Movie Details component
class MovieDetails extends Component {
console.log('This props: ', this.props.location.state); // The details object should be logged here
// ... your code
}
// Export Routes instead of App
export default Routes;
Hope that helps!