I'm developing an application that has the following component hierarchy (courtesy ProReact)
KanbanContainer => KanbanBoard => List => Card => CheckList
KanbanContainer contains methods that need to be passed down to the CheckList component (as that component has all the ui controls). The methods in the KanbanContainer are defined as follows
class KanbanBoardContainer extends Component {
state = { cards: [] };
componentDidMount() {
this.setState({ cards: API.getTasks() });
}
addTask = (cardId, taskName) => {
console.log(taskName, " invoked for cardId =", cardId);
};
deleteTask = (cardId, taskId, taskIndex) => {
console.log("deleteTask invoked for cardId = ", cardId);
};
toggleTask = (cardId, taskId, taskIndex) => {
console.log("toggleTask invoked fpr cardId = ", cardId);
};
render() {
return (
<KanbanBoard
cards={this.state.cards}
taskCallbacks={{
toggleTask: this.toggleTask,
addTask: this.addTask,
deleteTask: this.deleteTask
}}
/>
);
}
}
In all the other components, the taskCallbacks is simply passed on via props. For example:
class List extends React.Component {
render() {
let cards = this.props.cards.map(c => {
return (
<Card
id={c.id}
key={c.id}
title={c.title}
description={c.description}
color={c.color}
tasks={c.tasks}
taskCallbacks={this.props.taskCallbacks}
/>
);
});
return (
<div className="list">
<h1>{this.props.title}</h1>
{cards}
</div>
);
}
}
In the final component, the functions passed in via props are attached to the ui controls such as checkbox and a link.
class CheckList extends Component {
checkInputKeyPress = event => {
if (event.key === "Enter") {
this.props.taskCallbacks.addTask(this.props.cardId, event.target.value);
event.target.value = "";
}
};
render() {
const { deleteTask, toggleTask } = this.props.taskCallbacks;
let tasks = this.props.tasks.map((t, index) => (
<li key={t.id}>
<input
type="checkbox"
name=""
id=""
defaultChecked={t.done}
onChange={toggleTask(this.props.cardId, t.id, index)}
/>{" "}
{t.name}{" "}
<a
href="#"
onClick={deleteTask(this.props.cardId, t.id, index)}
/>
</li>
));
return (
<div>
<ul>{tasks}</ul>
<input
type="text"
placeholder="Key in a task and hit enter"
onKeyPress={this.checkInputKeyPress}
/>
</div>
);
}
}
However when I load the application, the functions get called "on load" and nothing happens when the controls are clicked. Only the addTask() gets called when you type in the textfield and hit enter. What am I missing?
By using:
onClick={deleteTask(this.props.cardId, t.id, index)}
The function will be invoked in place. Try switching for:
onClick={() => deleteTask(this.props.cardId, t.id, index)}
For clarity, deleteTask is a reference to a function, deleteTask() invokes a function. In a situation where you need to invoke the function (for example, to pass arguments) then the pattern above is an anonymous function that calls your deleteTask function.
Create a function in the last component that calls the prop function with the appropriate arguments. That is, don't invoke a function directly from onClick/onChange, becase those props expect a reference to a function, not the result of invoking a function.
Most importantly, you should check out the Context API in order to avoid passing down so many props.
Related
Got a question - the App function below has a 'filteredUsers' method that gets executed every time a pass is made - so when a search state is set it actually runs the method and its result is passed as a prop to the 'List' functional component and all is well. How do I change this into an older style React.Component so this still works? (as per my attempt below)
const App = () => {
const [text, setText] = React.useState('');
const [search, setSearch] = React.useState('');
const handleText = (event) => {
setText(event.target.value);
};
const handleSearch = () => {
setSearch(text);
};
console.log('*** App ***'); // each time as I type this is shown
const filteredUsers = users.filter((user) => {
console.log('Filter function is running ...'); // each time this is shown
return user.name.toLowerCase().includes(search.toLowerCase());
});
return (
<div>
<input type="text" value={text} onChange={handleText} />
<button type="button" onClick={handleSearch}>
Search
</button>
<List list={filteredUsers} />
</div>
);
};
const List = ({ list }) => {
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
};
const ListItem = ({ item }) => {
return <li>{item.name}</li>;
};
Then in this React.Component equivalent (App3) I am trying this:
Now how do I get a filtered list passed to the List component when I hit the search button?
class App3 extends React.Component {
state = {
text: '',
search: '',
}
handleText(event) {
this.setState({ text: event.target.value });
}
handleSearch() {
this.setState({ search: this.state.text });
}
filteredUsers = users.filter((user) => {
console.log('Filter function is running ...');
return user.name.toLowerCase().includes(this.state.search.toLowerCase());
});
render() {
return (
<div>
<input type="text" value={this.state.text} onChange={this.handleText.bind(this)} />
<button type="button" onClick={this.handleSearch.bind(this)}>
Search
</button>
<List list={this.filteredUsers} />
</div>
)
}
}
In your first version, since your component renders, the filteredUsers variable gets updated in every render, so you get the filtered data. You can use useMemo there also to make it slightly better.
In your second (class component) version, this variable is not getting updated. So, you can make it a function and invoke it to pass the list prop:
filteredUsers = () => // make function
users.filter((user) => {
console.log("Filter function is running ...");
return user.name.toLowerCase().includes(this.state.search.toLowerCase());
});
render() {
return (
<div>
<input
type="text"
value={this.state.text}
onChange={this.handleText.bind(this)}
/>
<button type="button" onClick={this.handleSearch.bind(this)}>
Search
</button>
<List list={this.filteredUsers()} /> // invoke
</div>
);
}
or you can move it into the render method and assign to a variable:
render() {
const filteredUsers = users.filter((user) => {
console.log("Filter function is running ...");
return user.name.toLowerCase().includes(this.state.search.toLowerCase());
});
return (
<div>
<input
type="text"
value={this.state.text}
onChange={this.handleText.bind(this)}
/>
<button type="button" onClick={this.handleSearch.bind(this)}>
Search
</button>
<List list={filteredUsers} />
</div>
);
}
Though, if it is not mandatory, you should go with the first version since class components are not the way to go for a new component anymore.
Here is the version with useMemo:
const filteredUsers = React.useMemo(
() =>
users.filter((user) => {
console.log("Filter function is running ..."); // each time this is shown
return user.name.toLowerCase().includes(search.toLowerCase());
}),
[search]
);
In this case, this variable is only evaluated when search changes instead of every state change.
In my website, it currently shows users a list of movies based on their input.
When user clicks on the title of the movie that is rendered, I want to setState(chosenOne: the movie title they clicked).
Currently, when I click on movie title, it returns an error stating the following:
Uncaught TypeError: Cannot read property 'onClick' of undefined
at onClick (Fav.js:62)
Any way to fix this?
Any help is greatly appreciated.
import React, { Component } from 'react'
import axios from 'axios';
import '../styles/Rec.scss'
export class Fav extends Component {
constructor (props) {
super (props)
this.state = {
inputOne: '',
chosenOne: '',
movies:[],
};
}
onChangeOne = (event) => {
this.setState({
inputOne: event.target.value
},()=>{
if(this.state.inputOne && this.state.inputOne.length > 1) {
this.getInfo()
} else {
}
})
}
onClick = (event) =>{
this.setState({
chosenOne: event.currentTarget.textContent
})
console.log(this.state.chosenOne)
}
onSubmit = (event) => {
event.preventDefault();
}
getInfo = () => {
let url = `https://api.themoviedb.org/3/search/movie?api_key=''&language=en-US&query='${this.state.inputOne}'&page=1&include_adult=false`
axios.get(url)
.then(res => {
if (res.data) {
const movieData = res.data.results.filter(movie => movie.poster_path != null);
this.setState({movies: movieData});
}
console.log(this.state.movies)
})
}
render() {
return (
<div>
<h1>Favorite Movie of All Time</h1>
<form onSubmit={this.onSubmit}>
<input onChange={this.onChangeOne}/>
<div className="rec__container">
{this.state.movies && this.state.movies.slice(0,3).map(function(movie, genre_ids) {
return(
<div className="rec__sample">
<img className="rec__img" src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} alt="movie poster"/>
<p onClick={event => this.onClick(event)}>{movie.title}</p>
</div>
)
})}
</div>
</form>
</div>
)
}
}
export default Fav
I am quite sure the problem is that this in this.onClick is undefined. This happens when it is not bound correctly to the the class.
I would recommend to change the function declared after map to an arrow function.
<div className="rec__container">
{this.state.movies &&
this.state.movies.slice(0, 3).map((movie, genre_ids) => {
return (
<div className="rec__sample">
<img className="rec__img" src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} alt="movie poster" />
<p onClick={event => this.onClick(event)}>{movie.title}</p>
</div>
);
})}
</div>;
Also, you are binding onClick function in your constructor as well as using it as an arrow function. Bind does not work on arrow functions.
Either remove this.onClick = this.onClick.bind(this) or convert onClick into a simple function rather than an arrow function.
I'm not yet a React master, hence my question. Why there is still invoking a parent function if in child component I'm writing new characters in input fields? I want to call parent method only when I clicked Search button in my child component.
Parent component:
class MainPage extends Component {
render() {
let searchOffersBar = (
<MuiThemeProvider>
<SearchOffer
offersFound={this.props.onOffersFound}
/>
</MuiThemeProvider>
);
let searchResults = (
<SearchResults
offers={this.props.offers}
/>
);
return (
<Aux>
<div className={classes.container}>
<Intro/>
<div className={classes.contentSection}>
{searchOffersBar}
{searchResults}
</div>
</div>
</Aux>
)
}
}
const mapStateToProps = state => {
return {
offers: state.offers.offers
}
}
const mapDispatchToProps = dispatch => {
return {
onOffersFound: (searchParams) => dispatch(actions.fetchOffersByCriteria(searchParams))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MainPage);
<SearchOffer> is my child component with a search section (input fields and button "Search offers"). I want to fill some data in my inputs and then click the button. I though that clicking the button will invoke a method in child component: onOffersFound:
const searchOffer = props => {
let currentDate = new Date();
const [searchCriteria, setSearchCriteria] = useState({
brand: 'xxx',
capacity: 100
})
const [drawerIsOpen, setDrawerIsOpen] = useState(false);
const handleToggle = () => setDrawerIsOpen(!drawerIsOpen);
const handleBrand = (event) => {
let mergedState = updateObject(searchCriteria, {brand: event.target.value})
setSearchCriteria(mergedState);
}
const handleCapacity = (event) => {
let mergedState = updateObject(searchCriteria, {capacity: event.target.value});
setSearchCriteria(mergedState);
}
const handleBookingFrom = (bookingFromValue) => {
let mergedState = updateObject(searchCriteria, {bookingFrom: bookingFromValue});
setSearchCriteria(mergedState);
}
const handleBookingTo = (bookingToValue) => {
let mergedState = updateObject(searchCriteria, {bookingTo: bookingToValue});
setSearchCriteria(mergedState);
}
return (
<div className={classes.sideNav}>
<Button variant={"outlined"} onClick={handleToggle} className={classes.sideNavBtn}>Search</Button>
<Drawer
className={classes.drawer}
containerStyle={{top: 55}}
docked={false}
width={200}
open={drawerIsOpen}
onRequestChange={handleToggle}
>
<AppBar title="Search"/>
<form noValidate autoComplete="off" onSubmit={props.offersFound(searchCriteria)}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Grid container justify="space-around">
<TextField
id="brand"
label="Brand"
margin="normal"
onChange={handleBrand}
/>
<TextField
id="capacity"
label="Capacity"
margin="normal"
onChange={handleCapacity}
/>
<Button variant="contained" color="primary">
Search
</Button>
</Grid>
</MuiPickersUtilsProvider>
</form>
</Drawer>
</div>
);
}
export default searchOffer;
onOffersFound in my action creator looks like:
export const fetchOffersByCriteria = (searchParams) => {
return dispatch => {
let queryParams = '?brand='+searchParams.brand + '&capacity='+searchParams.capacity;
axios.get('/getFilteredOffers' + queryParams)
.then(response => {
dispatch(saveFoundOffers(response.data)); --> saves the state
})
.catch(error => {
console.log(error);
})
}
}
My question is why the above method fetchOffersByCriteria is invoked every time I enter new character in my child component? I want to invoke this method only when I click the Search button in child component. Maybe my approach is bad?
Thanks for all tips!
The issue is that props.offersFound(searchCriteria) is being invoked every render. The onSubmit prop should be a function to be invoked when submitted. Currently, it's being invoked immediately.
This line:
onSubmit={props.offersFound(searchCriteria)}
Should be (or something similar):
onSubmit={() => props.offersFound(searchCriteria)}
Currently, when typing in the brand (or capacity) field, the handleBrand change callback is invoked. This invokes setSearchCriteria (a state update) which triggers a re-render of the component. While this component is re-rendering, it's immediately invoking props.offersFound(searchCriteria) and passing the return value to the onSubmit prop. You likely want the onSubmit prop to be a function to be invoked at the time of submitting.
See the documentation for controlled components for more de3tails.
<form
noValidate
autoComplete="off"
onSubmit={props.offersFound(searchCriteria)}>
You are immediately invoking prop and trying to use result returned as event listener. It should be
<form
noValidate
autoComplete="off"
onSubmit={() => props.offersFound(searchCriteria)}>
instead
I have a component inside my App that processes data from an API and uses it to dynamically create children (which are defined in their own components) based on one of the parameter values from the API call. I am able to get the data through to render the components, but the onClick methods don't work. Here's some code:
The parent Component(within the return):
{this.state.customInputs.map((input, i) => {
if(input.type === 'radio'){
return(
<div className="preference-section" key={i}>
<h2>{input.name}</h2>
<RadioCustomInput
radioActiveState={this.state.radioActiveState}
handleRadioClick={() => this.handleRadioClick()}
options={input.options} />
</div>
)
} else if ( input.type === 'checkbox'){
return(
<div className="preference-section" key={i}>
<h2>{input.name}</h2>
<CheckboxCustomInput
selectedBoxes={this.state.selectedCheckboxes}
handleOnClick={() => this.handleCheckboxClick()}
options={input.options} />
</div>
)
}
})
}
The children components:
class CheckboxCustomInput extends Component {
handleCheckboxOnClick = (e) => {
e.preventDefault();
let checkboxState = e.target.value;
console.log(checkboxState);
return this.props.handleCheckboxClick(checkboxState);
}
render(){
return(
<div className="preference-options">
{this.props.options.map((item, i) => {
return(
<div key={i} className="checkbox-group">
<label
className={this.props.selectedBoxes.includes(item) ? "active-box checkbox-label" : "checkbox-label"}
htmlFor={item}>
<input
className="checkbox-input"
type="checkbox"
value={item}
id={item + '_checkbox'}
onClick={() => this.handleCheckboxOnClick()} />
{item}
</label>
</div>
);
})}
</div>
)
}
}
export default CheckboxCustomInput;
and the other:
class RadioCustomInput extends Component{
handleRadioOnClick = (e) => {
e.preventDefault();
let radioState = e.target.value;
let radioName = e.target.name;
console.log(radioState);
return this.props.handleRadioClick(radioState, radioName);
}
render(){
return(
<div className="radio-group">
{this.props.options.map((item, i) => {
return(
<label
className={this.props.radioActiveState === item ? "active-box radio-label" : "radio-label"}
htmlFor={item + '-card'} key={i}>
<input
className="radio-input"
type="radio"
name={item}
value={item}
id={item + '_radio'}
onClick={() => this.handleRadioOnClick()} />
{item}
</label>
)
})}
</div>
)
}
}
export default RadioCustomInput;
Is the radio working but the checkbox isn't? It looks like you're passing a prop called handleOnClick to your checkbox component, but expecting a handleCheckboxClick prop.
After editing the method prop invocation in the Checkbox component, I was able to pass the event through using the following tweak to my onClick method for each child: onClick={(e) => this.handleCheckboxOnClick(e)}
I also, as suggested by #larz, removed the function return in the parent and passe dthe method by reference only there.
The rest of my issue was CSS, in that the <input/> had a css rule of visibility:hidden and the <label/> element was handling the CSS visual interaction withthe onClick. To fix this, I moved the onClick method to the <label/> and added a native JS method to the chain for the property definitions within the child-local onClick methods, like so:
let radioState = e.target.childNodes[0].value;
I created a basic interface using checkboxes that used a react design pattern that has served me well before and that I thought worked well - namely lifting up state and passing down props to UI components. My checkbox components are passed a value(a metric), an state-changing method, and a boolean for checked. The problem is that the checkboxes do not update immediately, even though you can see them updating in the React dev tools. They only update on the next click, as in when another checkbox is checked. Here is the code:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
metricsSelected: []
}
this.selectMetric = this.selectMetric.bind(this)
}
selectMetric(metric) {
const metricsSelected = this.state.metricsSelected
const index = metricsSelected.indexOf(metric)
if (index !== -1){
metricsSelected.splice(index, 1)
}
else {
metricsSelected.push(metric)
}
this.setState({
metricsSelected,
});
}
render() {
return (
<div>
<Sidebar
metricsSelected={this.state.metricsSelected}
selectMetric={this.selectMetric}/>
<SomethingElse/>
</div>
)
}
}
const SomethingElse = () => (<div><h2>Something Else </h2></div>)
const Sidebar = ({ metricsSelected, selectMetric }) => {
const metrics = ['first thing', 'second thing', 'third thing']
return (
<div>
<h3>Select Metrics</h3>
{ metrics.map( (metric, i) =>
<Checkbox
key={i}
metric={metric}
selectMetric={selectMetric}
checked={metricsSelected.includes(metric)}/>
)}
</div>
)
}
const Checkbox = ({ metric, selectMetric, checked }) => {
const onChange = e => {
e.preventDefault()
selectMetric(e.target.value)
}
return (
<ul>
<li>{metric}</li>
<li><input
type='checkbox'
value={metric}
checked={checked}
onChange={onChange} /></li>
</ul>
)
}
I've read pretty much everything I can get my hands on about checkboxes for react and most of the applications of the checkbox are doing something different from what I want to do. I've tried adding state to the Checkbox component, but that didn't seem to help, since the checked value still needs to come in from elsewhere. I thought react components rerendered when the props changed. What gives?
Here's a codepen: https://codepen.io/matsad/pen/QpexdM
Here is a working version: http://codepen.io/TLadd/pen/oWvOad?editors=1111
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
metricsSelected: {}
}
this.selectMetric = this.selectMetric.bind(this)
}
selectMetric(metric) {
this.setState(({ metricsSelected }) => ({
metricsSelected: {
...metricsSelected,
[metric]: !metricsSelected[metric]
}
}))
}
render() {
return (
<div>
<Sidebar
metricsSelected={this.state.metricsSelected}
selectMetric={this.selectMetric}/>
<SomethingElse/>
</div>
)
}
}
const SomethingElse = () => (<div><h2>Something Else </h2></div>)
const Sidebar = ({ metricsSelected, selectMetric }) => {
const metrics = ['first thing', 'second thing', 'third thing']
return (
<div>
<h3>Select Metrics</h3>
{ metrics.map( (metric, i) =>
<Checkbox
key={i}
metric={metric}
selectMetric={selectMetric}
checked={Boolean(metricsSelected[metric])}/>
)}
</div>
)
}
const Checkbox = ({ metric, selectMetric, checked }) => {
return (
<ul>
<li>{metric}</li>
<li>
<input
type='checkbox'
name={metric}
checked={checked}
onChange={() => selectMetric(metric)}
/>
</li>
</ul>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
The couple of things that were causing issues were that you were mutating state in selectMetric, and your checkbox input's onChange function is using e.target.value instead of e.target.checked.
I changed the metricsSelected state to be an object, since I think it makes the management of it quite a bit easier.