I have a simple component consisting of an input (jobId) and a span (jobDescription). When the user enters a jobId, the component makes an ajax call to fill in the description. Sounds easy. But here is my problem, the way I wrote it, the component makes a bunch of wasteful calls to lookup the jobDescription with each keypress.
I tried changing the lookup to onBlur instead of onChange, but then I miss the "initial load" (the initial render when the jobId is passed in from the parent). So jobDescription is blank when the form is first opened.
So here is what I need:
When the user types in a jobId, lookup the corresponding jobDescription. But not necessarily on every key stroke.
When the parent passes in a jobId, it should also lookup the corresponding jobDescription.
Here is the component as it is now:
import React from "react";
type Props = {
jobId:string,
onChange:(jobId: string)=>void
}
type State = {
description:string
}
export default class JobComponent extends React.Component {
props: Props;
state: State;
constructor(props: Props) {
super(props);
this.state = {
description: ""
}
}
componentWillReceiveProps(nextProps: Props) {
if (nextProps.jobId != this.props.jobId) {
this.loadDescription(nextProps.jobId)
.then((description)=> {
this.setState({description});
});
}
}
onChangeInternal = (event)=> {
const jobId = event.target.value;
this.props.onChange(jobId);
this.loadDescription(jobId)
.then((description)=> {
this.setState({description});
});
};
render() {
return <div>
<input className="job-id" value={this.props.jobId} onChange={this.onChangeInternal}/>
<span className="job-description">{this.state.description}</span>
</div>;
}
loadDescription = (jobId): Promise<string> => {
return new Promise((resolve) => {
if (!jobId) resolve("");
else
fetch('/components/job-picker/jobService.jsp?jobId=' + jobId)
.then((response) => {
return response.json();
})
.then((job) => {
resolve(job.description);
});
});
};
}
And here is a sample for the parent component:
import React from "react";
import JobComponent from "./JobComponent";
export default class FormTest extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "Dave",
jobId: "360107",
age: 50
}
}
render() {
const onNameChange = (event)=> {
this.setState({name: event.target.value});
};
const onAgeChange = (event)=> {
this.setState({age: event.target.value});
};
const onJobIdChange = (jobId)=> {
this.setState({jobId});
};
return (
<div>
Name<input value={this.state.name} onChange={onNameChange}/><br/>
JobId<JobComponent jobId={this.state.jobId} onChange={onJobIdChange}/><br/>
Age<input value={this.state.age} onChange={onAgeChange}/><br/>
</div>
);
}
}
So, assuming who understand what I'm trying to do, how would you write this component in React?
You should wait for a particular time before making a request. This is primarily delaying the request and wait for user input. Change your onChange handler to something like this
onChangeInternal = (event)=> {
const jobId = event.target.value;
this.props.onChange(jobId);
clearTimeout(this.timeoutId); // resets the timeout on new input
this.timeoutId = setTimeout(() => {
this.loadDescription(jobId)
.then((description)=> {
this.setState({description});
});
}, 500); //delays request by 500ms.
};
You should also check Lodash's Debounce. This will delay the function call for a given time.
Hope this helps!
Related
React & Firebase newbie here. I have a React component that needs to look up some stuff in Firebase before rendering. My database design requires first getting the correct doohick ids and subsequently looking up the doohick details, but I'm not sure how to do that with the asynchronous nature of Firebase database access. This doesn't work:
class Widget extends React.Component {
componentDidMount() {
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
});
this.state.doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
render() {
if (this.state.doohick-ids) {
return null;
} else {
// render the Doohick subcomponents
}
}
}
I can think of a few solutions here, but none that I like. What's the recommended way to chain together Firebase calls, or perhaps redesign this to eliminate the problem?
I think you should split one component Widget to two WidgetList and WidgetItem.
WidgetItem
subscribe and unsubscribe to firebase.database().ref(/doohick-details/${id})
class WidgetItem extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
}
constructor(props) {
super(props);
this.state = {};
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
const { id } = this.props;
this.dbRef = firebase.database().ref(`/doohick-details/${id}`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
// update state
this.setState(dataSnapshot);
}
render() {
return (
<pre>{JSON.stringify(this.state, null, 2)}</pre>
);
}
}
WidgetList
subscribe and unsubscribe to firebase.database().ref(/users/${username}/doohick-ids)
class WidgetItem extends React.Component {
constructor(props) {
super(props);
this.state = { doohick_ids: [] };
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
// Note: I've just copied your example. `username` is undefined.
this.dbRef = firebase.database().ref(`/users/${username}/doohick-ids`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
this.setState({ doohick_ids: dataSnapshot });
}
render() {
const { doohick_ids } = this.state;
if (doohick_ids.length === 0) {
return 'Loading...';
}
return (
<React.Fragment>
{doohick_ids.map(id => <WidgetItem key={id} id={id} />)}
</React.Fragment>
);
}
}
And code that requires the data from the database needs to be inside the callback that is invoked when that data is available. Code outside of the callback is not going to have the right data.
So:
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
});
There's many optimizations possible here, but they all boil down to the code being inside the callback and updating the state when a value comes from the database.
I want to validate the value that the user write in the input.
The browser works, creating a new room with the click of a button works, but the input doesn't change color according to the validation I set, why?
Inside addRoomName function I created setState for the value inside the room input
addRoomName=(e)=> {
this.setState({ room: e.target.value })
and additionally I created setState for the validation with the conditions
this.setState({ addRoomName: e.target.value });
if (e.target.value.length >= 6){
this.setState({roomNameInputColor:'green'})
} else {
this.setState({roomNameInputColor:'red'})
}
Is that may be the problem? because it seems that the react don't even recognize the validation but just the first setState (the one that bring the value that wrote in the room input)
So why the input doesn't change color?
I shared all the code
thanks!
App.js
import React, { Component } from 'react'
import './App.css';
import Addroom from './components/Addroom.js'
import Room from './components/Room.js'
export default class App extends Component {
state = {
roomsList:[{room:'',color:''}],
}
create = (r, c) => {
this.setState({ roomsList: [...this.state.roomsList, { room: r, color: c }] })
}
render() {
return (
<div>
<h1>My Smart House</h1>
{this.state.roomsList.map((element) => {
return <Room r={element.room} c={element.color} />
})}
<Addroom add={this.create}/>
</div>
)
}
}
Addroom.js
import React, { Component } from 'react'
export default class Addroom extends Component {
constructor(props) {
super(props)
this.state = {
roomNameInputColor:'white',
}
}
addRoomName = (e) => {
this.setState({ room: e.target.value })
this.setState({ addRoomName: e.target.value });
if (e.target.value.length >= 6) {
this.setState({ roomNameInputColor: 'green' })
} else {
this.setState({ roomNameInputColor: 'red' })
}
}
addColor = (e) => {
this.setState({ color: e.target.value })
}
createRoom = () => {
this.props.add(this.state.room, this.state.color);
}
render () {
return (
<div>
<input onChange={this.addRoomName} style={{ backgroundInputColor: this.state.roomNameInputColor }} placeholder='Name Your Room'/>
<br/>
<input onChange={this.addColor} placeholder='Whats The Room Color?'/>
<br/>
<button onClick={this.createRoom}>Create</button>
</div>
)
}
}
Room.js
import React, { Component } from 'react'
export default class Room extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
return (
<div>
<h1>Room: {this.props.r} </h1>
<h3>Color: {this.props.c} </h3>
</div>
)
}
}
In your addRoomName function, you are doing multiple setState in a row, where it's often a source of state confusions (that you are probably experiencing here).
Prefer to have a single call to the setState() method in your function like this:
addRoomName = (e) => {
const room = e.target.value;
let roomNameInputColor = '';
if (room.length >= 6) {
roomNameInputColor = 'green';
} else {
roomNameInputColor = 'red';
}
this.setState({ room, addRoomName: room, roomNameInputColor });
}
thanks everyone, now it works, I did like you send guys to have effective code and also I changed this
<input onChange={this.addRoomName} style={{backgroundInputColor:this.state.roomNameInputColor}} placeholder='Name Your Room'/><br/>
To this
<input onChange={this.addRoomName} style={{backgroundColor:this.state.roomNameInputColor}} placeholder='Name Your Room'/><br/>
Because backgroundColor is a reserved word and while I tried to fix the problem I didn't saw that little important thing.. thanks!
i have simple interactive app, and i want to render the CardList component, base on user search. the problem is i want to setTimeOut for the user search, and execute the function after 2000ms from when the user stoped typing.
here is the code, as you can see I managed to get it done, but its hacky and not really useful,im positive there is a better way to do this.
what I'm doing right now is to always change this.state.robots arrry, acording to the user input. notice the searchBox component has an input field
class App extends Component {
constructor(){
super();
this.state = {
robots: robots,
searchField: ''
}
}
onSearchange = (event) =>{
let timeOut = null;
this.setState({searchField: event.target.value,robots:robots});
event.target.onkeyup = (e) =>{
clearTimeout(timeOut);
timeOut = setTimeout(()=> {
const filterRobots = this.state.robots.filter(robot => {
return robot.name.toLowerCase().includes(this.state.searchField.toLowerCase());
})
this.setState({robots: filterRobots});
},2000);
}
}
render() {
return (
<div className = "tc">
<h1>RoboFriend</h1>
<SearchBox searchChange = {this.onSearchange}/>
<CardList robots = {this.state.robots} />
</div>
);
}
}
I would like to be able to send fillterRobots array dynamically to the CardList component so i can render the results properly
I would use something like lodash's debounce(). You don't just want a delay, you also want to cancel the previous delayed function execution if you receive a new event before the current delayed function has executed.
class TestComponent extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
this.delayedCallback = _.debounce(this.handleChange, 1000);
}
handleChange(value) {
this.setState({ value });
}
onChange(event) {
event.persist();
this.delayedCallback(event.target.value);
}
render() {
return (
<div>
<input onChange={this.onChange.bind(this)} />
<p>{this.state.value}</p>
</div>
);
}
}
I'm working on a form with interactive inputs. They have to actualise themselves with information into parent state.
I use Axios to get the data to show, getting them from an external API. I tried to set default values, but they never actualise with newer values.
class Form extends React.Component {
getData() {
axios.get('http://xxx/getform/').then(
res => this.setState(res.data)
);
}
componentDidMount() {
this.getData();
setInterval(() => {
this.getData();
}, 36000000)
}
render() {
return (
<div>
<form>
<DatePicker />
</form>
</div>
)
}
}
class DatePicker extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: new Date(),
runMin: new Date(),
runMax: new Date()
};
}
getDate() {
console.log('DAD');
try { // if axios didn't finish, to avoid undefined
this.setState({
runMin: super.state.RunMin,
runMax: super.state.RunMax})
} catch (e) {
this.setState({
runMin: new Date(),
runMax: new Date()})
}
}
componentDidMount() {
this.getDate();
this.setState({selected: this.state.runMax});
}
render() {
return (<div></div>);
}
}
Actually after axios call, the children doesn't rerender. I separated the call for axios and the component using it, because the Form component do a single call for multiple children (not displayed here), and they read the parent's state to render.
Firstly, you should not access the parents state using super and instead pass the required value as props
Secondly, componentDidMount lifecycle is executed on initial mount and hence the logic within it won't execute when the parent state updates.
The correct way to handle your case would be
class Form extends React.Component {
state = {
RunMin: new Date(),
RunMax: new Date()
}
getData() {
axios.get('http://xxx/getform/').then(
res => this.setState({RunMin: res.data.RunMin, RunMax: res.data.RunMax})
);
}
componentDidMount() {
this.getData();
setInterval(() => {
this.getData();
}, 36000000)
}
render() {
return (
<div>
<form>
<DatePicker runMin={this.state.RunMin} runMax={this.state.RunMax}/>
</form>
</div>
)
}
}
class DatePicker extends React.Component {
render() {
console.log(this.props.runMin, this.props.runMax);
return (<div></div>);
}
}
The way you are setting the state is incorrect
Change
this.setState(res.data);
To
this.setState({data: res.data});
You need to set the response to a state field you have in component and make sure you pass the data to the child component
I have a React app that queries an API that I built in Spring Boot. The React side should query the database to find a number of Activity objects to populate one Itinerary object. I want to keep track of the Ids of the activities so the app doesn’t return the same one more than once. Here’s how I set it up:
The Itinerary component has an array called "activities" in its state that will hold the ids. This starts out as empty. The Itinerary component also has a function called UpdateActivities() that updates the State with a new id when an activity is added.
UpdateActivities is passed as a callback to the child component Activity as props. The activities array in the state is turned into a string (because I'll be using this in the API call) and also passed as props to the Activity component.
In the Activity component, the callback function is further passed as a prop to the ActivityDetails component and the string of ids is further passed to ActivityDetails as well.
The ActivityDetails component makes the actual API call to my Spring Boot application and uses the callback function to update the State on the parent component. I attempted to save the string of ids to a constant to use in my API call. I want the string to go at the end of the call, so my app will know to skip over any activities with those Ids (currently hard-coded to 1).
The API call doesn't work if I replace 1 with the constant string of ids, because it shows up as empty. Is this an async issue? Is there a way to make it so the React app doesn't make the API call on ActivityDetails until the State is updated on the parent component?
Itinerary Component
class Itinerary extends Component {
constructor(props) {
super(props);
this.state = {
template: null,
activities: []
};
}
updateActivities = (id) => {
const activityList = [...this.state.activities, id];
this.setState({
activities: activityList
})
}
componentDidMount = () => {
const {duration, travelerType, pace} = this.props.userAnswers;
const transport = this.props.userAnswers.internalTravel.sort().join(', ');
const TEMPLATES_URL = `http://localhost:8080/templates/duration/${duration}/travelers/${travelerType}/pace/${pace}/transport/${transport}`;
axios.get(TEMPLATES_URL)
.then(response => {
this.setState({
template: response.data[0].content
});
})
.catch(function (error) {
console.log(error.message);
});
}
render() {
let cities;
if (this.state.template === null) {
cities = <div><h3>Loading...</h3></div>;
} else {
let data = this.state.template.split(", ");
cities = data.map((city, index) => {
return (
<section>
<City
key={index}
day={index + 1}
city={city}
/>
<Activity
key={`${city}${index}`}
day={index + 1}
lastDay={data.length}
city={city}
userAnswers={this.props.userAnswers}
updateActivityState={this.updateActivities}
activityList={this.state.activities.join(', ')}
/>
</section>
)
});
}
return (
<div>
<div className="row">
<div className="col s9">
<h3>{cities}</h3>
</div>
</div>
</div>
);
}
}
export default Itinerary;
Activity Component
class Activity extends Component {
render() {
let city = this.props.city;
if(this.props.day === 1) {
return (
<ActivityDetails
userAnswers={this.props.userAnswers}
city={city}
timeOfDay="evening"
handleActivityList={this.props.updateActivityState}
activities={this.props.activityList}/>
);
} else if (this.props.day === this.props.lastDay) {
return (
<ActivityDetails
userAnswers={this.props.userAnswers}
city={city} timeOfDay="morning"
handleActivityList={this.props.updateActivityState}
activities={this.props.activityList} />
);
} else {
return(
<section>
<ActivityDetails
userAnswers={this.props.userAnswers}
city={city}
timeOfDay="morning"
handleActivityList={this.props.updateActivityState}
activities={this.props.activityList} />
<ActivityDetails
userAnswers={this.props.userAnswers}
city={city}
timeOfDay="afternoon"
handleActivityList={this.props.updateActivityState}
activities={this.props.activityList} />
<ActivityDetails
userAnswers={this.props.userAnswers}
city={city}
timeOfDay="evening"
handleActivityList={this.props.updateActivityState}
activities={this.props.activityList} />
</section>
);
}
}
}
export default Activity;
ActivityDetails Component
class ActivityDetails extends Component {
constructor(props) {
super(props);
this.state = {
activity: 'Loading...',
};
}
componentDidMount() {
const {travelParty, budget, pace, sites} = this.props.userAnswers;
const cityTravel = this.props.userAnswers.cityTravel.sort().join(', ');
const interests = this.props.userAnswers.interests.sort().join(', ');
const entertainment = this.props.userAnswers.entertainment.sort().join(', ');
const currentActivities = this.props.activities;
console.log(`currentActivities: ${currentActivities}`);
const city = this.props.city;
const timeOfDay = this.props.timeOfDay;
const ACTIVITY_URL = `http://localhost:8080/filter/${city}/${timeOfDay}/${travelParty}/${budget}/${pace}/${sites}/${cityTravel}/${interests}/${entertainment}/1`;
console.log(ACTIVITY_URL);
axios.get(ACTIVITY_URL)
.then(response => {
const newActivity = response.data.content;
const updatedActivityId = response.data.id;
this.props.handleActivityList(updatedActivityId);
this.setState({
activity: newActivity,
});
})
.catch(function(error) {
console.log(error.message);
});
}
render () {
return (
<div>{this.state.activity}</div>
);
}
}
export default ActivityDetails;
Revised with ComponentDidMount:
class ActivityDetails extends Component {
constructor(props) {
super(props);
this.state = {
activity: 'Loading...',
};
}
getActivity() {
const {travelParty, budget, pace, sites} = this.props.userAnswers;
const cityTravel = this.props.userAnswers.cityTravel.sort().join(', ');
const interests = this.props.userAnswers.interests.sort().join(', ');
const entertainment = this.props.userAnswers.entertainment.sort().join(', ');
const city = this.props.city;
const timeOfDay = this.props.timeOfDay;
const currentActivities = this.props.activities;
console.log(`currentActivities: ${currentActivities}`);
const ACTIVITY_URL = `http://localhost:8080/filter/${city}/${timeOfDay}/${travelParty}/${budget}/${pace}/${sites}/${cityTravel}/${interests}/${entertainment}/${currentActivities}`;
console.log(ACTIVITY_URL);
axios.get(ACTIVITY_URL)
.then(response => {
const newActivity = response.data.content;
const updatedActivityId = response.data.id;
this.props.handleActivityList(updatedActivityId);
this.setState({activity: newActivity});
})
.catch(function(error) {
console.log(error.message);
});
}
componentDidMount() {
this.getActivity();
}
componentDidUpdate(prevProps) {
if(prevProps.activities !== this.props.activities) {
this.getActivity();
}
}
render () {
return (
<div>{this.state.activity}</div>
);
}
}
export default ActivityDetails;
The API call doesn't work if I replace 1 with the constant string of ids, because it shows up as empty. Is this an async issue?
This makes sense, yes. It's likely doing your Itinerary's fetch at roughly the same time as your ActivityDetails' fetch, so the state.activities there is still an empty array (ergo empty string once you've mutated it and passed it down).
Is there a way to make it so the React app doesn't make the API call on ActivityDetails until the State is updated on the parent component?
Yes, you can use other lifecycle methods to facilitate this. In your case, you probably want componentDidUpdate.
You could do something like:
class ActivityDetails extends Component {
constructor(props) {
super(props);
this.state = {
activity: 'Loading...',
};
}
doMyGet(values) {
const ACTIVITY_URL = `http://localhost:8080/filter/${city}/${timeOfDay}/${travelParty}/${budget}/${pace}/${sites}/${cityTravel}/${interests}/${entertainment}/1`;
console.log(ACTIVITY_URL);
axios.get(ACTIVITY_URL)
.then(response => {
const newActivity = response.data.content;
const updatedActivityId = response.data.id;
this.props.handleActivityList(updatedActivityId);
this.setState({
activity: newActivity,
});
})
.catch(function(error) {
console.log(error.message);
});
}
componentDidMount() {
const {travelParty, budget, pace, sites} = this.props.userAnswers;
const cityTravel = this.props.userAnswers.cityTravel.sort().join(', ');
const interests = this.props.userAnswers.interests.sort().join(', ');
const entertainment = this.props.userAnswers.entertainment.sort().join(', ');
const currentActivities = this.props.activities;
console.log(`currentActivities: ${currentActivities}`);
const city = this.props.city;
const timeOfDay = this.props.timeOfDay;
this.doMyGet(currentActivities)
}
componentDidUpdate(prevProps) {
if (this.props.activities !== prevProps.activities) {
this.doMyGet(this.props.activities)
}
}
render () {
return (
<div>{this.state.activity}</div>
);
}
}
export default ActivityDetails;