How to halt rendering of dynamic search results in React? - javascript

I am working on creating a dynamic search results workflow. I can make the results render with no issue, but can't figure out how best to toggle them off when I delete all the input from the search bar. If you start typing, addresses appear that match, but then as you delete all the way they don't all go away.
My thoughts were to use one of the two parameters in my state variables: showMatches or matches.length. I am struggling to see the final piece to this puzzle. Below is my current code:
App.js
import React, { Component } from 'react';
import { Form, Button, ListGroup } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import Match from './Match';
//import Render from './Render';
const my_data = require('./data/test.json')
class App extends Component {
state = {
links: [],
selectedLink:null,
userLocation: {},
searchInput: "",
showMatches: false,
matches: [],
searchLink:[]
}
componentDidMount() {
fetch('https://data.cityofnewyork.us/resource/s4kf-3yrf.json')
.then(res=> res.json())
.then(res=>
//console.log(json)
this.setState({links:res})
);
}
handleInputChange = (event) => {
console.log(event.target.value)
event.preventDefault()
this.setState({searchInput: event.target.value })
this.updateMatches()
console.log(this.state.showMatches)
console.log(this.state.matches.length)
}
handleSubmit = (event) => {
event.preventDefault()
this.displayMatches();
}
findMatches = (wordToMatch, my_obj) => {
return my_obj.filter(place => {
// here we need to figure out the matches
const regex = new RegExp(wordToMatch, 'gi');
//console.log(place.street_address.match(regex))
return place.street_address.match(regex)
});
}
updateMatches =() => {
const matchArray = this.findMatches(this.state.searchInput, this.state.links);
const newStateMatches = matchArray.map(place => {
//console.log(place.street_address);
return place
});
this.setState({matches:newStateMatches})
this.state.matches.length > 1 ? this.setState({showMatches: true}) : this.setState({showMatches: false})
}
alertClicked = address => {
//event.preventDefault(); // not sure what event you're preventing
this.setState({searchLink: address});
this.pushData();
}
render() {
return (
<div>
<input
placeholder="Search for a Link Near you..."
onChange = {this.handleInputChange}
value = {this.state.searchInput}
/>
<ListGroup defaultActiveKey="#link1">
{
this.state.matches.map(match => {
return <Match
address={match.street_address}
alertClicked={this.alertClicked}
value = {this.state.searchLink}
logState={this.logState}/>
})
}
</ListGroup>
</div>
);
}
}
export default App;
Match.js
import React from 'react';
import { ListGroup } from 'react-bootstrap';
const match = ({ alertClicked, address }) => {
return (
<ListGroup.Item
className="Matches"
action
// function expressions could cause this to rerender unnecessarily.
onClick={(address) => alertClicked(address)}>
<p>{`${address}`}</p>
</ListGroup.Item>
)
}
export default match;
Appreciate the help.

The simplest way I think you could implement this is in your handleInputChange, like so :
handleInputChange = (event) => {
event.preventDefault()
if (event.target.value.length === 0) {
this.setState({searchInput: "", showMatches: false, matches: [] })
return
}
this.setState({searchInput: event.target.value })
this.updateMatches()
}
But what do you mean by "but then as you delete all the way they don't all go away" ? Sounds like there actually might be a bug in you updateMatches.
EDIT: Chris' comment is spot on regarding updateMatches.

Related

How to update a react component after a fetch

I am learning react.
I have a simple react app sample that :
Fetch users
Once users are fetched, show their name on a Card
What I'd like to do is to expand this sample. Instead of using a simple list of users, I'd like to use a list of pokemons. What I try to do is :
Fetch the list of pokemon and add in state.pokemons
Show the Card with the pokemon name from state.pokemons
From that list, get the URL to fetch the detail of the given pokemon and add in state.pokemonsDetails
From the state.pokemonsDetails, update the Cards list to show the image of the pokemon.
My problem is: I don't even know how to re-render the Cards list after a second fetch.
My question is: How to update the Cards list after the second fetch?
See my code below:
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox"
import Scroll from "../components/Scroll"
import './App.css';
class App extends React.Component{
constructor(){
super();
this.state = {
pokemons:[],
pokemonsDetails:[],
searchfield: ''
}
}
getPokemons = async function(){
const response = await fetch('https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20');
const data = await response.json();
this.setState({pokemons:data.results})
}
getPokemonDetails = async function(url){
//fetch function returns a Promise
const response = await fetch(url);
const data = await response.json();
//console.log('getPokemonDetails', data);
this.setState({pokemonsDetails:data});
}
componentDidMount(){
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value})
}
render(){
const {pokemons, pokemonsDetails, searchfield} = this.state;
if(pokemons.length === 0){
console.log('Loading...');
return <h1>Loading....</h1>
}else if (pokemonsDetails.length === 0){
console.log('Loading details...');
pokemons.map(pokemon => {
return this.getPokemonDetails(pokemon.url);
});
return <h1>Loading details....</h1>
}else{
return(
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange}/>
<Scroll>
<CardList pokemons={pokemons}/>
</Scroll>
</div>
);
}
}
}
export default App;
Some remarks :
I can see a problem where my Cards list is first created with state.pokemons, then, I would need to update Cards list with state.pokemonsDetails. The array is not the same.
Second problem, I don't even know how to call the render function after state.pokemonsDetails is filled with the fetch. I set the state, but it looks like render is not called every time
More a question than a remark. The way I update my state in getPokemonDetails might be incorrect. I keep only one detail for one given pokemon. How to keep a list of details? Should I use something else than setState to expand pokemonsDetails array?
You can combine 2 API calls before pokemons state update that would help you to control UI re-renderings better
You can try the below approach with some comments
Side note that I removed pokemonDetails state, so you won't see the loading elements for pokemonDetails as well
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
//try to get all pokemon details at once with fetched URLs
const pokemonDetails = await Promise.all(
data.results.map((result) => this.getPokemonDetails(result.url))
);
//map the first and second API response data by names
const mappedPokemons = pokemonDetails.map((pokemon) => {
const pokemonDetail = pokemonDetails.find(
(details) => details.name === pokemon.name
);
return { ...pokemon, ...pokemonDetail };
});
//use mapped pokemons for UI display
this.setState({ pokemons: mappedPokemons });
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
If you want to update pokemon details gradually, you can try the below approach
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
this.setState({ pokemons: data.results });
for (const { url } of data.results) {
this.getPokemonDetails(url).then((pokemonDetails) => {
this.setState((prevState) => ({
pokemons: prevState.pokemons.map((pokemon) =>
pokemon.name === pokemonDetails.name
? { ...pokemon, ...pokemonDetails }
: pokemon
)
}));
});
}
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
Side note that this approach may cause the performance issue because it will keep hitting API for fetching pokemon details multiple times and updating on the same state for UI re-rendering

Manipulating Local Data in React JS, Can Not Get It To Work For My Quiz Application

I'm new to coding (it's been around three months) and I have a problem with React JS.
I took freecodecamp's eleven hour REact JS Course on YouTube and in the end of the video, there is a quiz application challenge called quizzy.
You can go to my github project file and check it out
I came to a point where I can't get the answer options selected.
I want to toggle between a different colored background whenever I click on an answer button, and I wanted it to stay as long as that button is clicked. As far as I checked, there seems to be a problem with the App.js file where I try to manipulate the data's isSelected key inside toggle function. I kindly ask anyone for help. I just don't know what I am doing wrong and it's driving me crazy.
My App.js file looks like this:
import { nanoid } from 'nanoid';
import React from 'react';
import data from '../data';
import QuestionsAndAnswers from './QuestionsAndAnswers';
function Quiz() {
const [quiz, setQuiz] = React.useState(data);
// const [isSelected, setIsSelected] = React.useState(false);
React.useEffect(() => {
const newData = data.map((data) => ({
...data,
answerOptions: data.answerOptions.map(answerOptions => ({
...answerOptions,
optionsID: nanoid()
}))
}))
setQuiz(newData);
}, [])
const handleSubmit = (event) => {
event.preventDefault();
console.log("completed")
}
function toggle(id, value) {
console.log(id, value)
setQuiz((oldState) => oldState.map((data) => {
return data.id === id
? {
...data,
answerOptions: data.answerOptions.map(answerOptions => {
return answerOptions.answerText === value
? {
...answerOptions,
isSelected: !answerOptions.isSelected
}
: {
...answerOptions,
isSelected: false
}
})
}
: data
}))
}
const selectedOptions = data.map(data => {
return (data.answerOptions.isSelected ? data : null)
})
console.log(selectedOptions)
const questions = quiz.map((quiz, index) => {
return (
<QuestionsAndAnswers
key={index}
quiz={quiz}
setQuiz={setQuiz}
toggle={toggle}
/>
)
})
// main function
return (
<main>
<form className="form-container" onSubmit={handleSubmit}>
<h2 className='header'>QuizCript</h2>
{questions}
<button className="complete-quiz-button" type='submit'>Complete the Quiz</button>
</form>
</main>
)
}
export default Quiz;

React AutoSuggest: search text box not updating on suggestion selection

I am using the react autosuggest library to build auto-suggestion
import Autosuggest from "react-autosuggest";
import React, { Component } from "react";
import QueryString from "query-string";
class AutoSuggestSearch extends Component {
constructor() {
super();
this.state = {
value: "",
suggestions: []
};
this.getSuggestionValue = this.getSuggestionValue.bind(this);
this.renderSuggestion = this.renderSuggestion.bind(this);
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
getSuggestionValue = suggestion => suggestion.fullNameSuggestion;
renderSuggestion = suggestion => <div>{suggestion.name}</div>;
onSuggestionSelected = (event, { suggestion}) => {
console.log(suggestion);
this.setState({
suggestions: [],
value: suggestion.name
});
};
onSuggestionsFetchRequested = ({ value }) => {
const params = {
stationPrefixName: value
};
const queryParams = QueryString.stringify(params);
fetch(`http://localhost:8000/api/suggest?${queryParams}`)
.then(res => res.json())
.then(data => {
console.log(data);
this.setState({
suggestions: data
});
})
.catch(console.log);
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: [],
value: ""
});
};
render() {
const { value, suggestions } = this.state;
const inputProps = {
placeholder: "Search",
value,
onChange: this.onChange
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
inputProps={inputProps}
/>
);
}
}
export default AutoSuggestSearch;
The suggestion gets rendered on typing on search box as well as the logging inside onSuggestionSelected gets logged correctly but the input search box does not update correctly.
On debugging further I found that onSuggestionsClearRequested also gets invoked after onSuggestionSelected which is causing the search input box to be empty.
I validated this by adding const string inside onSuggestionsClearRequested
onSuggestionsClearRequested = () => {
alert("clear request");
this.setState({
suggestions: [],
value: "mysearch"
});
};
Is there anyway to prevent onSuggestionsClearRequested invokation on suggestion selection?
Or updating the search query value inside onSuggestionsClearRequested is the correct way?
You can use componentDidUpdate or UseEffect if you are using it in functional component.
I have used react-autosuggest in functional component and clear suggestion works only if value doesn't matches with the suggestions:
const [clear, setClear] = useState(false);
const handleOnChange = newValue => {
setValue(newValue);
setClear(false);
};
useEffect(() => {
if (!suggestions.some(option => option === value) && clear) {
setValue('');
}
}, [clear]);
const onSuggestionsClearRequested = () => {
setClear(true);
setSuggestions([]);
};
The onSuggestionsClearRequested function gets called everytime you click outside the search input, which is the default implementation of the libary being used,
What we implement in onSuggestionsClearRequested is upto us.
you can change the implementation as follows :
Approach keep keyword inside input if available options are not selected
onSuggestionsClearRequested = () => {};
this should provide the desired implementation behaviour.
Hi you may approach with hooks. It looks good and less coding.
You may find below
https://github.com/rajmaravanthe/react-auto-suggestion

Retrieving a value from a child component back to the parent for a LIstGroup?

I've been trying to retrieve a value from the child component in react for an app I am putting together. I think I am making a very simple error here. I originally asked a related question here:
React read value of button clicked
Right now my code looks like this:
App.js
import React, { Component } from 'react';
import { Form, Button, ListGroup } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import Match from './Match'
const my_data = require('./data/test.json')
class App extends Component {
state = {
links: [],
selectedLink:null,
userLocation: {},
searchInput: "",
showMatches: false,
matches: [],
searchLink:[]
}
componentDidMount() {
fetch('https://data.cityofnewyork.us/resource/s4kf-3yrf.json')
.then(res=> res.json())
.then(res=>
//console.log(json)
this.setState({links:res})
);
}
handleInputChange = (event) => {
event.preventDefault()
this.setState({searchInput: event.target.value })
console.log(event.target.value)
}
handleSubmit = (event) => {
event.preventDefault()
this.displayMatches();
}
findMatches = (wordToMatch, my_obj) => {
return my_obj.filter(place => {
// here we need to figure out the matches
const regex = new RegExp(wordToMatch, 'gi');
//console.log(place.street_address.match(regex))
return place.street_address.match(regex)
});
}
displayMatches =() => {
const matchArray = this.findMatches(this.state.searchInput, this.state.links);
const newStateMatches = matchArray.map(place => {
console.log(place.street_address);
return place
});
this.setState({matches:newStateMatches})
this.setState({showMatches:true})
}
alertClicked =(event) => {
event.preventDefault()
//alert('you clicked an item in the group')
const data = event.target.value
console.log('clicked this data:', data)
this.setState({searchLink: event.target.value})
console.log(this.state.searchLink)
}
render() {
return (
<div>
<input
placeholder="Search for a Link Near you..."
onChange = {this.handleInputChange}
value = {this.state.searchInput}
/>
<Button onClick={this.handleSubmit}>
Search
</Button>
<ListGroup defaultActiveKey="#link1">
{
this.state.matches.map(match => {
return <Match
address={match.street_address}
alertClicked={this.alertClicked}
value = {this.state.searchLink}/>
})
}
</ListGroup>
</div>
);
}
}
export default App;
Match.js
import React from 'react';
import { ListGroup } from 'react-bootstrap';
const match = ( props ) => {
return (
<ListGroup.Item
className="Matches"
action onClick={props.alertClicked}
value = {props.value}>
<p>{`${props.address}`}</p>
</ListGroup.Item>
)
};
export default match;
I am trying to access the value of the ListGroup Item when I click on it with this:
alertClicked =(event) => {
event.preventDefault()
//alert('you clicked an item in the group')
const data = event.target.value
console.log('clicked this data:', data)
this.setState({searchLink: event.target.value})
console.log(this.state.searchLink)
}
But can't seem to get it to work. Probably been staring at this way too long. Appreciate the help guys.
Match.js
import React from 'react';
import { ListGroup } from 'react-bootstrap';
const match = ({ alertClicked, address }) => {
return (
<ListGroup.Item
className="Matches"
action
// function expressions could cause this to rerender unnecessarily.
onClick={(address) => alertClicked(address)}>
<p>{`${address}`}</p>
</ListGroup.Item>
)
Other.js
alertClicked = address => {
event.preventDefault(); // not sure what event you're preventing
this.setState({searchLink: address});
}
If you're worried about the unnecessary rendering, you should look for another method of achieving this functionality with a more defined element/component.

Why won't it work this way to display what the user has clicked to appear in the modal?

I want whatever the user clicks to appear in the modal. In this case, I want a random name to appear in the modal via displayPerson() fat arrow function upon the user clicking <Button/>.
In between <Modal/>, I'm trying to display it, but it's not working. It's just a blank screen.
The modal has no issues in terms of it being toggled and/or closing with the use of redux. The only issue I'm facing is what should appear inside the modal only when the user clicks the button.
How come it won't work the way I'm doing it?
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Modal from 'react-modal';
import Aux from '../../../../hoc/Aux';
import Button from '../Buttons/Button';
import CheckoutButton from '../../../../components/UI/buttons/CheckoutButton/CheckoutButton';
import { CLOSE_MODAL, OPEN_MODAL } from "../../../../store/action/NoNameAction";
class Main extends Component {
state = {
isClicked: false
}
componentWillMount() {
Modal.setAppElement('body');
}
displayPerson = () => {
this.setState({isClicked: true});
if(this.state.isClicked) {
return(
<p>a random name</p>
);
}
}
render() {
return (
<Aux>
<Button clicked={() => this.props.thisButtonChosen() && this.displayPerson()} label={"This button"}/>
<CheckoutButton clicked={() => this.props.openModalRedux()}/>
<Modal isOpen={this.props.isOpen}>
<p>{this.displayPerson}</p>
<button onClick={() => this.props.closeModalRedux()}>Close</button>
</Modal>
</Aux>
);
}
}
const mapStateToProps = state => {
return {
isOpen: state.global.isModalOpen
}
};
const mapDispatchToProps = dispatch => {
return {
thisButtonChosen: () => dispatch({type: THIS_BUTTON_CHOSEN}),
// Modal handlers
openModalRedux: () => dispatch({type: OPEN_MODAL}),
closeModalRedux: () => dispatch({type: CLOSE_MODAL})
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Main);
this.displayPerson is returning a value depending on the next state update, but setState is asynchronous.
You can use the callback in order to get the new state:
displayPerson = () => {
this.setState({isClicked: true}, () => {
if(this.state.isClicked) {
return(
<p>a random name</p>
);
}
});
}
but even with this approach you could have some issues.
I recommend storing the random name in the state as well.
displayPerson = () => {
const randomUserName = "some random user name...";
this.setState({isClicked: true, randomName: randomUserName});
}
And just use it in render:
<p>{this.state.randomUserName}</p>
Of course you'll need to trigger displayPerson

Categories