I set up a search bar, and after I search the results will pop up. However, the issue is, if I don't refresh the page and search again, it will push me to the new search, but the search results won't update with it. Why would the updated param be showing even though the results aren't updating?
Ex. first url is search/erl,second url is search/Groovy%20Playlist
First search
Second search, query param updated, but search results didn't
Searchbar.js
class SearchBar extends Component {
constructor(props) {
super(props)
this.state = {query: '', results: [], isLoading: false}
}
componentWillMount() {
this.resetComponent()
}
resetComponent = () => this.setState({ isLoading: false, results: [], query: '' })
search(query) {
this.setState({ query });
axios
.get(`/api/search?query=${query}`)
.then(response => {
this.setState({ results: response.data});
})
.catch(error => console.log(error));
}
handleFormSubmit = () => {
console.log('search:', this.state.query);
this.props.action
this.props.history.push(`/search/${this.state.query}`)
this.resetComponent()
}
handleInputChange = (query) => {
this.search(query);
this.setState({ isLoading: true, query })
setTimeout(() =>
this.setState({
isLoading: false,
}) , 300)
}
handleResultSelect = (e, { result }) => this.setState({ query: result.title} )
render () {
const resultRenderer = ({ title }) => <List content = {title}/>
return (
<Form onSubmit={this.handleFormSubmit}>
<Search
loading={this.state.isLoading}
onResultSelect={this.handleResultSelect}
onSearchChange={(event) => {this.handleInputChange(event.target.value)}}
showNoResults={false}
value={this.state.query}
resultRenderer={resultRenderer}
results ={this.state.results}
type={"submit"}
{ ...this.props} />
</Form>
);
}
}
export default withRouter (SearchBar)
Search.js
class Search extends Component {
constructor(props) {
super(props)
this.state = {
results: []
}
}
componentWillMount() {
const { match: { params } } = this.props;
axios
.get(`/api/search?query=${params.query}`)
.then(response => {
console.log(response);
this.setState({ results: response.data });
})
.catch(error => console.log(error));
}
render() {
console.log(this.state.results)
return(
<div>
<div className = "heading centered">
<h1> Search results for: {this.props.match.params.query} </h1>
</div>
{this.state.results.map((post) => {
return(
<Post key = {post.id} post={post}/>
)
})}
</div>
);
}
}
export default Search
Updating results of the SearchBars state will be passed down to Search's props, but you don't work with this.props.results but rather with this.state.results, and that doesnt get updated even if the props change. That works the first time as you reload the Search's state inside componentWillMount but that doesnt get called again as the component is not remounted. Therefore Search always works with its states results, that are never updated.
Now to solve this chaos, remove the componentWillMount logic from Search as that is actually doing what SearchBar already does, and add a listener to componentWillReceiveProps that updates the Searches state, or don't work with the state at all inside Search but take the passed in results instead as this.props.results.
const Search = ({ match, results }) => (
<div>
<div className = "heading centered">
<h1> Search results for: {match.params.query} </h1>
</div>
{results.map((post) =>
<Post key = {post.id} post={post}/>
)}
</div>
);
Related
After two days of being stuck on this component, I'm asking for any sort of help. I'm trying to search an API based on user input, and then filter that down to a more specific option as the user keeps typing. After solving a dozen or so errors, I'm still left with "Can't find variable 'Query'", and I just can't seem to find or figure out what exactly it's wanting. There was another post on here that led me in the right direction, but didn't provide any sort of answer for the issue I'm having. Any help here would be appreciated.
import axios from "axios";
import axiosRateLimit from "axios-rate-limit";
import React, { Component } from "react";
import SearchBar from "react-native-elements/dist/searchbar/SearchBar-ios";
class CardSearch extends Component {
state = {
data: [],
filteredData: [],
query: "",
};
handleInputChange = (event) => {
const query = event.target.value;
this.setState((prevState) => {
const filteredData = prevState.data.filter((element) => {
return element.name.toLowerCase().includes(query.toLowerCase());
});
return {
query,
filteredData,
};
});
};
getData = () => {
axiosRateLimit(
axios.get(`https://api.scryfall.com/cards/autocomplete?q=${query}`),
{ maxRPS: 8 }
)
.then((response) => response.json())
.then((data) => {
const { query } = this.state;
const filteredData = data.filter((element) => {
return element.name.toLowerCase().includes(query.toLowerCase());
});
this.setState({
data,
filteredData,
});
});
};
componentWillMount() {
this.getData();
}
render() {
return (
<>
<SearchBar
placeholder='Search For...'
value={this.state.query}
onChange={this.handleInputChange}
/>
<div>
{this.state.filteredData.map((i) => (
<p>{i.name}</p>
))}
</div>
</>
);
}
}
export default CardSearch;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Have a look at this Link. You are not setting the State in a Constructor. And as already mentioned in the comments you will then have to access the query using this.state.query
https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class
The Code-Sample from the React Documentation:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
So I have stored the current search term in a state using e.target.value, i titled it searchField but when I clear the input value, i cannot use it anymore. I am trying to post a message in the body telling the user that their specific searched term provided no results but i cleared the searchField state. So i need to create another state for searchedTerm in addition to my existing "searchField but unsure how to do it . Feel like I am overlooking something quite simple
On click/submit, can you take the state of searchField and add it into another state (searchedTerm) before setting the searchField to ''
import React, { Component } from 'react';
import './App.css';
import request from 'superagent';
import BookList from './BookList'
import Search from './Search'
class App extends Component {
constructor(props) {
super(props);
this.state = {
bookList: [],
hasError: false,
searchField: '',
value:'',
loading: false,
searchResult: ''
};
}
handleSearch = (e) => {
this.setState ({
searchField: e.target.value
})
}
handleClickBookLookup = (e) => {
this.setState({
hasError: false,
bookList: [],
loading: true
})
request
.get('https://www.googleapis.com/books/v1/volumes/')
.query({q: this.state.searchField})
.then ((data) => {
this.setState({
bookList: [...data.body.items],
loading: false,
searchField: ''
})
})
.catch(() => {
this.setState({
hasError: true ,
loading: false,
searchField: ''
});
});
}
handleSubmit = (e) => {
e.preventDefault()
}
render() {
return (
<div className="App">
<Search
handleSearch = {this.handleSearch}
handleSubmit = {this.handleSubmit}
handleClickBookLookup = {this.handleClickBookLookup}
searchField = {this.state.searchField}
/>
<h1>List Of Books</h1>
{this.state.hasError && (
<p>no books found for '________'.</p>
)}
<ul>
{this.state.loading && (
<p>Loading</p>
)}
{this.state.bookList.map((book, i) => {
return (
<BookList
book={book}
/>
)
})}
</ul>
</div>
);
}
}
export default App;
If you want to show the message to the user, you dont have to clear the state.
However if you want to clear it and still have to show the message. You can do that with a timeout. But since you want to clear the state, message should disappear after some time. You dont to want to keep different states for the same thing, it is ambiguous.
You can add a setTimeout in then or catch depending on your case. Clear the state after you have shown the message.
setTimeout(() => { this.setState({ hasError: false, searchField: '' }) },3000)
I think I get what you mean now. What you should do is copy the search value into a local instance variable of the component. That way, even if you reset the state, their search term is still accessible
I've added comments for the changes I've made:
import React, { Component } from 'react';
import './App.css';
import request from 'superagent';
import BookList from './BookList'
import Search from './Search'
class App extends Component {
constructor(props) {
super(props);
// Keep a local instance variable with their search term in it
this._searchField = '';
this.state = {
bookList: [],
hasError: false,
searchField: '',
value:'',
loading: false,
searchResult: ''
};
}
handleSearch = (e) => {
this.setState ({
searchField: e.target.value
});
}
handleClickBookLookup = (e) => {
// Update the local variable here
this._searchField = this.state.searchField;
this.setState({
hasError: false,
bookList: [],
loading: true
})
request
.get('https://www.googleapis.com/books/v1/volumes/')
.query({q: this.state.searchField})
.then ((data) => {
// In here we only reset the state value, NOT the local instance value.
// This means what they searched is still accessible
this.setState({
bookList: [...data.body.items],
loading: false,
searchField: ''
})
})
.catch(() => {
this.setState({
hasError: true ,
loading: false,
searchField: ''
});
});
}
handleSubmit = (e) => {
e.preventDefault()
}
render() {
return (
<div className="App">
<Search
handleSearch = {this.handleSearch}
handleSubmit = {this.handleSubmit}
handleClickBookLookup = {this.handleClickBookLookup}
searchField = {this.state.searchField}
/>
<h1>List Of Books</h1>
{this.state.hasError && (
// Here, we render the local instance variable
<p>no books found for {this._searchField}.</p>
)}
<ul>
{this.state.loading && (
<p>Loading</p>
)}
{this.state.bookList.map((book, i) => {
return (
<BookList
book={book}
/>
)
})}
</ul>
</div>
);
}
}
export default App;
Of course be aware that changing _searchField won't trigger a re-render, since it's not in the state. Doesn't matter in this use case, but just something to remember.
As a side note, in terms of UX, be careful completely wiping someone's search term(s) when they search. Means if they had a small typo, or want to make a small change to their search like adding or removing a word or changing some spelling, they gotta type it out all over again. It's often better to preserve their search, and give an option to clear it, like how many search boxes have a little cross icon on the right hand side that wipes the search box
First problem: Why, if I enter one letter in input, console.log (this.state.results.length) does not show me 1. Only after entering two letters console.log (this.state.results.length) shows me 2.
Second problem: I will type three letters, then remove two letters, console.log (this.state.results.length) should show me 1 and show2. The same is when I clear the input it should show 0;
Demo here: https://stackblitz.com/edit/react-frleaq
class App extends Component {
constructor() {
super();
this.state = {
results: ''
};
}
_handleSearch = query => {
this.setState({
results: query
})
}
render() {
console.log(this.state.results.length)
return (
<div>
<AsyncTypeahead
clearButton
id="basic-example"
labelKey="name"
onSearch={this._handleSearch}
/>
</div>
);
}
}
You can use onInputChange to handle text changes and you can keep the text in state. This way you can check it's length and do whatever you want.
Example:
import React from "react";
import { AsyncTypeahead } from "react-bootstrap-typeahead";
import "bootstrap/dist/css/bootstrap.css";
import "react-bootstrap-typeahead/css/Typeahead.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
multiple: true,
options: [],
selectedUsers: [],
currentInput: ""
};
}
handleSearch = query => {
this.setState({ isLoading: true });
fetch(
`https://api.github.com/search/users?q=${query}+in:login&page=1&per_page=3`
)
.then(resp => resp.json())
.then(({ items }) => {
const options = items.map(i => ({
id: i.id,
name: i.login
}));
return { options };
})
.then(({ options }) => {
this.setState({
isLoading: false,
options
});
});
};
handleChange = selectedItems => {
this.setState({
selectedUsers: selectedItems,
currentInput: ""
});
};
handleInputChange = input => {
this.setState({
currentInput: input
});
};
render() {
const { selectedUsers, isLoading, options, currentInput } = this.state;
return (
<div>
<AsyncTypeahead
clearButton
id="basic-example"
isLoading={isLoading}
options={options}
minLength={3}
multiple
labelKey="name"
onSearch={query => this.handleSearch(query)}
onChange={selectedItems => this.handleChange(selectedItems)}
onInputChange={input => this.handleInputChange(input)}
placeholder="Search for users"
/>
<hr />
<br/>
<br/>
<br/>
{currentInput.length > 0 && <button>MY BUTTON</button>}
<hr />
Selected {selectedUsers.length} Users: <br />
<ul>
{selectedUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
}
export default App;
Total beginner with React.
I am trying to work out the standard approach to this situation in React.
I am accessing an api, the data is being returned all ok, except I am trying to set the data as a state of my component, and the render() method is referencing the state before any data is returned so the state property is being defined as 'null'.
In my code sample below you can see I am logging to the console, and despite the order of things, the second log is being returned from the browser before the one that has setState to be the API data.
Any help / explanation as to why this is happening despite using .then() would be appreciated.
Thank you.
PS: I have removed the TeamList component for simplification, but like the 'second log', the component gets rendered before the data has actually been pulled in.
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: null,
}
}
componentDidMount() {
const uri = 'http://api.football-data.org/v2/competitions/PL/teams';
let h = new Headers()
h.append('Accept', 'application/json')
h.append('X-Auth-Token', 'XXXXXXXXXXXXXXXXXXXX')
let req = new Request(uri, {
method: 'GET',
headers: h,
mode: 'cors'
})
var component = this;
fetch(req)
.then( (response) => {
return response.json()
})
.then( (json) => {
this.setState({ data: json })
})
.then( (json) => {
console.log( 'second log', this.state.data )
})
.catch( (ex) => {
console.log('parsing failed', ex)
})
console.log( 'first log', this.state.data )
}
render() {
return (
<div>
<div className="App">
<TeamList list={this.state.data} />
</div>
</div>
);
}
}
export default App;
You need to add something like this to the start of your render():
if (this.state.data === null) {
return false;
}
So your code should be:
render() {
if (this.state.data === null) {
return false;
}
return (
<div>
<div className="App">
<TeamList list={this.state.data} />
</div>
</div>
);
}
render() is called immediately, but you want it to return false until this.state.data has data
When you mount a component, it gets rendered immeadiately with the initial state (that you've set in the constructor). Then later, when you call setState, the state gets updated and the component gets rerendered. Therefore it makes sense to show something like "loading..." until state.data is not null:
render() {
return (
<div>
<div className="App">
{this.state.data ? <TeamList list={this.state.data} /> : "loading..." }
</div>
</div>
);
}
Now additionally logging does not work as expected as setState does not return a promise, so:
.then( (json) => {
this.setState({ data: json })
})
.then( (json) => {
console.log( 'second log', this.state.data )
})
is actually the same as:
.then( (json) => {
this.setState({ data: json })
console.log( 'second log', this.state.data )
})
and that still logs null as setState is asynchronous, which means that calling it does not change this.state now but rather somewhen. To log it correctly use the callback:
then( (json) => {
this.setState({ data: json }, () => {
console.log( 'second log', this.state.data )
});
})
Just an idea:
import React, { Component } from 'react';
class App extends Component {
constructor(props)
{
super(props);
this.state = {
data: null,
};
}
componentDidMount()
{
fetch('http://api.football-data.org/v2/competitions/PL/teams')
.then(response => response.json())
.then(data => this.setState({ data }));
}
render() {
return (
<div>
<div className="App">
<TeamList list={this.state.data} />
</div>
</div>
);
}
}
export default App;
TeamList :
class TeamList extends React.Component {
constructor(props) {
super(props);
}
render(){
return (
<ul>
{
this.props.list.map((element, i) => {
return (
<li className="un-res t_d " key={i}>{element}</li>
)
}
})
}
}
export default TeamList
Happy coding!
I am willing to change my page whenever my prop changes. My prop is important in that it retrieves a specific data that corresponds to the prop.id from an API. Right now, my API request for the prop.id is on the componentWillMount() part, so it only gets runs once. I want to make it change whenver my prop.id changes, and I think I have to use
componentWillReceiveProps(newProps) {
console.log('Component WILL RECIEVE PROPS!')
}
shouldComponentUpdate(newProps, newState) {
return true;
}
to update my page whenever the prop changes, but I am not really sure how this works. I think the componentWillReceiveProps will get run when the prop changes, but I am not sure how to change the shouldComponentUpdate to make the page keep updating when my prop has changed.
Any tips?
EDIT:
A link that directs me to the detail/id page
return data.map((eachData, index) => {
return ( <Link to={{ pathname: '/details/' + parseID(eachData.url) }}><PokemonInfo poke={ eachData } key={ index } /></Link> );
});
};
goes on to the "/details/" + parseID(eachData.url) which is just an id of the corresponding data. The Next and Prev parts should render the page with id +- 1. It works but the update gets delayed and I have to double click to get the actual prop.
class PokemonDetails extends React.Component {
constructor() {
super();
this.state = {
pokemon: [],
imgurl: '',
abilities: [],
types: []
};
};
componentWillMount() {
// Called first time the comp is loaded right before the comp is added to the page
console.log('Component WILL MOUNT!')
console.log("passed" , this.props.match.params.id)
var url = "https://pokeapi.co/api/v2/pokemon/" + this.props.match.params.id;
axios.get(url)
.then((response) => {
// setState re-renders component
this.setState({
pokemon: response.data,
imgurl: response.data.sprites.front_default,
abilities: response.data.abilities,
types: response.data.types,
})
console.log(this.state.pokemon)
console.log('url', this.state.imgurl)
})
.catch((error) => {
console.log(error);
})
}
componentWillReceiveProps(newProps) {
console.log('Component WILL RECIEVE PROPS!')
console.log("passed" , this.props.match.params.id)
var url = "https://pokeapi.co/api/v2/pokemon/" + this.props.match.params.id;
axios.get(url)
.then((response) => {
// setState re-renders component
this.setState({
pokemon: response.data,
imgurl: response.data.sprites.front_default,
abilities: response.data.abilities,
types: response.data.types,
})
console.log(this.state.pokemon)
console.log('url', this.state.imgurl)
})
.catch((error) => {
console.log(error);
})
}
shouldComponentUpdate(newProps, newState) {
if(newProps.match.params.id != this.props.match.params.id) return true;
return false;
}
render() {
let abilities = this.state.abilities.map( (ability, idx) => {
return(
<Label key = { idx }>
{ ability.ability.name }
</Label>
)
});
let types = this.state.types.map( (type, idx) => {
return(
<Label key = { idx }>
{ type.type.name }
</Label>
)
});
return (
<div className="Home">
<h1>Gotta catchem all!</h1>
<div className="Menu">
<Link to="/"><span className="menuSpan">Search</span></Link >
<Link to="/gallery"><span className="menuSpan">Gallery</span></Link >
</div>
<div>
<img src= { this.state.imgurl } />
<h2>{ this.state.pokemon.name }</h2>
<h3>
<Link onChange = { this.handleChange } to={{ pathname: '/details/' + (parseInt(this.props.match.params.id) - 1) }}><span>Prev</span></Link>
Pokedex #{ this.state.pokemon.id }
<Link onChange = { this.handleChange } to={{ pathname: '/details/' + (parseInt(this.props.match.params.id) + 1) }}><span>Next</span></Link>
</h3>
<h3>
Height { this.state.pokemon.height }
</h3>
<h3>
Weight <label>{ this.state.pokemon.weight }</label>
</h3>
<h4>Abilities</h4>
{ abilities }
<h4>Type(s)</h4>
{ types }
</div>
</div>
);
}
}
You can set the condition in the shouldComponentUpdate. When shouldComponentUpdate return a true value then your component's render function will be called. So you can do something like this.
shouldComponentUpdate(newProps, newState) {
if(newProps.id!==props.id) {
return true
}
return false;
}