How trigger function when specific state changed - reactjs

I'm looking for a way to associate a function with a change in a particular state.
I've written this code
export type MoviesAppState = {
search: string;
searchBy:string;
startDate: number;
endDate: number;
}
export class App extends React.PureComponent<{}, MoviesAppState> {
startDate = (new Date("2018-11-13")).getTime();
endDate = new Date().getTime()
state: MoviesAppState = {
search: '',
searchBy:"Select one",
startDate: this.startDate,
endDate: this.endDate,
}
onSearch = async (val: string, newPage?: number) => {
clearTimeout(this.searchDebounce);
this.searchDebounce = setTimeout(async () => {
let newMovies = await api.getMovies(val, this.state.searchBy, this.state.startDate, this.state.endDate);
this.setState({
movies: newMovies,
search: val
});
}, 300);
}
startDateChanged = (date: Date) => {
const startDateTimestamp = new Date(date).getTime()
this.setState({
startDate: startDateTimestamp,
});
}
endDateChanged = async (date: Date) => {
const endDateTimestamp = new Date(date).getTime()
this.setState({
endDate: endDateTimestamp,
});
}
onSearchBy = (searchByCriterion:string)=>{
this.setState({
searchBy: searchByCriterion
});
}
render() {
return (<main>
<h1>Movies</h1>
<div>
<header>
<input type="search" placeholder="Search..." onChange={(e) => this.onSearch(e.target.value)} />
</header>
<SearchByCriterion
searchByHandler={this.onSearchBy}
initialCriterion={this.state.searchBy}
/>
</div>
<div>
<span>Start date: </span><MyDatePicker initialDate={this.startDate} dateChangeHandler={this.startDateChanged} ></MyDatePicker>
</div>
</main>)
}
}
export default App;
Basically what I want is that onSearch method will fire when one of the things happens- startDateChanged or endDateChanged and not only when the search input changed
I thought about calling it within the methods and I get what I want but I feel it's not the best practice.

componentDidUpdate is what you are after: https://reactjs.org/docs/react-component.html#componentdidupdate
componentDidUpdate(_, prevState) {
const {startDate, endDate, searchBy} = this.state
if(startDate !== prevState.startDate || endDate !== prevState.endDate || searchBy !== prevState.searchBy){
// trigger onSearch
}
}

You can achieve this with a componentDidUpdate lifecycle hook, which runs whenever there's a props or state change and allows you to compare the change before taking an action. See docs
It should look something like this
componentDidUpdate(_prevProps, prevState) {
// Typical usage (don't forget to compare props):
if (this.state.startDate !== prevState.startDate ||
this.state.startDate !== prevState.startDate) {
this.search();
}
}

Related

Create a SearchBar where after a user submits the search, display all matches (or partial matches) to that title

My problem is, once I click the search button for an empty string, it should set the state for moviesSearched to movieTitles (the array of all movie titles). However, for some reason, it renders the last element in the array which in this case, is Ex Machina and I tried so many things to fix it but I just can't seem to find a solution. I'm guessing that the way I'm setting state in my filter with [movie] is wrong but I don't know what else to do.
import React from 'react';
import movies from './movieData';
const movieTitles = movies.map(movie => movie.title);
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
searchQuery: '',
searchedMovies: []
}
}
handleQuery = e => {
this.setState({
searchQuery: e.target.value
});
}
handleSearch = () => {
if (this.state.searchQuery === '') {
this.setState({ searchedMovies: movieTitles })
}
movieTitles.filter(movie => {
(movie.toLowerCase().includes(this.state.searchQuery.toLowerCase())) &&
this.setState({ searchedMovies: [movie] });
console.log(this.state.searchedMovies);
});
this.setState({
searchQuery: '',
});
}
render() {
return (
<div>
<input
type="text"
placeholder="Search for a movie.."
name="searchQuery"
value={this.state.searchQuery}
onChange={this.handleQuery}
/>
<button
onClick={this.handleSearch}
>
Search
</button>
<br />
<span>{this.state.searchedMovies}</span>
</div>
);
}
}
export default SearchBar;
var movies = [
{title: 'Mean Girls'},
{title: 'Hackers'},
{title: 'The Grey'},
{title: 'Sunshine'},
{title: 'Ex Machina'},
];
export default movies;
Issue
Well, I certainly think you've an issue with your filtering logic, after you reset the searchedMovies state back to the full movieTitles array you then run the filter. In the filter you have a side-effect that is setting the searchedMovies state to the currently iterated movie. Each iteration overwrites the previous state value. This is why you only see the last movie in the array, it was the last movie iterated!
handleSearch = () => {
if (this.state.searchQuery === '') {
this.setState({ searchedMovies: movieTitles })
}
movieTitles.filter(movie => {
(movie.toLowerCase().includes(this.state.searchQuery.toLowerCase())) &&
this.setState({ searchedMovies: [movie] }); // <-- overwrites previous update
console.log(this.state.searchedMovies);
});
this.setState({
searchQuery: '',
});
}
Solution
You should save the result of the filtering into state instead.
handleSearch = () => {
const { searchQuery } = this.state;
this.setState(
searchedMovies: movieTitles.filter(
movie => movie.toLowerCase().includes(searchQuery.toLowerCase())
),
searchQuery: '',
);
}
You are using setState wrong way. You filter inside setState like this:
this.setState({
searchedMovies: movieTitles.filter((movie) => {
return movie.toLowerCase().includes(this.state.searchQuery.toLowerCase());
}),
});

Unable to change the value in the object

My objective is to change the value of the object to true or false while onchanging the checkbox.
Object contains:
{
id: '12497wewrf5144',
name: 'ABC',
isVisible: 'false'
}
Here is the code:
import React, { Component } from 'react'
class Demo extends Component {
constructor(props) {
super(props)
this.state = {
demo: {}
}
}
componentDidMount() {
axios
.get('/api/random')
.then(res => {
this.setState({ demo: res.data?.[0] })
})
.catch(error => {
console.log(error)
})
}
render() {
return (
<div>
<h1>{this.state.demo.name}</h1>
<input type="checkbox" value={this.state.demo.value} />
</div>
)
}
}
export default Demo
I don't know what to write in onchange method for checkbox to only change the value within the object.
Can anyone help me in this query?
<input
type="checkbox"
value={this.state.demo.value}
onChange={(event) => {
this.setState((prevState) => ({
...prevState,
demo: { ...prevState.demo, isVisible: event.target.checked }
}));
}}
/>
Given your state ends up looking like
this.state = {
demo: {
id: "12497wewrf5144",
name: "ABC",
isVisible: "false",
value: false
}
};
You can create a change handler as such
changeHandler = e => {
e.preventDefault();
const { checked } = e.target;
this.setState(prevState => ({
...prevState, // <-- spread existing state
demo: {
...prevState.demo, // <-- spread existing demo
value: checked, // <-- save the input's checked value
}
}))
}
Attach the changeHandler to the onChange event callback
<input
type="checkbox"
onChange={this.changeHandler}
value={this.state.demo.value}
/>
Ciao, you could use onClick event like this:
...
handleClick = (e, data) => {
const demo = { ...this.state.demo };
demo.isVisible = !demo.isVisible;
this.setState({ demo });
}
...
<input type="checkbox" value={this.state.demo.value} onClick={((e) => this.handleClick(e, data))}/>
...

Why is AsyncSelect only showing options once?

I'm trying to make use of <AsyncSelect /> to pull options from an API to populate a form.
It works find the first time I search for something, I can select the option and am then prompted to type again.
However when I type something else, I get No Options even though I can see the data show up in a console.log().
import React, { Component } from 'react';
import AsyncSelect from 'react-select/async';
type State = {
inputValue: string,
};
const promiseOptions = inputValue =>
new Promise(resolve => {
setTimeout(() => {
resolve(
fetch('https://restcountries.eu/rest/v2/all')
.then(r => r.json())
.then(rjson => {
return rjson.map(c => {
return { name: c.name, label: c.name}
})
})
);
}, 1000);
});
export default class AsyncMulti extends Component<*, State> {
state = { inputValue: '' };
handleInputChange = (newValue: string) => {
const inputValue = newValue.replace(/\W/g, '');
this.setState({ inputValue });
return inputValue;
};
render() {
return (
<AsyncSelect
isMulti
loadOptions={promiseOptions}
/>
);
}
}
I just realized that the object being returned in the Promise:
{ name: c.name, label: c.name }
had the wrong keys, and should have been:
{ value: c.name, label: c.name }
And it works now.

I think render works twice

I'm only learning React, trying to write a simple TODO list app. When I'm trying to add a new task, two identical tasks are added. I tried to debug by the console.log element and saw a problem. render works twice, so my button sends info to the function twice. Can someone please guide me to the solution? Here is the code.
import React from 'react';
class TaskInput extends React.Component {
constructor(props) {
super(props);
this.state = {
input: ''
};
}
addTask = () => {
const { input } = this.state;
if (input) {
this.props.addTask(input);
this.setState({ input: '' });
}
};
handleEnter = event => {
if (event.key === 'Enter') this.addTask();
};
inputChange = event => {
this.setState({ input: event.target.value });
};
render() {
const { input } = this.state;
console.log(this.state);
return (
<div className="task-input">
<input
type="text"
onKeyPress={this.handleEnter}
onChange={this.inputChange}
value={input}
></input>
<button onClick={this.addTask } >ADD</button>
</div>
);
}
}
export default TaskInput;
Here is the App.js code:
import React from 'react';
import Task from './components/Task';
import TaskInput from './components/TaskInput';
class App extends React.Component {
constructor () {
super();
this.state = {
tasks: [
{id: 0, title: 'Create Todo-app', done: false},
{id: 1, title: 'Do smth else', done: true},
{id: 2, title: 'Do more things', done: false}
]
};
}
addTask = task => {
this.setState(state => {
let {tasks} = state;
console.log("state");
tasks.push({
id: tasks.length !==0 ? tasks.length : 0,
title: task,
done: false
});
return tasks;
});
}
doneTask = id => {
const index = this.state.tasks.map(task => task.id).indexOf(id);
this.setState(state => {
let {tasks} = state;
tasks[index].done = true;
return tasks;
});
};
deleteTask = id => {
const index = this.state.tasks.map(task => task.id).indexOf(id);
this.setState(state => {
let {tasks} = state;
delete tasks[index];
return tasks;
})
};
render() {
const { tasks } = this.state;
const activeTasks = tasks.filter(task => !task.done);
const doneTasks = tasks.filter(task => task.done)
return (
<div className = "App">
<h1 className="top">Active tasks: {activeTasks.length}</h1>
{[...activeTasks, ...doneTasks].map(task => (
<Task
doneTask={() => this.doneTask(task.id)}
deleteTask={() => this.deleteTask(task.id)}
task={task}
key={task.id}
></Task>))}
<TaskInput addTask={this.addTask}></TaskInput>
</div>
);
}
}
export default App;
I think you are accidentally directly modifying the state inside addTask.
The line let {tasks} = state; is creating a reference to the original state, rather than a new copy, and then your push modifies the state directly.
Using expansion/spread syntax to get a copy of your array like this should work:
addTask = task => {
this.setState(state => {
const tasks = [ ...state.tasks ];
tasks.push({
id: tasks.length !==0 ? tasks.length : 0,
title: task,
done: false
});
return { tasks };
});
}
Using let tasks = [ ...state.tasks ]; will create a new array rather than a reference, and prevent the state from being modified directly.
The reason you were seeing double results was that you effectively set the state with the push, and then set it again with the returned value.
I've changed your code a little bit. It's working here. Would you please check?
class TaskInput extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
tasks: []
};
}
addTask = newTask => {
this.setState(state => ({
...state,
input: "",
tasks: [...state.tasks, newTask]
}));
};
handleEnter = event => {
if (event.key === "Enter") this.addTask(event.target.value);
};
inputChange = event => {
this.setState({ input: event.target.value });
};
render() {
const { input } = this.state;
console.log(this.state);
return (
<div className="task-input">
<input
onKeyPress={this.handleEnter}
onChange={this.inputChange}
value={input}
></input>
<button onClick={this.addTask}>ADD</button>
</div>
);
}
}
ReactDOM.render(<TaskInput/>, document.querySelector("#root"));
.as-console-wrapper {
max-height: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

How to Create a Search Field in ReactJS

I'm new to react, and I'm working on a small project that uses a search bar to find data that I've gotten from my database.
The code for this component is below:
import React, { Component } from 'react';
class BodyData extends Component {
state = {
query: '',
data: [],
}
handleInputChange = () => {
this.setState({
query: this.search.value
})
this.filterArray();
}
getData = () => {
fetch(`http://localhost:4000/restaurants`)
.then(response => response.json())
.then(responseData => {
// console.log(responseData)
this.setState({
data:responseData
})
})
}
filterArray = () => {
var searchString = this.state.query;
var responseData = this.state.data
if(searchString.length > 0){
// console.log(responseData[i].name);
responseData = responseData.filter(l => {
console.log( l.name.toLowerCase().match(searchString));
})
}
}
componentWillMount() {
this.getData();
}
render() {
return (
<div className="searchForm">
<form>
<input type="text" id="filter" placeholder="Search for..." ref={input => this.search = input} onChange={this.handleInputChange}/>
</form>
<div>
{
this.state.data.map((i) =>
<p>{i.name}</p>
)
}
</div>
</div>
)
}
}
export default BodyData;
So basically, I want to update the state as I type in the query text, and have the restaurant names I've mapped be reduced till a match is found.
From what I understood, this.state.data will be filtered as I type in my query in the search bar. However when I map out this.state.data, I get the whole list of restaurants instead of what I want to see.
Ive been through a bunch of tutes, and I'm not exactly sure how to go about doing that.
Can anyone help me with this please? Any other comments on the code are also welcome. I'm here to learn :)
Thank you!
You could keep an additional piece of state called e.g. filteredData that contains all elements in data that include your query in the name, and then render that.
Example
class BodyData extends React.Component {
state = {
query: "",
data: [],
filteredData: []
};
handleInputChange = event => {
const query = event.target.value;
this.setState(prevState => {
const filteredData = prevState.data.filter(element => {
return element.name.toLowerCase().includes(query.toLowerCase());
});
return {
query,
filteredData
};
});
};
getData = () => {
fetch(`http://localhost:4000/restaurants`)
.then(response => response.json())
.then(data => {
const { query } = this.state;
const filteredData = data.filter(element => {
return element.name.toLowerCase().includes(query.toLowerCase());
});
this.setState({
data,
filteredData
});
});
};
componentWillMount() {
this.getData();
}
render() {
return (
<div className="searchForm">
<form>
<input
placeholder="Search for..."
value={this.state.query}
onChange={this.handleInputChange}
/>
</form>
<div>{this.state.filteredData.map(i => <p>{i.name}</p>)}</div>
</div>
);
}
}
Here is the code that will work for you
import React, { Component } from 'react';
class BodyData extends Component {
state = {
query: '',
data: [],
searchString:[]
}
handleInputChange = (event) => {
this.setState({
query: event.target.value
},()=>{
this.filterArray();
})
}
getData = () => {
fetch(`http://localhost:4000/restaurants`)
.then(response => response.json())
.then(responseData => {
// console.log(responseData)
this.setState({
data:responseData,
searchString:responseData
})
})
}
filterArray = () => {
let searchString = this.state.query;
let responseData = this.state.data;
if(searchString.length > 0){
// console.log(responseData[i].name);
responseData = responseData.filter(searchString);
this.setState({
responseData
})
}
}
componentWillMount() {
this.getData();
}
render() {
return (
<div className="searchForm">
<form>
<input type="text" id="filter" placeholder="Search for..." onChange={this.handleInputChange}/>
</form>
<div>
{
this.state.responseData.map((i) =>
<p>{i.name}</p>
)
}
</div>
</div>
)
}
}
export default BodyData;
There are few changes which is needed.
Set State is worked asynchronous.SO, to avoid it use arrow function when you need to do something immediately after set state.
there are 2 different keyword in es6 instead of var. Let and Const . use them instead of var.
There is no need of ref in input. you can directly get value of input by event.target.value
Enjoy Coding!!
setState method is asynchronous and it has second parameter - callback that is called when the state is applied.
You need to change handleInputChange methods.
handleInputChange = () => {
this.setState({
query: this.search.value
}, this.filterArray)
}
Few pointers I'll like to show-
setState({}) function is asynchronous, you'll have to either use functional setState and call filteredArray method as a callback. Or, call filteredArray at render, which is where your values will be updated and be reflected.
Your filterArray method is not returning / setting the filtered list of data. So what you type, even though it is getting filtered, it is not getting set / returned anywhere.

Resources