This question already has answers here:
Basic React form submit refreshes entire page
(4 answers)
Closed 3 years ago.
I am pretty new to React so apologies if this is a dumb question, which I suspect it is.
I have a simple React app with a dropdown, button and list. When the button is clicked, the selected item in the dropdown is added to the list. Each item added to the list also has a delete button associated with it.
I need the SelectComponent (dropdown and button) and ListComponent (list and buttons) to know what the items in the list are so they can add/remove items from it, so I am storing the state in the parent App component and passing it down to the children as props, along with a callback function that can update it (using setState()). Here is what I have:
Select Component
class SelectComponent extends Component<SelectProps, {}> {
constructor(props: any) {
super(props);
this.changeHandler = this.changeHandler.bind(this);
this.clickHandler = this.clickHandler.bind(this);
}
changeHandler(event: ChangeEvent<HTMLSelectElement>) {
currentSelection = event.target.value;
}
clickHandler(event: MouseEvent<HTMLButtonElement>) {
this.props.selectedItems.push(currentSelection);
this.props.updateList(this.props.selectedItems);
}
render() {
let optionItems = this.props.options.map((optionItem, index) =>
<option>{optionItem}</option>
);
return (
<form>
<div>
<select onChange={this.changeHandler}>
<option selected disabled hidden></option>
{optionItems}
</select>
<br />
<button type="submit" onClick={this.clickHandler}>Add to list</button>
</div>
</form>
);
}
}
List Component
class ListComponent extends Component<ListProps, {}> {
constructor(props: any) {
super(props);
this.removeListItem = this.removeListItem.bind(this);
}
removeListItem(i: number) {
this.props.selectedItems.filter((selection, j) => i !== j);
this.props.updateList(this.props.selectedItems);
}
render() {
let listItems;
if (this.props.selectedItems) {
listItems = this.props.selectedItems.map((listItem, index) =>
<li>{listItem}<button onClick={() => this.removeListItem(index)}>Delete</button></li>
);
}
return (
<div>
<ul>
{listItems}
</ul>
</div>
);
}
}
main App
class App extends Component<{}, State> {
constructor(props: any) {
super(props);
this.state = {
selectedItems: []
}
this.updateList = this.updateList.bind(this);
}
updateList(selectedItems: string[]) {
this.setState({selectedItems});
}
render() {
return (
<div>
<SelectComponent options={["Cyan", "Magenta", "Yellow", "Black"]} selectedItems={this.state.selectedItems} updateList={this.updateList} />
<ListComponent selectedItems={this.state.selectedItems} updateList={this.updateList} />
</div>
);
}
}
I also have a couple interfaces defining the props and state as well as a variable to hold the currently selected item in the dropdown.
What I want to happen is: the "Add to list" button is pressed, adding the current dropdown selection to the props, then passing the props to the updateList() function in the parent class, updating the state. The parent class should then re-render itself and the child components according to the new state. From what I can tell by looking at the console, this does happen.
However for some reason after it gets done rendering the ListComponent, the app completely reloads, clearing the state and the list and returning the dropdown to it's default value. I can tell because I see Navigated to http://localhost:3000/? in the console right after the ListComponent render function is called.
So what have I done wrong? Again I am pretty new to React so I have a feeling this is something simple I am missing. Any help would be greatly appreciated!
Edit: Forgot to mention (although it is probably obvious) that I am coding this in TypeScript, although I don't think that is related the issue.
If you are working with form and submit handler then you have to set the event.preventDefault() in your submit method.
You have to set preventDefault() in clickHandler method.
clickHandler(event) {
event.preventDefault(); // set it...
this.props.selectedItems.push(currentSelection);
this.props.updateList(this.props.selectedItems);
}
because you're using form and button type submit. Type submit is reason your app reload.
To prevent app reload by default, in the clickHandler function of Select Component, add preventDefault in the top of function
event.preventDefault();
Related
I have currently a drop-down select to filter some charts after 'Apply'. It works fine.(See screenshot below).
The problem is that when another timespan gets selected, React does a re-render to all charts before I click 'Apply' button.
I want to avoid this unnecessary re-render by implementingshouldComponentUpdate, but I can't figure out how.
Below what I tried but it did not work(still a re-render):
shouldComponentUpdate(nextState) {
if (this.state.timespanState !== nextState.timespanState) {
return true;
}
return false;
}
But it always return true, because nextState.timespanState is undefined. Why?
Drop-down Select
<Select value={this.state.timespanState} onChange={this.handleTimeSpanChange}>
handleTimeSpanChange = (event) => {
this.setState({ timespanState: event.target.value });
};
constructor(props) {
super(props);
this.state = { timespanState: 'Today'};
this.handleTimeSpanChange = this.handleTimeSpanChange.bind(this);
}
You're on the right track with using shouldComponentUpdate, it's just that the first parameter is nextProps and the second is nextState, so in your case, the undefined value is actually nextProps with the wrong name.
Change your code to this,
shouldComponentUpdate(nextProps,nextState) { // <-- tweak this line
if (this.state.timespanState !== nextState.timespanState) {
return true;
}
return false;
}
Finally, I solve the problem by separating drop-down selectbox and charts into two apart components and made the drop-down component as a child component from its parent component, charts components.
The reason is the following statement
React components automatically re-render whenever there is a change in their state or props.
Therefore, React will re-render everything in render() method of this component. So keeping them in two separate components will let them re-render without side effect. In my case, any state changes in drop-down or other states in Filter component, will only cause a re-render inside this component. Then passing the updated states to charts component with a callback function.
Something like below:
Child component
export class Filter extends Component {
handleApplyChanges = () => {
this.props.renderPieChart(data);
}
render(){
return (
...
<Button onClick={this.handleApplyChanges} />
);
}
}
Parent component
export class Charts extends Component{
constructor(props){
this.state = { dataForPieChart: []};
this.renderPieChart = this.renderPieChart.bind(this);
}
renderPieChart = (data) => {
this.setState({ dataForPieChart: data });
}
render(){
return (
<Filter renderPieChart={this.renderPieChart} />
<Chart>
...data={this.state.dataForPieChart}
</Chart>
);
}
}
If still any question, disagreement or suggestions, pls let me know:)
I want to send events down to my React child.
I feel like this is kind of an easy thing to do, so maybe i just have a mental block, and there is something obvious that is staring me in the face.
Anyway, I have a little Test app which illustrates the problem:
export class Test extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
let {buttonClicked, textFieldChanged} = this.state
return (
<div>
<button onClick={()=>this.handleClick()}>
Click
</button>
<input type={"text"} onChange={()=>this.handleTextChange()}/>
<Inner buttonClicked={buttonClicked} textFieldChanged={textFieldChanged}/>
</div>
);
}
handleClick(e) {
this.setState({ buttonClicked: true })
}
handleTextChange(e) {
this.setState({textFieldChanged:true})
}
}
class Inner extends React.Component {
render() {
let {buttonClicked, textFieldChanged} = this.props;
return (
<React.Fragment>
<div>Clicked : {buttonClicked ? "CLICKED!" : " "}</div>
<div>Text input : {textFieldChanged ? "TYPED!" : " "}</div>
</React.Fragment>
);
}
}
A button and a textfield live in the parent. Both these widgets can fire off events and change the child component.
This is simply achieved by passing a state value as a property down to the child. Very easy stuff.
However I would like an either/or situation. When I click the button this removes the text event, and vice versa. Ie. I do not want to see a situation like this :
Now there is a very obvious way to fix this by changing the state value to "false" of the other value.
handleClick(e) {
this.setState({ buttonClicked: true, textFieldChanged: false })
}
handleTextChange(e) {
this.setState({textFieldChanged:true, buttonClicked: false})
}
Is there any OTHER way of doing this?
The problem is that I have LOTS and LOTS of even handlers in my component and I don't want to negate the other state properties of the other values.
if i understood you correctly just one function will help - pass the attribute name into it
handleClick(propName) {
this.setState({
...this.state,
[propName]: !this.state[propName]
})
}
Create property lastEventType in parent component state , whenever you click or type - update it. And pass only this property to Inner component
TLDR;
Does adding a className to an exciting div reload the parent component in ReactJs? Added images near button to show console.log being called multiple times.
Here is the bug..
I'm building a simple 'order' app, which includes a sidebar.
I recreated the sidebar in a new project for a sanity check. Same issues. Here is the simple version.
class App extends Component {
constructor() {
super();
this.state = {
addList : [],
}
}
render() {
return (
<div className="App">
<Sidebar list = {this.state.addList}/>
</div>
);
}
}
export default App;
and in the sidebar component
class Sidebar extends Component {
constructor(props){
super(props);
this.state = {
active : false
}
}
toggleSidebar () {
if (this.state.active) {
this.setState({
active : false
})
} else {
this.setState({
active: true
})
}
}
render () {
return (
<div className={ 'sidebar ' + ((this.state.active) ? '' : 'hidden')}>
<button className='tab' onClick={(e)=>{this.toggleSidebar()}}>
TAB
</button>
<div className="itemList">
{console.log(this.props.list)}
</div>
</div>
)
}
}
export default Sidebar;
The sideBar class has a position: fixed and I move it out of the screen on a button click and adding a hidden className (.hidden { right: -x })
When an item gets selected in the parent app component, it gets added to its state (addItem).
The Sidebar component has that property passed into so when addItem get a new item, it displays it. It works just as predicted. I was able to add items and display them no problem.
I noticed the bug when I started adding number and doing price totals etc, because it seems the sidebar keep rendering I would find myself getting caught in infinite setState loops
Any solutions or advice?
Images for a those that are visual (clicking the tab and console displaying):
The answer is simple, I'm foolish. In fact I AM changing the state of sidebar causes a reload.
Therefor to get around this is having the parent component hold the values and pass them down as properties for the sidebar to just display. Therefor on reload the values don't change or re-add.
I have a react component that consists of an input field and a button. When the button is clicked I want to run an update function that is also inherited from the parent controller. In the react documentation they have an onchange handler attached to this input and they get the new value of the input with the onchange event object. However in my case I get an event object describing the button, not the input field. What is the correct way to access the new input field value from handle click?
class QuoteButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick () {
this.props.onQuoteUpdate(//what should go here?)
}
render() {
const cost = this.props.cost;
return (
<div>
<Input value={cost}/>
<Button basic color='green' onClick={this.handleClick}>Submit Quote</Button>
</div>
);
}
}
If you a going to change input value in this component you need to use react states (if you don't use state management libraries such as mobx or redux). In most cases input have to be a controlled component.
After component was mounted add cost value to states. You also need appropriate handler for input.
P.S. You could use arrow functions to avoid binding handlers in constructor.
class QuoteButton extends React.Component {
constructor(props) {
super(props);
this.state = { inputValue: '' };
}
componentDidMount() {
this.setState({inputValue: this.props.cost});
}
handleClick = () => {
this.props.onQuoteUpdate(this.state.inputValue);
}
handleInputChange = event => {
this.setState({inputValue: event.target.value});
}
render() {
return (
<div>
<Input value={this.state.inputValue} onChange={this.handleInputChange} />
<Button basic color='green' onClick={this.handleClick}>Submit Quote</Button>
</div>
);
}
}
Hope it helps
I have a component "BulkActionPanel" that renders some buttons. Buttons are enabled or disabled based on the array property "selectedJobIds" passed as a props from its parent component "Grid". Precisely, if length of props "selectedJobIds" is greater than 0 then buttons are enabled else they are disabled.
I have a callback on "onClick" of all the buttons inside BulkActionPanel component, that sets the selectedJobIds to '0' by calling actionCreator "this.props.removeSelectedJobIds([rowData.id])" and it ensures that buttons are disabled.
Since action creator takes a lot of time (does heavy processing on grid), I am maintaining a local state "disable" inside BulkActionPanel to ensure button gets disabled first and then selectedJobIds state is updated in redux store.
I wrote the code below but buttons are not getting disabled until action creator " this.props.removeSelectedJobIds([rowData.id]);" finishes.
export default class Grid extends Component {
render() {
<BulkActionPanel
actions={this.bulkActions}
selectedJobIds={this.getFromConfig(this.props.config, [SELECTED_ROWS_PATH_IN_GRID_CONFIG])}
/>
<SlickGrid/>
}
}
export default class BulkActionPanel extends Component {
constructor() {
super();
this.state = {
disable: true
}
}
componentWillReceiveProps(nextProps){
if(nextProps.selectedJobIds && nextProps.selectedJobIds.length > 0){
this.setState({disable:false});
}
}
shouldComponentUpdate(nextProps) {
return nextProps.selectedJobIds !== undefined && nextProps.selectedJobIds.length
}
#autobind
onActionButtonClick(action) {
this.setState({disable:true}
, () => {
// Action creator that takes a lots of time
this.props.removeSelectedJobIds([rowData.id]);
}
);
}
#autobind
renderFrequentActions() {
return this.props.actions.frequentActions.map((frequentAction) => (
<button
className="btn btn-default"
key={frequentAction.DISPLAY_NAME}
onClick={() => this.onActionButtonClick(frequentAction)}
disabled={this.state.disable}
>
{frequentAction.DISPLAY_NAME}
</button>
));
}
render() {
const frequentActions = this.renderFrequentActions();
return (
<div className="btn-toolbar bulk-action-panel">
{frequentActions}
</div>
);
}
}
Does it has something to do with parent child relation of Grid and BulkActionPanel component? Leads here is appreciated.
Thanks!
I think your component is not passing this
if(nextProps.selectedJobIds && nextProps.selectedJobIds.length > 0){
this.setState({disable:false});
}
you have in your componentWillReceiveProps
if callback from removeSelectedJobIds isn't fired, state won't be changed, try set state of button like you did, and use reducer to dispatch action when removeSelectedJobIds finished, catch that action and rerender or change what you need.
OR
Use reducer for everything. onclick call actin type that let's you know data in table is rendering, use initail state in reducer to disable btn, when data in table finishes calucating fire action in reducer that send new data to component state