I have a picklist component that render some children based on the selected option. The problem comes when I render the same component in two options but with different props, because in that case, the component is not rerendered with the new props.
Let me clarify the problem: I have a picklist, I select option "A", then a text component is rendered below the picklist, I type "error" in that text field, then select option "B" in the picklist, then the other text field component disappear and another text field component is rendered just below the picklist. The last component should have been rendered empty, but the problem is that it contains the word "error".
Here's a minimized version of the code reproducing the error:
import React from "react";
class TextField extends React.Component {
constructor(props) {
super(props);
this.state = { value: props.value };
}
_onChange = (event) => {
this.setState({ value: event.currentTarget.value });
};
_handleBlur = (event) => {
this.setState({ value: event.currentTarget.value.trim() });
};
render() {
const { value } = this.state;
return (
<div>
<label>TextField</label>
<input
type="text"
onChange={this._onChange}
onBlur={this._handleBlur}
value={value}
/>
</div>
);
}
}
class Picklist extends React.Component {
constructor(props) {
super(props);
this.state = { selectedOption: "" };
this.options = ["Blank", "A", "B"];
}
// eslint-disable-next-line consistent-return
_handleSelectorCallback = (newOption) => {
this.setState({ selectedOption: newOption.currentTarget.value });
};
_renderChildren(selectedOption) {
if (!selectedOption) {
return null;
}
if (selectedOption === "A") {
return <TextField value="optionA"/>;
}
if (selectedOption === "B") {
return <TextField value="optionB"/>;
}
}
_renderOptions() {
const { selectedOption } = this.state;
return this.options.map((option) => (
<option value={option} selected={selectedOption === option}>
{option}
</option>
));
}
render() {
const { selectedOption } = this.state;
return (
<React.Fragment>
<div>
<label>Demo of the Error!</label>
<select
onChange={this._handleSelectorCallback}
value={selectedOption}
>
{this._renderOptions()}
</select>
</div>
{this._renderChildren(selectedOption)}
</React.Fragment>
);
}
}
export default Picklist;
Ignore how bad the component is written, is just to reproduce the error I'm having. Any idea why the component is not being rerendered with a new value?
I think what's happening is, when you switch from one <TextField> to the other, React is trying to be efficient by just passing different props to the same instance. You can tell React they are different and should be rerendered by adding a key:
_renderChildren(selectedOption) {
if (!selectedOption) {
return null;
}
if (selectedOption === "A") {
return <TextField key="A" value="optionA" />;
}
if (selectedOption === "B") {
return <TextField key="B" value="optionB" />;
}
}
alternatively, you could make your TextField a controlled component, which means it has no internal state, and the value/onChange fn are passed in as props. I edited your codesandbox to follow this pattern: https://codesandbox.io/s/empty-wind-43jq4?file=/src/App.js
I tried your sandbox. Your TextField component does not get unmounted so it's constructor gets called just once. Every change you make that has to do with that component, goes to it's componentDidUpdate hook.
So this is what you are missing:
componentDidUpdate() {
if (this.state.value !== this.props.value) {
this.setState({ value: this.props.value });
}
}
Related
I have built this site
https://supsurvey.herokuapp.com/surveycreate/
now I am trying to move the fronted to React so I can learn React in the process.
with vanila js it was much easier to create elements dynamically.
I just did createElement and after that when I clicked "submit" button
I loop throw all the elements of Options and take each target.value input.
so I loop only 1 time in the end when I click Submit and that's it I have now a list of all the inputs.
in react every change in each input field calls the "OnChange" method and bubbling the e.targe.value to the parent and in the parent I have to copy the current array of the options and rewrite it every change in every field.
is there other way? because it seems crazy to work like that.
Options.jsx
```import React, { Component } from "react";
class Option extends Component {
constructor(props) {
super(props);
this.state = { inputValue: "", index: props.index };
}
myChangeHandler = event => {
this.setState({ inputValue: event.target.value });
this.props.onChange(this.state.index, event.target.value);
};
render() {
return (
<input
className="survey-answer-group"
type="text"
placeholder="Add Option..."
onChange={this.myChangeHandler}
/>
);
}
}
export default Option;
______________________________________________________________________________
Options.jsx````
```import React, { Component } from "react";
import Option from "./option";
class Options extends Component {
render() {
console.log(this.props);
return <div>{this.createOptions()}</div>;
}
createOptions = () => {
let options = [];
for (let index = 0; index < this.props.numOfOptions; index++) {
options.push(
<Option key={index} onChange={this.props.onChange} index={index} />
);
}
return options;
};
}
export default Options;```
______________________________________________________________________________
App.jsx
```import React from "react";
import OptionList from "./components/Options";
import AddButton from "./components/add-button";
import "./App.css";
class App extends React.Component {
state = {
numOfOptions: 2,
options: [{ id: 0, value: "" }, { id: 1, value: "" }]
};
handleChange = (index, value) => {
const options = [...this.state.options];
console.log("from App", value);
options[index].value = value;
this.setState({
options: options
});
console.log(this.state);
};
addOption = () => {
const options = [...this.state.options];
options.push({ id: this.state.numOfOptions + 1, value: "" });
this.setState({
numOfOptions: this.state.numOfOptions + 1,
options: options
});
};
submitButton = () => {};
render() {
return (
<div className="poll-create-grid">
<div id="poll-create-options">
<OptionList
onChange={this.handleChange}
numOfOptions={this.state.numOfOptions}
/>
</div>
<button
className="surveyCreate-main-btn-group"
onClick={this.addOption}
>
Add
</button>
<button
className="surveyCreate-main-btn-group"
onClick={this.submitButton}
>
Submit
</button>
</div>
);
}
}
export default App;
```
So firstly,
The issue is with the way your OptionList component is defined.
Would be nice to pass in the options from the state into the component rather than the number of options
<OptionList
onChange={this.handleChange}
options={this.state.options}
/>
The you basically just render the options in the OptionsList component (I'm assuming it's same as the Options one here
class Options extends Component {
...
render() {
return
(<div>{Array.isArray(this.props.options) &&
this.props.options.map((option) => <Option
key={option.id}
index={option.id}
onChange={this.props.onChange}
value={option.value}
/>)}
</div>);
}
...
}
You would want to use the value in the Option component as well.
this.props.onChange(this.state.index, event.target.value); No need using the state here to be honest
this.props.onChange(this.props.index, event.target.value); is fine
There has been a number of questions on React checkboxes. This answer is pretty good, and it helps to modularize the idea of select-all checkboxes in React.
Now, I have a problem. I have a dictionary like this:
{
regionOne: {
subareaOne,
subareaTwo,
subareaThree,
subareaFour
},
regionTwo: {
subareaFive,
subareaSix
}
}
Here, each region is mapped to an arbitrary number of sub areas, which I do not know beforehand.
I want to create checkboxes such that each region and each subarea has a checkbox, and each region's checkbox acts as a select-all/de-select all for all the subareas it is mapped to. That is, something like this:
So, when you click on the checkbox for regionOne, the checkboxes for subareaOne, subareaTwo, subareaThree and subareaFour should all be checked as well, but not those in regionTwo.
I think I can adapt this answer, but its getInitialState function assumes that you know how many children checkboxes there are.
Any idea on how to do this in an elegant method? I am now considering initialising the checkboxes dynamically using mapping, but I am not sure...VanillaJS would have been much simpler >.<
I actually went ahead and implemented it anyway. There are two components involved, a parent CheckboxGroup component, and a child StatelessCheckbox component.
Here is the parent component:
import React from 'react';
import { StatelessCheckbox } from './StatelessCheckbox';
export class CheckboxGroup extends React.Component {
constructor(props) {
super(props);
this.state = {
checked: false,
boxes: {},
disabled: false
};
}
componentDidMount() {
const { boxes, boxId, disabled } = this.props;
let boxesState = {};
boxes.map(box => {
boxesState[box[boxId]] = false;
});
this.setState({ checked: false, boxes: boxesState, disabled: disabled });
}
handleSelectAll(event) {
const isChecked = event.target.checked;
let boxesState = {};
Object.keys(this.state.boxes).map(box => {
boxesState[box] = isChecked;
});
this.setState({ checked: isChecked, boxes: boxesState });
}
handleSelect(event) {
const isChecked = event.target.checked;
const boxId = event.target.value;
let newBoxes = {};
Object.assign(newBoxes, this.state.boxes);
newBoxes[boxId] = isChecked;
// Check parent checkbox if all children boxes are checked
const checkedBoxes = Object.keys(newBoxes).filter((box) => {
return newBoxes[box] === true;
});
const parentIsChecked = (checkedBoxes.length === Object.keys(newBoxes).length);
this.setState({ checked: parentIsChecked, boxes: newBoxes });
}
render() {
const {
passDataToParent=(() => { return false; }),
groupClassName='',
headClassName='',
headName='',
headBoxClass='',
headLabelClass='',
headLabelText='',
bodyClassName='',
bodyName='',
bodyBoxClass='',
bodyLabelClass='',
boxes,
boxId,
boxLabel
} = this.props;
return (
<div className="row">
<div className={ groupClassName }>
<div className={ headClassName }>
<StatelessCheckbox name={ headName } className={ headBoxClass }
labelClass={ headLabelClass } labelText={ headLabelText }
checked={ this.state.checked } value={ headName }
passedOnChange={ (e) => { this.handleSelectAll(e); } } />
</div>
<div className={`row ${ bodyClassName }`}>
{ boxes.map(box => (
<div key={ box[boxId] }>
<StatelessCheckbox name={ bodyName } className={ bodyBoxClass }
labelClass={ bodyLabelClass } labelText={ box[boxLabel] }
checked={ this.state.boxes[box[boxId]] } value={ box[boxId] }
passedOnChange={ (e) => { this.handleSelect(e); } } />
</div>
))}
</div>
</div>
</div>
);
}
}
and here is the child component:
import React from 'react';
/**
* Implements a React checkbox as a stateless component.
*/
export class StatelessCheckbox extends React.Component {
constructor(props) {
super(props);
}
render() {
const {
passedOnChange=(() => { return false; }),
className='',
name='',
labelClass='',
labelText='',
value='',
checked=false,
} = this.props;
return (
<label className={`for-checkbox ${ className }`} tabIndex="0">
<input onChange={(e) => passedOnChange(e) }
checked={ checked } type="checkbox"
name={ name } value={ value } />
<span className={`label ${ labelClass }`}>{ labelText }</span>
</label>
);
}
}
Things to note:
the child component is a stateless component in this case, used purely for rendering
the parent component (CheckboxGroup) maintains the state for all the child components as well so there is still a single source of truth as per React philosophy
boxes in properties is a list of the children checkboxes in each parent CheckboxGroup, and boxes in the parent state holds the state for each child checkbox
Something like that?
onSelect(id) {
let allElms = Object.assign({}, retriveElmentsSomehow());
let elm = findTheElm(id, allElms);
updateSelected(elm, !elm.selected);
// Then update state with new allElms
}
updateSelected(elm, selectedOrNot) {
elm.selected = selectedOrNot;
if (elm.children) {
elm.children.foreach(c => onSelect(c, selectedOrNot));
}
}
Where elements are in state or redux store.
i'm using React(Typescript Version) to display some input inside a form.
The problem (as you can see from the image) is that when i update the values, from the setState function, values will not 'scroll' on the right
render() {
return(
<input
name={this.props.input.Name}
type={this.props.input.Type}
defaultValue={this.state.value}
ref={this._input}
key={key()}
)}
The function that updates the Value is a common set Function :
public set Value(data: string) {
this.setState({
internalValue: data,
inputError: !this.validateValue(data)
});
}
Note that the input works as expected if i write from the Keyboard, but if i write the input using a 'simulated' keyboard on screen happens what i just described
Any ideas?
Thank you
Update after simbathesailor support:
render() {
return(
<input
name={this.props.input.Name}
type={this.props.input.Type}
defaultValue={this.state.value}
ref={this._input}
key={key()}
onChange={this.setValue}
/>
)
}
componentDidUpdate(prevProps: InputProps, prevState: InputState) {
if (prevState.value!== this.state.value) {
this._input.current.focus();
}
}
setValue(event: React.ChangeEvent<HTMLInputElement>) {
console.log('change');
this.setState({
value: event.target.value
})
}
shouldComponentUpdate(nextProps: InputProps, nextState: InputState): boolean {
return (this.state.value!= nextState.value);
}
public set Value(data: string) {
this.setState({
value: data,
inputError: !this.validateValue(data)
}, () => {
this._input.current.focus();
});
}
You can use the refs and commit lifecycle method componentDidUpdate method. to achieve this.
In the example mentioned below, it is done for the uncontrolled component. But idea will remain same for controlled component also.
class Test extends React.Component {
constructor(props) {
super(props)
this.InputRef = React.createRef()
this.state = {
value: 0
}
}
setValue = (event) => {
this.setState({
value:event.target.value
})
}
update = () => {
this.setState({
value: (this.state.value || 0) + 1000
})
}
componentDidUpdate(prevProps, prevState) {
if(prevState.value !== this.state.value) {
this.InputRef.current.focus()
}
}
render() {
return (
<div>
<input
value={this.state.value}
onChange={this.setValue}
ref={this.InputRef}
/>
<button onClick={this.update}>update</button>
</div>
)
}
}
ReactDOM.render(<Test />, document.getElementById("root"))
Here is the codepen link to see it working:
Uncontrolled approach(javascript) codepen link
Controlled approach(javascript) codepen link
I have tried typescript for the first time. Thanks for your question :). Typescript is good. And here is your desired solution needed in typescript.
Codesandbox link(Typescript)
I want to display the selected checkbox items, for which I'm using material-ui checkbox.
Right now I'm only able to display the items with checkboxes, but I am not able to display the selected items.
I know it is easy but I'm new to reactjs and redux so finding it difficult to start.
Hoping for a help.
Thank you.
this.state = {
data: [apple, kiwi, banana, lime, orange, grape],
}}
handleCheck(x) {
this.state.checkedValues.push(x);
}
render(){
{this.state.data.map((x) =>
<Checkbox
label={x} key={x.toString()}
onCheck={() => this.handleCheck(x)}
checked=true
}/>
)}}
Modifying the answer by #BravoZulu by adding the event as the argument in onChange() function.(Also note that use onChange() instead of onCheck() for material-UI checkboxes as shown in the official documentation). Also, don't forget to bind the function in the constructor. I hope this helps the community. Below is the code.
class App extends Component {
constructor(props) {
this.handleCheck = this.handleCheck.bind(this);
super(props);
this.state = {
data: [apple, kiwi, banana, lime, orange, grape],
checkedValues: []
}
}
handleCheck(e,x) {
this.setState(state => ({
checkedValues: state.checkedValues.includes(x)
? state.checkedValues.filter(c => c !== x)
: [...state.checkedValues, x]
}));
}
render() {
return (<div>
{ this.state.data.map(x =>
<Checkbox
label={x} key={x.toString()}
onChange={e => this.handleCheck(e,x)}
checked={this.state.checkedValues.includes(x)}
/>
)}}
</div>)
}
}
In the handleCheck function, you are attempting to update your component state incorrectly. You need to use setState to make changes to state. In your example, state isn't getting updated so that is probably why you aren't seeing anything get selected. Also, gonna help clean up your example a bit:
class CheckboxList extends React.Component{
constructor() {
super();
this.state = {
data: ['apple', 'kiwi', 'banana', 'lime', 'orange', 'grape'],
checkedValues: []
}
}
handleCheck(index) {
this.setState({
checkedValues: this.state.checkedValues.concat([index])
});
console.log(this.state.checkedValues.concat([index]))
}
render(){
const checks = this.state.data.map( (item, index) => {
return (
<span key={item}>
<input type="checkbox"
value={item}
onChange={this.handleCheck.bind(this, index)} //Use .bind to pass params to functions
checked={this.state.checkedValues.some( selected_index => index === selected_index )}
/>
<label>{item}</label>
</span>)
});
return <div>{checks}</div>
}
}
Update:
Added working jsfiddle.
A bit late to the party but here's a solution using a functional component and hooks
import React, { useState } from 'react';
import Checkbox from '#material-ui/core/Checkbox';
const App = ({ data }) => {
const [checked, setChecked] = useState([]);
const handleCheck = (event) => {
const { value } = event.target;
setChecked(checked.includes(value) ? checked.filter(c => c !== value) : [...checked, value]);
};
return (
<div>
{data.map(({ value }) => (
<Checkbox onChange={e => handleCheck(e)} checked {checked.includes(value)} />
))}
</div>
);
};
export default App;
In React, you shouldn't push data directly to your state. Instead, use the setState function.
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: [apple, kiwi, banana, lime, orange, grape],
checkedValues: []
}
}
handleCheck(x) {
this.setState(state => ({
checkedValues: state.checkedValues.includes(x)
? state.checkedValues.filter(c => c !== x)
: [...state.checkedValues, x]
}));
}
render() {
return (<div>
{ this.state.data.map(x =>
<Checkbox
label={x} key={x.toString()}
onCheck={() => this.handleCheck(x)}
checked={this.state.checkedValues.includes(x)}
/>
)}}
</div>)
}
}
I was also stuck on this issue for quite some time when I finally found a fix to this. It never works for a functional component which returns a check box. I made a separate class component and wrapped it in Redux Field component and it worked perfectly. I really never understood why it didn't work for the fucntional component as it what is shown in their official example.
I have written the code that worked for me. Hope it helps :)
class CheckBoxInput extends React.Component {
onCheckBoxSelectChange = (input) =>
{
input.onChange();
this.props.onSelectChange();
}
render() {
const { label, input,} = this.props;
let name = input.name;
return (
<div>
<InputLabel htmlFor={label} style={{paddingTop: '15px'}}> {label} </InputLabel>
<FormControl {...input} >
<Checkbox
name = {name}
label = {label}
color= "primary"
checked={input.value ? true : false}
onChange={() => this.onCheckBoxSelectChange(input)}
/>
</FormControl>
</div>
)
}
}
const CheckBox = (props) => <Field component={CheckBoxInput} {...props} />;
export default CheckBox;
And to use this checkbox component:
<CheckBox name="isCurrent" label="Current" onSelectChange = {this.toggleCurrentEmployerSelection} />
In case you are working with objects instead of simple data types, here is a working approache:
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: [{id: 1, name:'banana'},
{id: 2, name:'kiwi'}],
checkedValues: []
}
}
handleCheck(element) {
const values = this.state.checkedValues.filter(e => e.id === element.id).length > 0
? this.state.checkedValues.splice( this.state.checkedValues.findIndex( e => e.id === element.id),1)
: this.state.checkedValues.push(element);
this.setState({
checkedValues: values
});
}
render() {
return (<div>
{ this.state.data.map(el =>
<Checkbox
checked={this.state.checkedValues.filter(e => e.id === el.id).length > 0}
onChange={this.handleCheck.bind(this, el)} //Use .bind to pass params to functions
value={el}
/>
)}}
</div>)
}
}
So basically what the function handleCheck does is it checks whether the selected object is in the checkedValues array, if that is the case then it deletes it (case uncheck), otherwise it adds it (case check), is i add the checked object to the checkedValues array.
in the Checkbox checked checks whether there is an object in the checkedValues array that is equal to the current loop object, (case checked/unchecked)
I'm trying to create a custom radio button. The issue that i'm facing is that i'm unable to uncheck the radio button when another radio button is clicked. Currently it behaves like a checkbox.
import {React, ReactDOM} from '../../shared/lib/react';
export default class RadioButton extends React.Component {
constructor(props) {
super(props);
this.state = {
checkedRadioBtn: false
};
this.toggleRadioBtn = this.toggleRadioBtn.bind(this);
};
toggleRadioBtn(){
this.setState({checkedRadioBtn: !this.state.checkedRadioBtn});
};
render() {
return (
<div className="radio-btn-group">
<div onClick={this.toggleRadioBtn} className={this.state.checkedRadioBtn ? "radiobtn checked" : "radiobtn unchecked"} data-value={this.props.value}></div>
<label>{this.props.text}</label>
</div>
);
}
};
You need to have container for group of radio buttons. That container will maintain the state for all the radio buttons and handle check/uncheck for each option. Here is the sample code for that,
import React from 'react'
import ReactDOM from 'react-dom'
class RadioBtn extends React.Component{
constructor(props) {
super(props);
}
handleClick(){
this.props.handler(this.props.index);
}
render() {
return (
<div className="radio-btn-group" onClick={this.handleClick.bind(this)}>
<div className={this.props.isChecked ? "radiobtn checked" : "radiobtn unchecked"} data-value={this.props.value}></div>
<label>{this.props.text}</label>
</div>
);
}
}
class RadioGrp extends React.Component{
constructor() {
super();
this.state = {
selectedIndex: null,
selectedValue: null,
options: ["option 0","option 1","option 2","option 3"]
};
}
toggleRadioBtn(index){
this.setState({
selectedIndex: index,
selectedValue: this.state.options[index],
options: this.state.options
});
}
render() {
const { options } = this.state;
const allOptions = options.map((option, i) => {
return <RadioBtn key={i} isChecked={(this.state.selectedIndex == i)} text={option} value={option} index={i} handler={this.toggleRadioBtn.bind(this)} />
});
return (
<div>{allOptions}</div>
);
}
}
var app = document.getElementById('app');
ReactDOM.render(<RadioGrp />, app);
Since you're using a div for a custom checkbox that doesn't behave like a normal checkbox you should be checking value against the selected value.
toggleRadioBtn(e){
this.setState({checkedRadioBtn: e.target.value});
};
Another question that I have is that you are assuming a single checkbox here so I have to assume you have a calling component that returns multiple instances. If that is the case then you need to pass your onClick down so you can pass the value back up to the parent. Then pass the selected value back down.
This is an example that I have in my application.
var languges = this.props.languages;
var languageToSelect = this.state.selectedLanguage;
var handleChange = this.handleChange;
var languageRows = Object.keys(languges).map(function(key) {
var language = languges[key];
return <LanguageBlock
key={ key }
language={ language }
languageCode={ key }
checked={ languageToSelect === key }
handleChange={ handleChange }
/>;
});
In my use case I have multiple languages and onChange I pass the selected language back up then on rerender the selected language will be updated so the radio options will reflect the change.
handleChange: function handleChange(event) {
this.setState({ selectedLanguage: event.target.value });
},
The handle change just sets state for the new value. The language block itself is just a simple component so no need to make it a class/component.
const LanguageBlock = ({ checked, language, languageCode, handleChange }) => {
return (
<div className="truncification">
<input type="radio" name="lang" id={ 'lang_' + languageCode }
value={ languageCode } checked={ checked } onChange={ (evt) => { handleChange(evt); } } />
<label htmlFor={ 'lang_' + languageCode }>{ language }</label>
</div>
);
};