How to create algolia autocomplete custom renderer using react class component - javascript

I am tickling with Algolia autocomplete, and I am trying to replicate their custom renderer in react using the class component. This is the sandbox of the minimal demo of custom renderer using functional component,
and here is my attempt to convert it into a class component.
import { createAutocomplete } from "#algolia/autocomplete-core";
import { getAlgoliaResults } from "#algolia/autocomplete-preset-algolia";
import algoliasearch from "algoliasearch/lite";
import React from "react";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
);
// let autocomplete;
class AutocompleteClass extends React.PureComponent {
constructor(props) {
super(props);
this.inputRef = React.createRef();
this.autocomplete = null;
this.state = {
autocompleteState: {},
};
}
componentDidMount() {
if (!this.inputRef.current) {
return undefined;
}
this.autocomplete = createAutocomplete({
onStateChange({ state }) {
// (2) Synchronize the Autocomplete state with the React state.
this.setState({ autocompleteState: state });
},
getSources() {
return [
{
sourceId: "products",
getItems({ query }) {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: "instant_search",
query,
params: {
hitsPerPage: 5,
highlightPreTag: "<mark>",
highlightPostTag: "</mark>",
},
},
],
});
},
getItemUrl({ item }) {
return item.url;
},
},
];
},
});
}
render() {
const { autocompleteState } = this.state;
return (
<div className="aa-Autocomplete" {...this.autocomplete?.getRootProps({})}>
<form
className="aa-Form"
{...this.autocomplete?.getFormProps({
inputElement: this.inputRef.current,
})}
>
<div className="aa-InputWrapperPrefix">
<label
className="aa-Label"
{...this.autocomplete?.getLabelProps({})}
>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
ref={this.inputRef}
{...this.autocomplete?.getInputProps({})}
/>componentDidUpdate()
</div>
</form>
<div className="aa-Panel" {...this.autocomplete?.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source">
{items.length > 0 && (
<ul
className="aa-List"
{...this.autocomplete?.getListProps()}
>
{items.map((item) => (
<li
key={item.objectID}
className="aa-Item"
{...this.autocomplete?.getItemProps({
item,
source,
})}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
}
export default AutocompleteClass;
and the sandbox of the same version, I also tried using componentDidUpdate() but no luck, any lead where I did wrong would be much appreciated thank you :)

Ok, dont know why you need it made into class component but here you go:
import { createAutocomplete } from "#algolia/autocomplete-core";
import { getAlgoliaResults } from "#algolia/autocomplete-preset-algolia";
import algoliasearch from "algoliasearch/lite";
import React from "react";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
);
// let autocomplete;
class AutocompleteClass extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
autocompleteState: {},
query: '',
};
this.autocomplete = createAutocomplete({
onStateChange: this.onChange,
getSources() {
return [
{
sourceId: "products",
getItems({ query }) {
console.log('getting query', query)
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: "instant_search",
query,
params: {
hitsPerPage: 5,
highlightPreTag: "<mark>",
highlightPostTag: "</mark>"
}
}
]
});
},
getItemUrl({ item }) {
return item.url;
}
}
];
}
});
}
onChange = ({ state }) => {
console.log(state)
this.setState({ autocompleteState: state, query: state.query });
}
render() {
const { autocompleteState } = this.state;
return (
<div className="aa-Autocomplete" {...this.autocomplete?.getRootProps({})}>
<form
className="aa-Form"
{...this.autocomplete?.getFormProps({
inputElement: this.state.query
})}
>
<div className="aa-InputWrapperPrefix">
<label
className="aa-Label"
{...this.autocomplete?.getLabelProps({})}
>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
value={this.state.query}
{...this.autocomplete?.getInputProps({})}
/>
</div>
</form>
<div className="aa-Panel" {...this.autocomplete?.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source">
{items.length > 0 && (
<ul
className="aa-List"
{...this.autocomplete?.getListProps()}
>
{items.map((item) => (
<li
key={item.objectID}
className="aa-Item"
{...this.autocomplete?.getItemProps({
item,
source
})}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
}
export default AutocompleteClass;
Anyway the componentDidMount is called only once, and because of ref object is undefined it just returned from it.
Also messing with this in class components is quite a bad idea (that is why func components are recommended)

Related

Refactor class to function components in React

I'm having a hard time converting these 3 class components to function components, the class components are working i just am trying to convert them for learning purposes.
the API call: yelp.js
const { default: SearchBar } = require("../components/SearchBar/SearchBar");
const Yelp = {
searchYelp(term, location) {
return fetch(`/api/hello?term=${term}&location=${location}`)
.then((response) => {
// console.log(response)
return response.json()
}).then((jsonResponse) => {
// console.log(jsonResponse)
if (jsonResponse.businesses) {
return jsonResponse.businesses.map((business) => {
return {
id: business.id,
imageSrc: business.image_url,
name: business.name,
address: business.location.address1,
city: business.location.city,
state: business.location.state,
zipCode: business.location.zip_code,
category: business.categories.title,
rating: business.rating,
reviewCount: business.review_count,
}
})
}
})
}
}
export default Yelp
The Home component as a function that renders a SearchBar and BusinessList component: Home.js
import React, { useState } from "react";
import BusinessList from '../../../src/components/BusinessList/BusinessList';
import SearchBar from '../../../src/components/SearchBar/SearchBar';
import Yelp from '../../util/yelp';
const Home = (term, location) => {
const [businesses, setBusinesses] = useState([]);
const searchYelp = Yelp.searchYelp(term, location).then(businesses => {
setBusinesses(businesses)
})
return (
<>
<SearchBar searchYelp={searchYelp} />
<BusinessList business={businesses} />
</>
)
}
export default Home;
The Home component as a class: Home.js
// import React from 'react';
// import BusinessList from '../../../src/components/BusinessList/BusinessList';
// import SearchBar from '../../../src/components/SearchBar/SearchBar';
// import Yelp from '../../util/yelp';
// class Home extends React.Component {
// constructor() {
// super();
// this.state = {
// businesses: [],
// };
// this.searchYelp = this.searchYelp.bind(this);
// }
// searchYelp(term, location, sortBy) {
// Yelp.searchYelp(term, location, sortBy).then((businesses) => {
// this.setState({ businesses: businesses })
// })
// }
// render() {
// return (
// <>
// <SearchBar searchYelp={this.searchYelp} />
// <BusinessList businesses={this.state.businesses} />
// </>
// )
// }
// }
// export default Home;
The BusinessList component as a function that renders a Business component: BusinessList.js
import React, { useState } from "react";
import './BusinessList.css';
import Business from '../Business/Business';
function BusinessList(businesses) {
console.log(businesses)
return (
<div className="BusinessList">
{
businesses.map(business => {
<Business key={business.id} business={business} />
})
}
</div>
)
};
export default BusinessList;
The BusinessList component as a class: BusinessList.js
// import React from 'react';
// import './BusinessList.css';
// import Business from '../Business/Business';
// class BusinessList extends React.Component {
// constructor(props) {
// super(props)
// console.log(props.businesses)
// }
// render() {
// return (
// <div className="BusinessList">
// {
// this.props.businesses.map((business) => {
// return <Business key={business.id} business={business} />
// })
// }
// </div>
// )
// }
// };
// export default BusinessList;
The Business component as a function: Business.js
import React from "react";
import './Business.css';
const Business = (business) => {
return (
<div className="Business">
<div className="image-container">
<img src={business.business.imageSrc} alt={business.imageSrc} />
</div>
<h2>{business.business.name}</h2>
<div className="Business-information">
<div className="Business-address">
<p>{business.business.address}</p>
<p>{business.business.city} {business.state} {business.zipCode}</p>
</div>
<div className="Business-reviews">
<h3>{business.business.category}</h3>
<h3 className="rating">{business.business.rating}</h3>
<p>{business.business.reviewCount} reviews</p>
</div>
</div>
</div>
)
};
export default Business;
The Business component as a class: Business.js
// import React from "react";
// import './Business.css';
// class Business extends React.Component {
// render() {
// const { business } = this.props
// return (
// <div className="Business">
// <div className="image-container">
// <img src={business.imageSrc} alt={business.imageSrc} />
// </div>
// <h2>{business.name}</h2>
// <div className="Business-information">
// <div className="Business-address">
// <p>{business.address}</p>
// <p>{business.city} {business.state} {business.zipCode}</p>
// </div>
// <div className="Business-reviews">
// <h3>{business.category}</h3>
// <h3 className="rating">{business.rating}</h3>
// <p>{business.reviewCount} reviews</p>
// </div>
// </div>
// </div>
// )
// }
// };
// export default Business;
EDIT **
My attempt at SearchBar component as function: SearchBar.js
import React, { useState, useEffect } from "react";
import './SearchBar.css';
const SearchBar = (props) => {
const [term, setTerm] = useState('')
const [location, setLocation] = useState('')
const [sortBy, setSortBy] = useState('best_match')
const sortByOptions = {
'Best Match': 'best_match',
'Highest Rated': 'rating',
'Most Reviewed': 'review_count'
};
const handleSortByChange = () => {
setSortBy(sortBy)
// console.log(sortByOption)
console.log(sortBy)
}
const renderSortByOptions = (sortByOptions) => {
// console.log(Object.keys(sortByOptions))
return Object.keys(sortByOptions).map(sortByOption => {
let sortByOptionValue = sortByOptions[sortByOption]
// console.log(sortByOptionValue)
return <li
className={sortBy === sortByOption ? 'active' : ''}
onClick={handleSortByChange}
key={sortByOptionValue}>
{sortByOption}
</li>;
})
}
const handleTermChange = (event) => {
setTerm(event.target.value)
}
const handleLocationChange = (event) => {
setLocation(event.target.value)
}
const handleSearch = (event) => {
event.preventDefault()
props.searchYelp(term, location)
}
return (
<div className="SearchBar">
{props.searchYelp}
<div className="SearchBar-sort-options">
<ul>
{renderSortByOptions(sortByOptions)}
</ul>
</div>
<div className="SearchBar-fields">
<input
onChange={handleTermChange}
placeholder="Search Businesses"
/>
<input
onChange={handleLocationChange}
placeholder="Where?"
/>
<button className="SearchBar-submit" onClick={handleSearch}>Let's Go</button>
</div>
</div>
)
}
export default SearchBar;
EDIT**
SearchBar component as a class: SearchBar.js
import React from 'react';
import './SearchBar.css';
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
term: '',
location: '',
sortBy: 'best_match'
}
this.handleTermChange = this.handleTermChange.bind(this)
this.handleLocationChange = this.handleLocationChange.bind(this)
this.handleSearch = this.handleSearch.bind(this)
this.sortByOptions = {
'Best Match': 'best_match',
'Highest Rated': 'rating',
'Most Reviewed': 'review_count'
};
}
getSortByClass(sortByOption) {
// console.log(sortByOption)
if (this.state.sortBy === sortByOption) {
return 'active'
}
return ''
}
handleSortByChange(sortByOption) {
this.setState({
sortBy: sortByOption
})
}
handleTermChange(event) {
this.setState({
term: event.target.value
})
}
handleLocationChange(event) {
this.setState({
location: event.target.value
})
}
handleSearch(event) {
this.props.searchYelp(this.state.term, this.state.location, this.state.sortBy)
event.preventDefault()
}
renderSortByOptions() {
return Object.keys(this.sortByOptions).map(sortByOption => {
let sortByOptionValue = this.sortByOptions[sortByOption]
console.log(sortByOptionValue)
return <li
onClick={this.handleSortByChange.bind(this, sortByOptionValue)}
className={this.getSortByClass(sortByOptionValue)}
key={sortByOptionValue}>
{sortByOption}
</li>;
})
}
render() {
return (
<div className="SearchBar">
{this.searchYelp}
<div className="SearchBar-sort-options">
<ul>
{this.renderSortByOptions()}
</ul>
</div>
<div className="SearchBar-fields">
<input onChange={this.handleTermChange} placeholder="Search Businesses" />
<input onChange={this.handleLocationChange} placeholder="Where?" />
<button className="SearchBar-submit" onClick={this.handleSearch}>Let's Go</button>
</div>
</div>
)
}
};
export default SearchBar;
I keep getting the error "Cannot read properties of undefined (reading 'map')
Or the error "Businesses.map is not a function"
Im also a little confused as to why when everything is converted to function components in my final Business component in order to get things to showup im required to pass things in as business.business.imageSrc instead of just business.imageSrc
First in Home searchYelp should be declared as a function so it can be passed as a callback to the SearchBar component.
const Home = () => {
const [businesses, setBusinesses] = useState([]);
const searchYelp = (term, location) => {
Yelp.searchYelp(term, location)
.then(businesses => {
setBusinesses(businesses);
});
};
return (
<>
<SearchBar searchYelp={searchYelp} />
<BusinessList business={businesses} />
</>
)
};
Then in BusinessList you need to access the passed business prop. Your current code is naming the props object businesses and then attempts to map it. It could be businesses.business.map, but by convention we name the props object props or simply destructure the props you want to use. You need to also return the Business component you are mapping to.
function BusinessList({ business }) {
return (
<div className="BusinessList">
{business.map(business => {
return <Business key={business.id} business={business} />;
})}
</div>
)
};
Same issue with the props object name in the Business component.
const Business = (props) => {
return (
<div className="Business">
<div className="image-container">
<img src={props.business.imageSrc} alt={props.business.imageSrc} />
</div>
<h2>{props.business.name}</h2>
<div className="Business-information">
<div className="Business-address">
<p>{props.business.address}</p>
<p>{props.business.city} {props.business.state} {business.zipCode}</p>
</div>
<div className="Business-reviews">
<h3>{props.business.category}</h3>
<h3 className="rating">{props.business.rating}</h3>
<p>{props.business.reviewCount} reviews</p>
</div>
</div>
</div>
)
};
BusinessList receives props, an object containing the props passed in.
The function parameter would either need to destructure it:
function BusinessList({ businesses }) { ... }
Or reference it off the props object:
function BusinessList(props) {
console.log(props.businesses)
// ...
}
Few notes:
Right now Yelp.searchYelp returns Promise<any[] | undefined>, i.e undefined is a legitimate value that the consume may get. Up to you to decide if setBusinesses(businesses) when businesses is undefined is useful or not, but in that case, handle it. Otherwise default to an empty array, setBusinesses(businesses ?? []) or throw an error.
Do not run side effects in the render phase, i.e call the api inside a useEffect:
React.useEffect(() => {
const searchYelp = Yelp.searchYelp(term, location).then(businesses => {
setBusinesses(businesses ?? [])
})
}, [term, location])
Lastly, const Business = (business) => { here business is actually the props object. You can simply destructure it const Business = ({ business }) => { to get the value directly.

Unable to render a child component inside main Component using iteration

I have a main component defined as App.js
import "./styles.css";
import { Component } from "react";
import Item from "./components/Item";
class App extends Component {
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
}
insertItem() {
if (this.state.textInput !== "") {
this.setState((state) => {
const list = state.items.push(state.textInput);
return {
items: list,
textInput: ""
};
});
}
}
deleteItem(index) {
this.setState((state) => {
const list = [...state.items];
list.splice(index, 1);
return {
items: list,
textInput: ""
};
});
}
handleChange(event) {
this.setState({ textInput: event.target.value });
}
render() {
const template = (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
return template;
}
}
export default App;
and a child Component defined in Item.js
import { Component } from "react";
class Item extends Component {
render() {
return (
<div>
<span>{this.props.name}</span>
<span onClick={this.props.removeItem}>X</span>
</div>
);
}
}
export default Item;
Now my UI looks like
In the above code(App.js) Iam trying to iterate the items and then display the names using the child component. But due to some reason, on entering the text in the input and clicking add its not showing up. Also there are no errors in the console.
Please help, thanks in advance
Edited:
After the recent changes I get this error
You do not need to call this.deleteItem(idx) while passing it to the child.
import "./styles.css";
import { Component } from "react";
import Item from "./components/Item";
class App extends Component {
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
}
insertItem() {
if (this.state.textInput !== "") {
this.setState((state) => {
const list = state.items.push(state.textInput);
return {
items: list,
textInput: ""
};
});
}
}
deleteItem(index) {
this.setState((state) => {
const list = state.items.splice(index, 1);
return {
items: list,
textInput: ""
};
});
}
handleChange(event) {
this.setState({ textInput: event.target.value });
}
render() {
const template = (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
return template;
}
}
export default App;
Updating state in react requires a new reference to objects.You're using Array#push. It will not detect your new change and the DOM will not update. You need to return a new reference.
insertItem() {
if (this.textInput === "") {
this.setState((state) => {
// const list = state.items.push(state.textInput);
const list = [...state.items, state.textInput];
return {
list,
textInput: ""
};
});
}
}
In order to track the array, you must add the key attribute:
{this.state.items.map((item, idx) => {
return <Item key={idx} name={item} removeItem={this.deleteItem(idx)} />;
})}
Here I used the index, but it would be better to use some ID of your model.
UPDATE:
I'd move the handler binding in the constructor:
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
this.insertItem = this.insertItem.bind(this);
}
Then:
<button onClick={this.insertItem}>Add</button>
UPDATE 2:
Okay, it seems you have several mistakes (and I didn't notice them at first glance).
Here is the complete working source (tested):
class App extends Component {
constructor(props) {
super(props);
this.state = { textInput: "", items: [] };
}
insertItem() {
if (this.state.textInput !== "") {
this.setState((state) => {
//const list = state.items.push(state.textInput);
const list = [...state.items, state.textInput];
return {
items: list,
textInput: ""
};
});
}
}
deleteItem(index) {
this.setState((state) => {
const list = [...state.items];
list.splice(index, 1);
return {
items: list,
textInput: ""
};
});
}
handleChange(event) {
this.setState({ textInput: event.target.value });
}
render() {
const template = (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item key={idx} name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
return template;
}
}
export default App;
I think you should move display of template into return statement rather than inside render
...
render() {
return (
<div>
<div>
<input
type="text"
value={this.state.textInput}
onChange={(e) => this.handleChange(e)}
/>
<button onClick={this.insertItem.bind(this)}>Add</button>
</div>
<div>
{this.state.items.map((item, idx) => {
return <Item name={item} removeItem={this.deleteItem.bind(this, idx)} />;
})}
</div>
</div>
);
}

How to target specific element after mapping and passing onClick function as props

I am facing such problem, i got my array of records fetched from an API, mapped it into single elements and outputting them as single components. I have function which changes state of parent Component, passes value to child component and child component should hide/show div content after button is clicked.
Of course. It is working, but partially - my all divs are being hidden/shown. I have set specific key to each child component but it doesn't work.
App.js
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
import countries from '../../countriesList';
import CitySearchForm from './CitySearchForm/CitySearchForm';
import CityOutput from './CityOutput/CityOutput';
import ErrorMessage from './ErrorMessage/ErrorMessage';
class App extends Component {
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false
}
getCities = (e) => {
e.preventDefault();
const countryName = e.target.elements.country.value.charAt(0).toUpperCase() + e.target.elements.country.value.slice(1);
const countryUrl = 'https://api.openaq.org/v1/countries';
const wikiUrl ='https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&format=json&category=city&redirects&origin=*&titles=';
const allowedCountries = new RegExp(/spain|germany|poland|france/, 'i');
if (allowedCountries.test(countryName)) {
axios
.get(countryUrl)
.then( response => {
const country = response.data.results.find(el => el.name === countryName);
return axios.get(`https://api.openaq.org/v1/cities?country=${country.code}&order_by=count&sort=desc&limit=10`)
})
.then( response => {
const cities = response.data.results.map(record => {
return { name: record.city };
});
cities.forEach(city => {
axios
.get(wikiUrl + city.name)
.then( response => {
let id;
for (let key in response.data.query.pages) {
id = key;
}
const description = response.data.query.pages[id].extract;
this.setState(prevState => ({
cities: [...prevState.cities, {city: `${city.name}`, description}],
infoMessage: prevState.infoMessage = ''
}))
})
})
})
.catch(error => {
console.log('oopsie, something went wrong', error)
})
} else {
this.setState(prevState => ({
infoMessage: prevState.infoMessage = 'This is demo version of our application and is working only for Spain, Poland, Germany and France',
cities: [...prevState.cities = []]
}))
}
}
descriptionTogglerHandler = () => {
this.setState((prevState) => {
return { visible: !prevState.visible};
});
};
render () {
return (
<div className="App">
<ErrorMessage error={this.state.infoMessage}/>
<div className="form-wrapper">
<CitySearchForm getCities={this.getCities} getInformation={this.getInformation} countries={countries}/>
</div>
{this.state.cities.map(({ city, description }) => (
<CityOutput
key={city}
city={city}
description={description}
show={this.state.visible}
descriptionToggler={this.descriptionTogglerHandler} />
))}
</div>
);
}
}
export default App;
CityOutput.js
import React, { Component } from 'react';
import './CityOutput.css';
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show } = this.props;
let descriptionClasses = 'output-record description'
if (show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
export default CityOutput;
Put the visible key and the toggle function in the CityOutput instead of having it in the parent
import React, { Component } from "react";
import "./CityOutput.css";
class CityOutput extends Component {
state = {
visible: true
};
descriptionTogglerHandler = () => {
this.setState({ visible: !this.state.visible });
};
render() {
const { city, description } = this.props;
let descriptionClasses = "output-record description";
if (this.state.visible) {
descriptionClasses = "output-record description open";
}
return (
<div className="output">
<div className="output-record">
<b>City:</b> {city}
</div>
<button onClick={() => this.descriptionTogglerHandler()}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
);
}
}
export default CityOutput;
There are two ways of how I would approach this,
The first one is setting in your state a key property and check and compare that key with the child keys like:
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false.
currKey: 0
}
descriptionTogglerHandler = (key) => {
this.setState((prevState) => {
return { currKey: key, visible: !prevState.visible};
});
};
// then in your child component
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show, currKey, elKey } = this.props;
let descriptionClasses = 'output-record description'
if (show && elKey === currKey) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={() => descriptionToggler(elKey)}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
The other way is to set an isolated state for every child component
class CityOutput extends Component {
constructor(props) {
this.state = {
show: false
}
}
function descriptionToggler() {
const {show} = this.state;
this.setState({
show: !show
})
}
render() {
const { city, descriptionToggler, description } = this.props;
let descriptionClasses = 'output-record description'
if (this.state.show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
I hope this helps ;)

Trying to get my delete button to work on a React Component

I'm working on my First project with React, I have an App and a ToDo. I am defining a deleteToDo method and I want the method to call this.setState() and pass it a new array that doesn't have the to-do item being deleted with the use of the .filter() array method. I don't want to alter the code to much or introduce more complexity. In essence I would like to keep it as straight forward as possible. I am still a beginner with React so this has been a big learning process. I feel that I am close.
This is the main app
import React, { Component } from 'react';
import './App.css';
import ToDo from './components/ToDo.js';
class App extends Component {
constructor(props) {
super(props);
this.state = {
todos: [
{ description: 'Walk the cat', isCompleted: true },
{ description: 'Throw the dishes away', isCompleted: false },
{ description: 'Buy new dishes', isCompleted: false }
],
newTodoDescription: ''
};
}
deleteToDo(index) {
const todos = this.state.todos.slice();
const todo = todos[index];
todo.deleteToDo = this.state.filter(index);
this.setState({ todos: todos });
}
handleChange(e) {
this.setState({ newTodoDescription: e.target.value })
}
handleSubmit(e) {
e.preventDefault();
if (!this.state.newTodoDescription) { return }
const newTodo = { description: this.state.newTodoDescription, isCompleted: false };
this.setState({ todos: [...this.state.todos, newTodo], newTodoDescription: '' });
}
toggleComplete(index) {
const todos = this.state.todos.slice();
const todo = todos[index];
todo.isCompleted = todo.isCompleted ? false : true;
this.setState({ todos: todos });
}
render() {
return (
<div className="App">
<ul>
{ this.state.todos.map( (todo, index) =>
<ToDo key={ index } description={ todo.description } isCompleted={ todo.isCompleted } toggleComplete={ this.toggleComplete } deleteToDo={ this.deleteToDo } />
)}
</ul>
<form onSubmit={ (e) => this.handleSubmit(e) }>
<input type="text" value={ this.state.newTodoDescription } onChange={ (e) => this.handleChange(e) } />
<input type="submit" />
</form>
</div>
);
}
}
export default App;
And this the ToDo aspect
import React, { Component } from 'react';
class ToDo extends Component {
render() {
return (
<li>
<button type="button" onClick={ this.props.deleteTodo} > delete </button>
<input type="checkbox" checked={ this.props.isCompleted } onChange={ this.props.toggleComplete } />
<span>{ this.props.description }</span>
</li>
);
}
}
export default ToDo;
You slice and array without the index, that's may be why your delete not work
deleteToDo(index) {
const todos = this.state.todos.slice(index, 1);
this.setState({ todos: todos });
}
1) You need to bind your deleteToDo method in the constructor
this.deleteToDo = this.deleteToDo.bind(this);
2) You need to set a new property on the component that is the same as its index.
<ToDo
key={index}
id={index}
description={ todo.description }
// ...
/>
3) Then you can pass that index as the argument to deleteToDo (making sure you spell the method name correctly).
<button
type="button"
onClick={() => this.props.deleteToDo(this.props.index)}
>Delete
</button>
4) Finally, you can strip down your deleteToDo method to the following:
deleteToDo(index) {
// Return a new array that doesn't
// have a row with a matching index
const todos = this.state.todos.filter((el, i) => i !== index);
this.setState({ todos });
}
Here's a working version.

Where to fetch data in React component

I'm trying to get a feel for reactjs, new to front end development. Google Books API is simple so I decided to use it to build a react page that lists 10 books given the user input.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
const GOOGLE_BOOKS_API = 'https://www.googleapis.com/books/v1/volumes?q=';
const GOOGLE_BOOKS_API_LIMIT = 'maxResults=10';
class BookItem extends React.Component {
render() {
return (
<div>
<img alt="Book" src={this.props.data.volumeInfo.imageLinks.thumbnail} />
<span>{this.props.data.volumeInfo.imageLinks.thumbnail}</span>
</div>
);
}
}
class BookResults extends React.Component {
render() {
const isBookResultsEmpty = !(this.props.books && this.props.books.length > 1);
const bookItems = isBookResultsEmpty ? [] : this.props.books.map((book,index) =>
<BookItem key={index} data={book} />
);
return (
<div className='book-results'>
{isBookResultsEmpty ? (
<h1>No Results</h1>
) : (
<div> {bookItems} </div>
)}
</div>
);
}
}
class BookSearch extends React.Component {
constructor(props) {
super(props);
this.state = {
bookQuery: '',
books: []
};
this.handleInputChange = this.handleInputChange.bind(this);
this.getBooks = this.getBooks.bind(this)
}
getBooks() {
let queryString = '';
if (this.state.bookQuery && this.state.bookQuery.length > 1) {
queryString = this.state.bookQuery.replace(/\s/g, '+');
fetch(`${GOOGLE_BOOKS_API}${queryString}&${GOOGLE_BOOKS_API_LIMIT}`)
.then(results => {
return results.json();
})
.then(json => {
this.setState({
books: json.items
});
})
.catch(e => console.log('error', e));
}
}
handleInputChange(event) {
this.setState({
bookQuery: this.search.value
},
this.getBooks());
}
render() {
return (
<div className="book-search">
<form>
<input
placeholder="Search for Books"
ref={input => this.search = input}
onChange={this.handleInputChange}
/>
</form>
<BookResults books={this.state.books} />
</div>
);
}
}
ReactDOM.render(<BookSearch />, document.getElementById('root'));
I get an error when typing the input:
TypeError: Cannot read property 'thumbnail' of undefined
I added a check for the data in the BookResults component but the error still occurs. I assume it has to do with the state or props value changing while rendering, but I don't know enough about React to be sure
Some books don't have imageLinks, so you need to make sure it is not undefined before you use it.
Example
class BookItem extends React.Component {
render() {
const { imageLinks } = this.props.data.volumeInfo;
if (!imageLinks) {
return null;
}
return (
<div>
<img alt="Book" src={imageLinks.thumbnail} />
<span>{imageLinks.thumbnail}</span>
</div>
);
}
}

Categories

Resources