Search bar functionality for book API in React - reactjs

I have a BooksAPI file that contains the following search method:
export const search = (query) =>
fetch(`${api}/search`, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json'
},
body: JSON.stringify({ query })
}).then(res => res.json())
.then(data => data.books)
I'm trying to make it so that results start showing up when I type into the search bar, and I also want no books to show when the search bar is empty.
I have it working so that I can display results with the initial search, but if I try to backspace to type in a new term, I get a cannot read property map of undefined error, which makes sense, but I'm not sure how to address this. I can only do a new search if I refresh the page.
Any help is greatly appreciated.
This is what I have so far for App.js:
class BooksApp extends React.Component {
state = {
books: []
}
componentDidMount() {
BooksAPI.getAll()
.then((books) => {
this.setState(() => ({
books
}))
})
}
render() {
return (
<div className="app">
<div className="list-books">
<div className="list-books-title">
<h1>MyReads</h1>
</div>
<Route exact path="/" render={() => (
<BookList
books={this.state.books}
/>
)} />
<Route path="/search" component={Search} />
<Route path="/search" render={({ history }) => (
<Search
onSearch={(query) => {
this.search(query)
history.push('/')
}}
/>
)} />
<div className="open-search">
<Link to="/search">Add a book</Link>
</div>
</div>
</div>
)
}
}
export default BooksApp
And here is my Search component:
class Search extends Component {
state = {
query: '',
books: []
}
search = (query) => {
BooksAPI.search()
.then((books) => {
this.setState(() => ({
query,
books
}))
})
}
render() {
const { query, books } = this.state
return (
<div className="search-books">
<div className="search-books-bar">
<Link
className="close-search"
to='/'>Close
</Link>
<div className="search-books-input-wrapper">
<input
type="text"
onChange={(event) => this.search(event.target.value)}
placeholder="Search by title or author"
value={query}
/>
</div>
</div>
<div className="search-books-results">
<ol className="books-grid">
{books.map((book) => (
<li>
<div>
<p>{book.title}</p>
<p>{book.author}</p>
</div>
</li>
))}
</ol>
</div>
</div>
)
}
}
export default Search;

I had similar functionality I wanted to implement recently, I would recommend going to a library such as react-select to solve this problem rather than reinventing the wheel.

Related

Remove item from array react js

I'm having trouble with removing an object from an array. I have tried many options and nothing works.
Please help me solve this problem.
Thanks for your help
My array:
export const SavedList = [
{
word: 'hello',
translate: 'привет',
note: 'say say'
}
]
Remove callback (deletePost)
class Menu extends Component {
state = {
word: '',
translate: '',
note: '',
data: SavedList,
}
deletePost =(id)=>{
this.setState({
data : this.state.data.filter((el)=> el.id !== id)
})
}
render() {
return (
<div className="content">
<Routes>
<Route path="/" element={<Layout />}>
<Route path="saved" element={<Saved data={this.state.data} del={this.deletePost}/>}/>
</Route>
</Routes>
<div>
</div>
</div>
);
}
}
export default Menu;
here button delete with onClick
class Saved extends Component {
render() {
const sl = this.props.data.map((sl, id) => {
return (
<div className="saved-card" key={id}>
<div className="content">
<p>{id}</p>
<p>{sl.word}</p>
<p>{sl.translate}</p>
<p>{sl.note}</p>
</div>
<div className="btn-block">
<button
onClick={() => this.props.del(id)}
type="button"
className="delete-btn"
>
delete
</button>
</div>
</div>
);
});
return (
<div>
<h2>Saved list</h2>
<div className="saved-inner">
<div className="saved-list">{sl}</div>
</div>
</div>
);
}
}
export default Saved;
This issue is because el.id doesn't have any value in delete Post Function. That function also needs to have an ID variable, I have named it as i_di.
class App extends Component {
state = {
word: '',
translate: '',
note: '',
data: SavedList,
}
deletePost =(id)=>{
this.setState({
data : this.state.data.filter((el, i_di)=> i_di !== id )
})
}
render() {
return (
<div className="content">
{<Saved data={this.state.data} del={this.deletePost}/>}
</div>
);
}
}
export default App;

Problem to render a component dynamically with React Router

I'm working on a personal website and I have a problem rendering a component dynamically using React Router. To me, everything seems correct but for some reason, it's not working.
I tried to follow the documentation and watched a couple of tutorials but I have been stuck for a long time so I feel like I need help on this one.
In this component, I want to render the 'Articles' component dynamically using the id
class JobCard extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentWillMount() {
fetch(url)
.then(data => data.json())
.then(result =>
this.setState({
data: result
})
);
}
render() {
const { match } = this.props;
const { data, value } = this.state;
return (
<>
<div>
{results.map((job, id) => (
<div key={id}>
<div key={job._id} className="blog-card">
<div className="meta">
<div className="photo">
<img src={job.img} alt="logo" />
</div>
</div>
<div className="description">
<h5>{job.position_name}</h5>
<p className="read-more">
<p>{job.location}</p>
<p>
<span className="learn-pow">
{" "}
<Link
to={{
pathname: `${match.url}/${job._id}`,
state: job
}}
>
{job.workplace_name}
</Link>{" "}
Enter Location
</span>
</p>
</p>
</div>
</div>
</div>
))}
</div>
}
<Route path={`${match.path}/:id`} component={Articles} />
</>
);
}
}
export default JobCard;
And here is the component that i want to render:
import React from 'react';
const Articles = ({ location }) => (
<div>
<h1>{location.state.name}</h1>
<h2>{location.state.email}</h2>
<h2>{location.state.place}</h2>
</div>
)
export default Articles;
When I click on the Card the URL is right so I get the id but I don't have access to the Article component. I tried to console log but nothing appears.
Instead of this
<Route path={`${match.path}/:id`} component={Articles} />
try doing this
<Route path={`${match.url}/:id`} component={Articles} />

React app state not updating

I am creating a basic React app to hold books on certain shelves and am trying to create the functionality to move books between shelves.
The problem I have is that when I select the new target shelf from the book objects dropdown, the onUpdateShelf method in ListBooks.js does not seem to initiate the update and subsequent state change.
I am new to React, my understanding is that calling the setState function in updateShelf should trigger the re-render with the updated object.
My question then is, is my implementation wrong and where?
App.js
import React, { Component } from 'react'
import ListBooks from './ListBooks'
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) => {
console.log(book)
console.log(shelf)
this.books.forEach(b => {
if(b.id === book.id && b.shelf !== book.shelf ) {
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}
/>
)} />
</div>
)
}
}
export default BooksApp
And my ListBooks.js
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import './App.css'
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
}
state = {
showSearchPage: false,
query: ''
}
render() {
const { books, onUpdateShelf } = this.props
function getBooksForShelf(shelfKey) {
return books.filter(book => book.shelf === shelfKey);
}
console.log(books);
return(
<div className="app">
{this.state.showSearchPage ? (
<div className="search-books">
<div className="search-books-bar">
<a className="close-search" onClick={() => this.setState({ showSearchPage: false })}>Close</a>
<div className="search-books-input-wrapper">
{/*
NOTES: The search from BooksAPI is limited to a particular set of search terms.
You can find these search terms here:
https://github.com/udacity/reactnd-project-myreads-starter/blob/master/SEARCH_TERMS.md
However, remember that the BooksAPI.search method DOES search by title or author. So, don't worry if
you don't find a specific author or title. Every search is limited by search terms.
*/}
<input type="text" placeholder="Search by title or author"/>
</div>
</div>
<div className="search-books-results">
<ol className="books-grid"></ol>
</div>
</div>
) : (
<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) => (
<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>
<option value="none" disabled>Move to...</option>
<option value="currentlyReading" onClick={() => onUpdateShelf(book, 'currentlyReading')} >Currently Reading</option>
<option value="wantToRead" onClick={() => onUpdateShelf(book, 'wantToRead')} >Want to Read</option>
<option value="read" onClick={() => onUpdateShelf(book, 'read')} >Read</option>
<option value="none" onClick={() => onUpdateShelf(book, '')} >None</option>
</select>
</div>
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.author}</div>
</div>
))}
</li>
</ol>
</div>
</div>
)) }
</div>
</div>
<div className="open-search">
<a onClick={() => this.setState({ showSearchPage: true })}>Add a book</a>
</div>
</div>
)}
</div>
)
}
}
export default ListBooks
When you passe updateShelf to your component ListBooks, you lose the value of this inside updateShelf, and as a result this.books will be undefined.
You can solve this by, either doing this inside the constructor of BooksApp :
this.updateShelf = this.updateShelf.bind(this)
Or by using arrow functions:
<Route exact path='/' render={() => (
<ListBooks
books={this.state.books}
onUpdateShelf={() => { this.updateShelf()} }
/>
)} />
EDIT
You are already using arrow functions inside BooksApp, so what I said before isn't necessary.
But still, you should use this.state.books and not this.books inside updateShelf.

React Router does not update component

I got a component here which should display gifs from giphy on a route base search term:
public render(){
return(
<div>
<Router>
<div>
<Route path='/:term' component={this.dispGiphy} />
</div>
</Router>
</div>
)
}
private readonly dispGiphy = (props) => {
this.getGifs(props.match.params.term)
return(
this.state.items.map(data => (
<img className="images" src={data.images.downsized.url} />
)))
}
public getGifs(inputValue){
// Get Request to the API and write the result to this.state.items
fetch("https://api.giphy.com/v1/gifs/search?limit=3&api_key=KEY_GOES_HERE&q=" + inputValue)
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.data
});
},
(error) => {
this.setState({
error,
isLoaded: true
});
}
)
}
I also got a list, listing previous search terms:
public render(){
return(
<div>
<ul id="ul_LeftList">
{this.state.terms.map((term) => (
<li key={term}>
<Link to={`${term}`}>{term} </Link><button className="fa fa-cancel btnDelete" onClick={this.deleteElement(term)}
>X</button>
</li>
))}
</ul>
</div>
)
}
These two components are bound together in the app.tsx
return(
<div>
<div><header className="App-header">
<input type="text" placeholder="Search..." onChange={this.searchChanged} />
<button type="submit" className="fa fa-search searchButton" onClick={this.process} />
</header>
</div>
<Router>
<p className="leftList">
<LeftList terms={this.state.searchterms} />
</p>
</Router>
<p className="App-content">
<Router>
<Route path='/:term' component={Giphy} />
</Router>
<Giphy />
</p>
</div>
)
Now, when I click on a Link in the LeftList, I expect two things:
1. The Route will be updated with the search term I just clicked (working)
2. The Giphy component shows up the new gifs relating to the search term from the route (not working)
Actually I don't know what the problem is.

Route within list-item

I want to render a Route inside a list item. When I click on the Link the li shall open and reveal the child component. I have tried this simple solution:
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<ul>
{
topics.map(topic => (
<li key={topic.id}>
<Link to={`${match.url}/${link.path}`}>
{topic.title}
</Link>
<Route path={`${match.path}/${topic.path}`}
component={Topic} />
</li>
))
}
</ul>
</div>
)
It works, but I'm not sure this is proper coding. Another variant I wrote is this:
class Topics extends Component {
state = { open: false }
handleClick = value => {
this.setState({ open: value })
}
render () {
const { match } = this.props
const { open } = this.state
return (
<div>
<h1>Topics</h1>
<ul>
{ topics.map(({ name, id }) => (
<li key={id}
className={(open===id) ? 'open' : null}>
<Link to={`${match.url}/${id}`}
onClick={() => this.handleClick(id)}>
{name}
</Link>
<div>
{
open===id ?
<Route path={`${match.path}/:topicId`}
render={({ match }) => (
<Topic
topic={topics.find(({ id }) =>
id === match.params.topicId)}
match={match}
/>)}
/> : null
}
</div>
</li>
))}
</ul>
</div>
)
}
}
Is anyone of these solutions good and proper? Any other suggestions on how to solve this?
I would recommend you define a new component for the Route and use the children property.
Do you mean something like this?
const ListItemLink = ({ to, ...rest }) => (
<Route path={to} children={({ match }) => (
<li className={match ? 'active' : ''}>
{ log(match) }
<Link to={to} {...rest}>
{ to }
</Link>
{ match ?
<h1>{match.url}</h1> : null }
</li>
)} />
)
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<ul>
{
LinksArray.map(link => (
<ListItemLink key={link.id}
to={`${match.url}/${link.path}`} />
))
}
</ul>
</div>
)

Resources