I am trying to create a textbox with an autocomplete feature that pulls suggestions from an API but having trouble updating the state after I receive the array from the API. I am modifying code from here: https://blog.bitsrc.io/building-a-react-autocomplete-component-from-scratch-b78105324f4c
I think I have to use ComponentDidMount() but I am not sure how to apply it to an onChange Function.
class App extends React.Component {
render() {
return (
<div className="App">
<Autocomplete/>
</div>
);
}
}
class Autocomplete extends React.Component{
state = {
activeOption: 0,
filteredOptions: [],
showOptions: false,
userInput: ''
};
onChange = (e) => {
const userInput = e.currentTarget.value;
fetch("/places", {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(userInput)
}).
then(response => response.json())
.then(data => data.filter(element =>
element.PlaceName.toLowerCase().indexOf(userInput.toLowerCase()) > -1))
.then(filteredOptions => this.setState(
{
activeOption: 0,
filteredOptions: filteredOptions,
showOptions: true,
userInput: e.currentTarget.value
}));
};
.
.
.
.
render() {
const {
onChange,
onClick,
onKeyDown,
state: { activeOption, filteredOptions, showOptions, userInput }
} = this;
let optionList;
if (showOptions && userInput) {
console.log(filteredOptions)
if (filteredOptions.length) {
optionList = (
<ul className="options">
{filteredOptions.map((optionName, index) => {
let className;
if (index === activeOption) {
className = 'option-active';
}
return (
<li className={className} key={optionName} onClick={onClick}>
{optionName}
</li>
);
})}
</ul>
);
} else {
optionList = (
<div className="no-options">
<em>No Option!</em>
</div>
);
}
}
return (
<React.Fragment>
<div className="search">
<input
type="text"
className="search-box"
onChange={onChange}
onKeyDown={onKeyDown}
value={userInput}
/>
<input type="submit" value="" className="search-btn" />
</div>
{optionList}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.querySelector("#app"));
Once I try and run this, I get two errors: one for a synthetic event being reused for performance issues and one for a component changing an controlled input
You could put a setTimeout in the onChange method. And if the user doesn't type, you make the request, in other wise, you can't make the request
It looks like you were anticipating the filteredOptions variable to be a list of strings. It is actually a list of objects, which was causing React to throw the "object not allowed as children" error. Also, you can't use the event object to set state as it is already released. However, you were already storing the value in a variable which you could use, userInput. I updated your code with some very minor tweaks, and it appears to work. Take a look at a working example:
import React from "react";
import "./styles.css";
class App extends React.Component {
render() {
return (
<div className="App">
<Autocomplete />
</div>
);
}
}
class Autocomplete extends React.Component {
state = {
activeOption: 0,
filteredOptions: [],
showOptions: false,
userInput: ""
};
onChange = (e) => {
const userInput = e.currentTarget.value;
// Mock out the API call and JSON
Promise.resolve()
.then(() => {
const data = [
{ PlaceName: "Place 1" },
{ PlaceName: "Place 2" },
{ PlaceName: "Another Place 1" },
{ PlaceName: "Another Place 2" }
];
return data.filter(
(element) =>
element.PlaceName.toLowerCase().indexOf(userInput.toLowerCase()) >
-1
);
})
.then((filteredOptions) =>
this.setState({
activeOption: 0,
filteredOptions: filteredOptions,
showOptions: true,
userInput: userInput
})
);
};
render() {
const {
onChange,
onClick,
onKeyDown,
state: { activeOption, filteredOptions, showOptions, userInput }
} = this;
let optionList;
if (showOptions && userInput) {
if (filteredOptions.length) {
optionList = (
<ul className="options">
{filteredOptions.map((option, index) => {
let className;
if (index === activeOption) {
className = "option-active";
}
return (
<li
className={className}
key={option.PlaceName}
onClick={onClick}
>
{option.PlaceName}
</li>
);
})}
</ul>
);
} else {
optionList = (
<div className="no-options">
<em>No Option!</em>
</div>
);
}
}
return (
<React.Fragment>
<div className="search">
<input
type="text"
className="search-box"
onChange={onChange}
onKeyDown={onKeyDown}
value={userInput}
/>
<input type="submit" value="" className="search-btn" />
</div>
{optionList}
</React.Fragment>
);
}
}
export default App;
Related
I have this working stateful component in React:
import React, {Component} from "react";
class MyComponent extends Component {
constructor() {
super();
this.myRef = React.createRef();
this.state = {
array: [],
noresults: false
};
}
loadData = () => {
let data = this.myRef.current.value;
let url = "someurl="+data;
if(!data) {
return;
}
fetch(url)
.then((res) => res.json())
.then((json) => {
if(json.data.length > 0) {
this.setState({
array: json.data,
noresults: false
});
} else {
this.setState({
noresults: true
});
}
})
}
render() {
const { array, noresults } = this.state;
return (
<div>
<section>
<input ref={this.myRef} type="number"/>
<button onClick={this.loadData}>Click Here</button>
</section>
<ul>
{
array.map((e) => (
<li key = { e.key } >
{ e.data }
</li>
))
}
</ul>
{noresults && <div>No Records</div>}
</div>
);
}
}
export default MyComponent
I want to convert this to stateless like this:
function MyComponent() {
return (
<div>
<section>
<input type="number"/>
<button>Click Here</button>
</section>
<ul>
<li></li>
</ul>
<div>No Records</div>
</div>
);
}
export default MyComponent
Now how can I pass data of input to my method to make API call. Also how to pass the response from API to display as ul and li elements?
Just pass it as component props:
const MyComponent = ({array = [], loadData = () => {}}) => {
const [inputValue, setInputValue] = useState();
const handleInputChange = (evt) => {
setInputValue(evt.target.value);
};
return (
<div>
<section>
<input type="number" onChange={handleInputChange} />
<button onClick={e => loadData(e, inputValue)}>Click Here</button>
</section>
<ul>
{array.map((e) => (<li key={e.key}>{e.data}</li>))}
</ul>
{array.length === 0 && <div>No Records</div>}
</div>
);
}
For input, I created a local state which is updated on input change and passed it to loadData function. You can access the current value by parametrizing loadData function:
loadData = (e, currentInputValue) => { ... };
I have five Users in the array.
The code below displays each users info from the arrays when pop up button is clicked and everything works fine.
Now I have created a form to update each user's age based on their respective person Id on form submission via call to nodejs
backend. Am actually getting the result from nodejs backend..
Here is my issue.
Each time I entered age in the input and click on submission button Eg. for user 1. Instead of the age result to
appear near that very user 's name in the space provided in the button, it will appears on the body of the page as can be seen from
screenshots provided.
If call it as props For instance {this.props.messages.personAge}
as per below
<button
onClick={() => this.open(this.props.data.id, this.props.data.name)}
>
(Age should Appear Here-- ({this.props.messages.personAge})--)
{this.props.data.name}
</button>
It shows error
TypeError: Cannot read property 'personAge' of undefined
at User.render
Here is how am getting the response from nodejs server
componentDidMount(){
this.socket = io('http://localhost:8080');
this.socket.on('response message', function(data){
addAge(data);
});
const addAge = data => {
console.log(data);
//this.setState({messages: [...this.state.messages, data]});
this.setState({messages: [data]});
};
}
below is how am displaying the age result for each unique user
{this.state.messages.map((message, i) => {
//if (message.personId == this.props.data.id) {
//if (message.personId == person.id) {
if (message.personId) {
return (
<div key={i}>
<div>
({message.personAge}--years)
</div>
</div>
)
}
})}
</ul>
Here is the Entire Code
import React, { Component, Fragment } from "react";
import { render } from "react-dom";
import { Link } from 'react-router-dom';
import axios from 'axios';
import io from "socket.io-client";
class User extends React.Component {
open = () => this.props.open(this.props.data.id, this.props.data.name);
render() {
return (
<React.Fragment>
<div key={this.props.data.id}>
<button
onClick={() => this.open(this.props.data.id, this.props.data.name)}
>
(Age should Appear Here-- ({this.props.messages})--)
{this.props.data.name}
</button>
</div>
</React.Fragment>
);
}
}
class OpenedUser extends React.Component {
constructor(props) {
super(props);
this.state = {
hidden: false,
personId: '',
personAge: '',
};
}
componentDidMount(){
this.socket = io('http://localhost:8080');
var userId= this.props.data.id;
}
sendPost = (personId,personAge) => {
alert(personId);
alert(personAge);
this.socket.emit('messageUpdate', {
personId: personId,
personAge: personAge,
});
this.setState({personId: ''});
this.setState({personAge: ''});
}
toggleHidden = () =>
this.setState(prevState => ({ hidden: !prevState.hidden }));
close = () => this.props.close(this.props.data.id);
render() {
return (
<div key={this.props.data.id} style={{ display: "inline-block" }}>
<div className="wrap_head">
<button onClick={this.close}>close</button>
<div>user {this.props.data.id}</div>
<div>name {this.props.data.name}</div>
{this.state.hidden ? null : (
<div className="wrap">
<div className="wrap_body">Update Age Info</div>
<div> </div>
<div>
<label></label>
<input type="text" placeholder="personAge" value={this.state.personAge} onChange={ev => this.setState({personAge: ev.target.value})}/>
<br/>
<span onClick={ () => this.sendPost(this.props.data.id, this.state.personAge)} className="btn btn-primary">Update Age</span>
</div>
</div>
)}
</div>
</div>
);
}
}
class App extends React.Component {
constructor() {
super();
this.state = {
showingAlert_UserTyping: false,
shown: true,
activeIds: [],
messages: [],
data: [
{ id: 1, name: "user 1" },
{ id: 2, name: "user 2" },
{ id: 3, name: "user 3" },
{ id: 4, name: "user 4" },
{ id: 5, name: "user 5" }
]
};
}
componentDidMount(){
this.socket = io('http://localhost:8080');
this.socket.on('response message', function(data){
addAge(data);
console.log(' am add message' +data);
});
const addAge = data => {
console.log(data);
//this.setState({messages: [...this.state.messages, data]});
this.setState({messages: [data]});
};
} // close component didmount
toggle() {
this.setState({
shown: !this.state.shown
});
}
open = (id,name) => {
this.setState(prevState => ({
activeIds: prevState.activeIds.find(user => user === id)
? prevState.activeIds
: [...prevState.activeIds, id]
}));
};
close = id => {
this.setState(prevState => ({
activeIds: prevState.activeIds.filter(user => user !== id)
}));
};
renderUser = id => {
const user = this.state.data.find(user => user.id === id);
if (!user) {
return null;
}
return (
<OpenedUser messages={this.state.messages}
key={user.id}
data={user}
close={this.close}
/>
);
};
renderActiveUser = () => {
return (
<div style={{ position: "fixed", bottom: 0, right: 0 }}>
{this.state.activeIds.map(id => this.renderUser(id))}
</div>
);
};
render() {
return (
<div>
<ul>
{this.state.messages.map((message, i) => {
//if (message.personId == this.props.data.id) {
//if (message.personId == person.id) {
if (message.personId) {
return (
<div key={i}>
<div>
({message.personAge}--years)
</div>
</div>
)
}
})}
</ul>
{this.state.data.map(person => {
return (
<User key={person.id} data={person} open={this.open} />
);
})}
{this.state.activeIds.length !== 0 && this.renderActiveUser()}
</div>
);
}
}
Here is how I solved the issue:
I created a const resultdata and using map() and Filter() function.
Here is how I initialized the the variable resultdata and then pass it within state.data.map() method
const resultdata = this.state.messages.filter(res => res.personId == person.id).map(res => res.personAge));
I am a beginner in React. When I try to pass props from children to parent, the whole app is refreshed and the state recovery to initial. Is there any problem on my code? I have no idea how to solve it.
(ps: The following sentence is just for the number of words. Please don't see it. Why I have to add more details. If I have the ability to know every detail, I already solved it by myself)
Parent:
class App extends Component {
constructor(props) {
super(props);
this.state = {
stops: [],
legs: [],
driver: null,
finishedSign: false,
stopsSign: false,
legsSign: false,
driverSign: false
};
}
componentDidMount() {
console.log("-DID");
this.getStops();
this.getLegs();
this.getDriver();
}
// garentee all of data have received
checkFinished() {
const { stopsSign, legsSign, driverSign } = this.state;
const mark = stopsSign && legsSign && driverSign;
if (mark)
this.setState({
finishedSign: mark
});
}
// GET/STOPS API
getStops() {
fetch("/api/stops")
.then(res => res.json())
.then(stops => {
this.setState({ stops: stops, stopsSign: true }, () =>
console.log("stops fetched !", stops)
);
this.checkFinished();
});
}
// GET/LEGS API
getLegs() {
fetch("/api/legs")
.then(res => res.json())
.then(legs => {
this.setState({ legs: legs, legsSign: true }, () =>
console.log("driver fetched !", legs)
);
this.checkFinished();
});
}
// GET/Driver API
getDriver() {
console.log("-DRIVER");
fetch("/api/driver")
.then(res => {
return res.json();
})
.then(driver => {
this.setState(
{
driver: driver,
driverSign: true
},
() => console.log("driver fetched !", driver)
);
this.checkFinished();
});
}
// passing func
updateDriver(driver) {
console.log("update app!");
alert(driver);
}
renderMaps() {
return (
<Maps
stops={this.state.stops}
legs={this.state.legs}
driver={this.state.driver}
/>
);
}
renderDriverController() {
return (
<DiverController
legs={this.state.legs}
driver={this.state.driver}
update={this.updateDriver}
/>
);
}
render() {
return (
<div className="container">
<div className="row">
<div className="col-sm-3 col-md-3">
{this.state.finishedSign && this.renderDriverController()}
</div>
<div className="col-sm-8 col-md-8">
{
//this.state.finishedSign && this.renderMaps()
}
</div>
</div>
</div>
);
}
}
export default App;
children:
class DriverController extends Component {
constructor(props) {
super(props);
this.state = {
items: this.props.legs,
driver: this.props.driver
};
}
handleUpdate = e => {
const driver = null;
driver.activeLegID = this.refs.selectedLeg.value;
driver.legProgress = this.refs.selectedProgress.value;
if (driver.legProgress >= 0 && driver.legProgress <= 100)
this.props.update("test");
else alert("out of range!");
};
render() {
const { items, driver } = this.state;
console.log("items:", items);
return (
<form>
<hr />
<label>Driver Location:</label>
<div className="form-group">
<select
id="inputState"
className="form-control"
defaultValue={driver.activeLegID}
ref="selectedLeg"
>
{items.map(item => (
<option key={item.legID}>{item.legID}</option>
))}
</select>
<div className="input-group input-group-sm mb-3">
<div className="input-group-prepend">
<span className="input-group-text" id="inputGroup-sizing-sm">
Percentage:
</span>
</div>
<input
type="number"
className="form-control"
defaultValue={driver.legProgress}
ref="selectedProgress"
/>
</div>
<button onClick={this.handleUpdate} className="btn btn-primary">
Submit
</button>
<hr />
</div>
</form>
);
}
}
export default DriverController;
Try to use
onClick={() => this.handleUpdate}
You should not pass props from a child to its parent. Thats an anti-pattern.
You could pass a function from parent to child which will be triggered in
the child and hence updating the required state in the parent.
Refresh issue:
I think cause the child is wrapped inside a form.
Add
e.preventDefault() to your handleSubmit function to prevent the refresh
handleUpdate = e => {
e.preventDefault()
When a user enters a search item, if the data is available, then <Pictures /> is displayed. If the data is not present then <NoResultsFound /> is displayed.By default <NoResultsFound /> state is false and <Pictures /> is true because when the page loads the list of pictures are present. I tried to switch the state like this: this.setState({uisNoResultsFound: true}) and this.setState({uisPictures: false}) throws syntax error. I want this conditional rendering of the UI states within app.js. How to do this?
App.js:
class App extends Component {
constructor(props) {
super(props);
this.state = {
uisSearchBarItems: true,
uisNoResultsFound: false,
uisPictures: true,
dsPictures: []
};
}
componentDidMount() {
unsplash.search.collections("frog", 1, 60)
.then(toJson)
.then(json => {
this.setState({ dsPictures:json.results });
})
}
enteredDatahandler = (ctp) => {
unsplash.search.collections(ctp, 1, 60)
.then(toJson)
.then(json => {
this.setState({ dsPictures:json.results })
})
//******** conditional rendering ***********
if(this.state.dsPictures.length === 0){
return (
this.setState({uisNoResultsFound: true})
this.setState({uisPictures: false})
)
}
else{
this.setState({uisNoResultsFound: false})
this.setState({uisPictures: true})
}
//***********************************
}
render() {
return (
<div className="App">
<SearchBarItems ctpEnteredData={this.enteredDatahandler}/>
<NoResultsFound />
<Pictures ptcEnteredData={this.state.dsPictures}/>
</div>
);
}
}
export default App;
searchbaritems.js
class SearchBarItems extends Component {
enterKeyHandler = (event) => {
if (event.key === 'Enter'){
event.preventDefault();
this.props.ctpEnteredData(this.search.value)
}
}
render() {
return (
<div>
<form autoComplete="off" ref={(el) => this.myFormRef = el}>
<input
type="text"
name="search"
ref={input => this.search = input}
onKeyPress={this.enterKeyHandler}/>
</form>
</div>
)
}
}
Use a ternary expression inside your render method.
{this.state.dsPictures.length === 0 ? <NoResultsFound /> : <Pictures ptcEnteredData={this.state.dsPictures}/> }
In your render function you are returning both components, you need to either have if statements or you can do what #Barazu did - which is the most cleanest code.
Github gist: https://gist.github.com/tintinmovie/ed5b4782fa98c3482b561ea3243f98ea
render() {
if (this.state.uisNoResultsFound === true) {
return(
<div className="App">
<SearchBarItems ctpEnteredData={this.enteredDatahandler}/>
<NoResultsFound />
</div>
);
}
else if (this.state.uisPictures === true) {
return(
<div className="App">
<SearchBarItems ctpEnteredData={this.enteredDatahandler}/>
<Pictures ptcEnteredData={this.state.dsPictures}/>
</div>
);
}
}
I have a CheckboxGroup component which takes in an options array prop and generates CheckboxInput components. On page load I make a call to an API which returns an array of pre-selected checkboxes (delivered to the value prop). Depending on the logged in user, this call can return an empty array or a selection of previously selected checkbox options.
The following code successfully takes the response of the API call and sets the relevant checkboxes to 'checked'. The issue I have is that this code doesn't allow me to make changes to the checkboxes after page load (clicking a checkboxes has no effect).
I think there is also some disconnect between the initial selectedCheckboxes state and the value of the API call but I read that setting props as initial state is an anti-pattern (e.g. selectedCheckboxes: props.value,).
export default class CheckboxGroup extends Component {
constructor(props) {
super(props);
this.state = {
selectedCheckboxes: [],
};
}
addCheckboxToSelected = (id) => {
if (this.state.selectedCheckboxes.includes(id)) {
// Remove checkbox from array and update state
const filteredArray = this.state.selectedCheckboxes.filter(item => item !== id);
this.setState({ selectedCheckboxes: filteredArray });
} else {
// Add checkbox to array and update state
this.setState({ selectedCheckboxes: this.state.selectedCheckboxes.concat(id) });
}
}
checkIfSelected = (checkboxValue) => {
const preSelectedCheckboxes = this.props.value;
let selectedBool = false;
preSelectedCheckboxes.some(function(object) {
if (object.id === checkboxValue) {
selectedBool = true;
}
return false;
});
return selectedBool;
}
render() {
const { label, name, options } = this.props;
return (
<div className="form-group form-inline">
<span className="checkboxgroup-heading">{label}</span>
<div className="form-group-container">
{options.map(object => (
<CheckboxInput
key={object.value}
name={name}
label={object.label}
onChange={this.addCheckboxToSelected}
value={object.value}
checked={this.checkIfSelected(object.value)}
/>
))}
</div>
</div>
);
}
}
This is the stateless CheckboxInput component
const CheckboxInput = ({ name, label, onChange, value, checked }) => {
return (
<div className="field form-group filter-input">
<input
type="checkbox"
id={value}
name={name}
value={value}
onChange={() => onChange(value)}
checked={checked}
/>
<label htmlFor={value} className="form-control">{label}</label>
</div>
);
};
Check the following code snippet. This might help. Let me know if you have questions.
const CheckboxField = ({checked, onChange}) => {
return (
<input type="checkbox" checked={checked} onChange={ev => onChange(ev.target.checked)} />
);
};
class App extends React.Component {
constructor() {
super();
this.state = {
options: [{id: "1", checked: true}, {id: "2", checked: false}]
};
}
handleCheckboxChange(checked, option) {
const {options} = this.state;
var cOptions = [...options];
for(var i in cOptions) {
if(cOptions[i].id == option.id) {
cOptions[i].checked = checked;
}
}
this.setState({
options: cOptions
}, () => console.log(options));
}
render() {
const {options} = this.state;
return (
<div>
{
options.map(option => {
return (
<CheckboxField key={option.id} checked={option.checked} onChange={value => this.handleCheckboxChange(value, option)} />
)
})
}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>