I've got a parent container that calls a class-based react component child (since the semantic-ui react docs are all written with class-based components). The child component has a Form.Field:
<Form.Field
control={Checkbox}
label={{children: 'cardreader'}}
checked = {this.state.cardReaderChecked}
onChange={this.cardReaderToggleHandler}
/>
I've got a state:
state = {
cardReaderChecked: false,
}
and a cardReaderToggleHandler:
cardReaderToggleHandler = () => {
console.log(this.state.cardReaderChecked);
this.setState((prevState, props) => ({
cardReaderChecked : !prevState.cardReaderChecked
}))
console.log(this.state.cardReaderChecked);
}
I've tried toggling on this.state.cardReaderChecked but I found a lot of references and docs recommending this approach with the prevState to avoid the delay in state updating.
However, there must a logical fault because it doesn't work. If I refresh the page, the checkbox is clear as this.state.cardReaderChecked.
The first time I click the checkbox it renders with the tick, and my this.state.cardReaderChecked updates to true (according to my trusty React tools in Chrome). However, both console.log printouts give me a false and if I pass my state back to the parent form, it also shows that the checkbox is false.
Every subsequent click toggles but a ticked checkbox shows a true state but passes on a false to the parent form (that's where the console.log is currently) and vice versa an unticked checkbox passes back a true.
I'm almost tempted to remove the not from the prev.state in the setState, but I would prefer to understand why this happens.
Bonus Question: How can I query the checkbox state checked in a functional component?
Pass the function to a child as prop
class Parent extends Component {
state = {
cardReaderChecked: false,
}
cardReaderToggleHandler = () => {
this.setState((prevState, props) => ({
cardReaderChecked : !prevState.cardReaderChecked
}), () => console.log(this.state.cardReaderChecked))
}
....
render () {
return (
....
<Form.Field
control={Checkbox}
label={{children: 'cardreader'}}
checked = {this.state.cardReaderChecked}
onChange={this.cardReaderToggleHandler}
/>
)
}
Related
I am rendering some cards and upon clicking a card, I have the corresponding modal. Both the card and its modal have a heart icon and I want to mark both as "favorite" when either of them is clicked. I have a "favoriteBeers" array, where I want to push the favorite beers. I also have another piece of state named "favorite" and this one is boolean. The issue is that this state seems to be reversed (it is false when it's supposed to be true and the other way around). Also, only one item seems to be in the favorites array, no matter how may items I try to set as favorite.
I have lifted the array state on the root component, this is the piece of code that lies there:
const [favoriteBeers, setFavoriteBeers] = useState([]);
const handleSetFavorite = id => {
setFavoriteBeers([...favoriteBeers, beers.find(beer => beer.id === id)]);
};
const handleRemoveFavorite = id => {
setFavoriteBeers(favoriteBeers.filter(beer => beer.id !== id));
};
I also have one component for the card, and one for its modal. I have an identical function in both components:
const [isFavorite, setIsFavorite] = useState(false);
if (!isFavorite) {
setIsFavorite(true);
handleSetFavorite(id);
} else {
setIsFavorite(false);
handleRemoveFavorite(id);
}
};
//the icon that calls the function
<IconButton aria-label='add to favorites'>
{!isFavorite ? (
<FavoriteBorderIcon
onClickCapture={e => handleIconClick(e, beer.id)}
/>
) : (
<FavoriteIcon onClickCapture={e => handleIconClick(e, beer.id)} />
)}
</IconButton>
I have also prepared a codesandbox codesandbox with the components, thanks in advance
One problem is that in <Home> your isFavorite prop is undefined since <App> isn't passing such a thing. Also you're not using this prop value (currently undefined) to initialize the <BeerCard> or <BeerCardExpanded> components.
Second problem - the code below is only updating a components own isFavorite state and calling handleSetFavorite, since each component has it's own local isFavorite state.
if (!isFavorite) {
setIsFavorite(true);
handleSetFavorite(id);
}
For example when <BeerCard> flips it's isFavorite state, <BeerCardExpanded> doesn't. So I've removed these local states and directly used props.isFavorite instead.
Here is updated sandbox
I've added this method in <Home>:
const isFavorite = (beer, favoriteBeers) => {
return favoriteBeers.includes(beer);
};
Which is used to pass proper boolean value for the same for isFavorite prop of both <BeerCard> (isFavorite={isFavorite(beer, favoriteBeers)})
And <BeerCardExpanded> (isFavorite={isFavorite(isClicked, favoriteBeers)}) components.
I've used rest params syntax ...props just to avoid renaming isFavorite prop and have minimal changes. You can improvise the whole thing.
I have the following scenario:
1) There is a parent component "ModuleListContainer".
2) A module (in the module list, also a child component, but not interesting in this context) gets selected when hovering over it a module item in the list.
3) When hovering over a module, a menu should be shown in the corner of the module.
4) The whole parent component should NOT be updated when a module is selected, since it can be quite a long list of modules, that is why I set shouldComponentUpdate = false when updating which module should be selected.
5) The menu is loaded when the parent component loads, and only its position is updated when hovering over a module.
This is the parent component (simplified)...
class ModuleListContainer extends Component {
constructor(props) {
super(props);
this.state = {
selectingModule: false,
currentlySelectedModule: nextProps.currentModule
}
}
shouldComponentUpdate(nextProps, nextState) {
if (nextState.selectingModule === true) {
this.setState({
selectingModule: false,
currentlySelectedModule: null
})
return false;
}
return true;
}
mouseEnterModule = (e, moduleItem) => {
const menu = document.getElementById('StickyMenu');
const menuPosition = calculateModuleMenuPosition(e.currentTarget);
if (moduleItem.ModuleId !== this.props.currentModuleId) {
this.props.actions.selectModule(moduleItem);
this.setState({
selectingModule: true
});
}
menu.style.top = menuPosition.topPos + 'px';
menu.style.left = menuPosition.leftPos + 'px';
}
render() {
return (
<div>
<section id="module-listing">
{/* ... list of mapped modules with mouseEnterModule event */}
</section>
<ModuleMenu {... this.props} currentlySelectedModule={this.state.currentlySelectedModule} />
</div>
);
}
}
This is the menu component (simplified)...
class ModuleMenu extends Component {
constructor(props) {
super(props);
this.state = {
currentModule: this.props.currentlySelectedModule
};
}
clickMenuButton = () => {
console.log('CURRENT MODULE', this.state.currentModule);
}
render() {
return (
<div id="StickyMenu">
<button type="button" onClick={this.clickMenuButton}>
<span className="fa fa-pencil"></span>
</button>
</div>
);
}
}
When, in my menu component, I try to console.log the current module from the state, I keep getting null.
My question is if this is because...
I have set the shouldComponentUpdate to false and the menu's state does not get updated?
Or could it be because I do not re-render the whole component?
Or is it because I load the menu together with the parent component
and it does not get re-rendered when a module is selected?
Or is it possibly a combination of some of the above?
The react docs (https://reactjs.org/docs/react-component.html) says:
Returning false does not prevent child components from re-rendering
when their state changes.
Therefore, I am hoping that it is none of the above since I really don't want to have to re-render the entire component when selecting a module.
Your children state doesn't change in this case, you're only changing the state of the parent. What you should probably do is split the render method of your component into two components:
render() {
return (
<div>
<NoUpdateComponent someProps={this.props.someProps}/>
<ModuleMenu {... this.props} currentlySelectedModule={this.state.currentlySelectedModule} />
</div>
);
}
And then in your first costly component, use the shouldComponentUpdate method to prevent it from re rendering
I think that in your code there are other problems you need to solve before looking for a practical solution, starting from the use you make of shouldComponentUpdate().
Official doc says that:
Use shouldComponentUpdate() to let React know if a component’s output is not affected by the current change in state or props. The default behavior is to re-render on every state change, and in the vast majority of cases you should rely on the default behavior.
shouldComponentUpdate() is invoked before rendering when new props or state are being received. Defaults to true. This method is not called for the initial render or when forceUpdate() is used.
If you perform a setState() call inside the shouldComponentUpdate() function it might work but essentially you are telling React to start a new render cycle before knowing if in this execution it should render or not.
Also keep in mind that setState() is not guaranteed to be executed immediately:
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
Moreover (not very clear from the code, so I guess) you are separating Component and DOM object for ModuleMenu: its representation must be guided by state or props, here instead you are using HTMLElement.style.x syntax to set its representation properties.
I'd restructure ModuleListContainer to store in its state an object that represents ModuleMenu properties, that will be passed to ModuleMenu component as props, something like this:
moduleMenu {
currentModuleId: ... ,
top: ... ,
left: ...
}
And set the state in mouseEnterModule handler:
mouseEnterModule = (e, moduleItem) => {
const menuPosition = calculateModuleMenuPosition(e.currentTarget);
if (moduleItem.ModuleId !== this.props.currentModuleId) {
this.props.actions.selectModule(moduleItem);
this.setState({
moduleMenu: {
currentModuleId: moduleItem.ModuleId,
left: menuPosition.leftPos + 'px',
top: menuPosition.topPos + 'px'
}
});
}
}
Then ModuleMenu can get the new position like this:
<div id="StickyMenu">
style={{
left: this.props.left,
top: this.props.top
}}
>
...
</div>
Of course you can still use shouldComponentUpdate() to determine which modules in the list should be updated but this time returning just a boolean after a comparison of (once again, I guess) ids of items; avoiding too many calls to setState() method.
Hope this may help you!
I'm just looking for advice on how to properly set / read state in a component that is just a filter (i.e. select dates, min max values, etc).
I basically have:
onMinDateChange(minDate) {
this.setState({minDate});
},
onMaxDateChange(maxDate) {
this.setState({maxDate});
},
...
Now I want to call this.props.onChange() on every state change, but I have two issues:
state doesn't immediately update; how do I call this on the "next tick"? componentDidUpdate?
I'm not sure how to observe any state change so that I don't have to write:
onMinDateChange(minDate) {
this.setState({minDate});
this.update();
},
onMaxDateChange(maxDate) {
this.setState({maxDate});
this.update();
},
...
Any help on both of these points?
You can pass a callback to the this.setState(). see below:
_onStateUpdate() {
*Some code here*
}
onMinDateChange(minDate) {
this.setState({minDate}, _onStateUpdate);
},
Regarding the both of your issues, including this one:
I'm not sure how to observe any state change
You can use componentDidUpdate( prevProps, prevState ) callback, and determine inside, whenever the state was changed.
https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
Here you're trying to synchronize your local state with upper components. That's possible, but it's a hard thing to do it right. Especially when you will occasionally need to set some filter values from the top. Consider moving the state of the filter to the upper component.
To do that you will need to pass your state object and function used to update it instead of setState as component props. State object would be stored as a part of the upper component state then.
You can use value links to make it look elegant. Here's what you code will look like in this case:
import { Link } from 'valuelink'
import { Input } from 'valuelink/tags.jsx'
const UpperComponent = React.createClass({
getInitialState(){
return {
filter : {
text : '',
...
}
}
},
render(){
return (
...
<Filter state={ Link.state( this, 'filter' ) } />
)
}
});
const Filter = ({ state }) => (
<div className="filter">
<Input valueLink={ state.at( 'text' ) } />
...
</div>
)
Here's an explanation of the technique:
https://medium.com/#gaperton/state-and-forms-in-react-part-3-handling-the-complex-state-acf369244d37#.nuloz9adx
And here's the lib:
https://github.com/Volicon/NestedLink
tl;dr React refuses to honor checked={checkThisOption} on inputs, even though it honors data-ischecked={checkThisOption} perfectly on the same set of inputs.
I haven't made this work on jsfiddle, but I have reproduced the issue using this code.
the long version
I've got a simple ReactJS component that presents a list of radio buttons to the user. The user is supposed to be able to pick a radio and then push a button to confirm their choice.
Here's the component def (note: I'm using ES6 & webpack):
import React from 'react';
class Widget extends React.Component {
constructor(props) {
super(props);
this.state = {
currentValue: null // tracks currently-selected choice, by its value
};
}
onClickOptionRadio = (event) => {
this.setState({
currentValue: String(event.currentTarget.value)
});
}
onConfirm = (event) => {
if(!this.props.onChange) return;
this.props.onChange(this.state.currentValue);
};
render() {
let choices = this.props.choices;
let currentValue = this.state.currentValue;
return (
<div className="Widget">
<ol className="choices">
{
choices.map((choice, i) => {
// decide whether to mark radio as checked:
// - if no current choice, check first radios
// - otherwise, check radio matching current choice
let noCurrentChoice = (currentValue === null);
let drawingFirstChoice = (i === 0);
let thisChoiceIsSelected = (String(choice.value) === currentValue);
let checkThisOption = thisChoiceIsSelected || (noCurrentChoice && drawingFirstChoice);
return (
<li key={i}>
<input type="radio" name="choices"
value={choice.value}
onChange={this.onClickOptionRadio}
checked={checkThisOption?'checked':''}
data-ischecked={checkThisOption}
/>
<label>{choice.label}</label>
{' '}
{checkThisOption ? 'CHECKED' : ''}
</li>
);
})
}
</ol>
<button onClick={this.onConfirm}>Confirm choice</button>
</div>
);
}
}
export default Widget;
Here's the owning component:
import React from 'react';
import Widget from 'components/widget';
class Owner extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
let choices = [
{ value: 10, label: 'First' },
{ value: 20, label: 'Second' },
{ value: 30, label: 'Third' }
];
return (
<div className="Owner">
<Widget
choices={choices}
/>
</div>
);
}
}
export default Owner;
Here's a gif of it in action:
Note several things from the video:
the logic clearly works for checking the first radio on initial render
the other radios don't become checked when the user clicks on them
however, the logic clearly works for identifying which item is selected, as indicated by margin-right: 2rem on the radio that ought to be checked
the text indicating which option has been chosen is accurate throughout
when I click a radio, the componentWillUpdate method fires only for the Widget itself; none of its ancestors update
I think this demo proves that this isn't a case of the Widget instance being replaced by a different instance whose state is empty. The fact that the current selection is accurately reflected by a data- attr on the input, as well as plain text, shows that the state is persisting as desired. I am certain this unwanted behavior is by design, and I want to know how to work around the bizarre, exceptional logic that React applies to the form-related properties of controlled inputs.
Why do I think the current behavior is wrong? I don't want the owning component to know about each radio click -- the owner should bind to Widget's onChange method to be notified once a final choice is made.
This is a simplified example. The real component is more complicated, but the principle is the same: just as a date-picking component may have lots of internal state that the owning component is unaware of (like what time scale to show, which year, month, or week to display, etc.), so too does this component have some interesting internal state that owning components have no business managing.
As far as I can tell, I've done this exactly correctly. The component publishes its important state updates via onChange(event, newValue), which owning components should bind to. I think it's quite clear that React is deciding to not update the checked attr on these inputs, even though it's clearly capable of updating other attrs on the same elements in response to the same user actions.
Note that the owner isn't currently listening for the onChange, but that shouldn't matter: the child component should be able to manage its own internal state even when the owner isn't listening. And I reject the assertion that the radio state can't be accurate simply because Owner isn't providing a currentValue via props: Widget is plainly managing and rendering its state without that prop. React must be doing something special to prevent checked from being handled according to the rules that apply to every other element and attribute. This is an exception, and I think it's a bad one.
Finally, note that this problem only seems to occur when this component is beneath a certain comp-tree depth. When it is the only component in a Flux "page" or a Redux "container," it works great. When it's nested more deeply, it fails as I've described. I haven't yet worked out a concise way of showing that.
Any advice is appreciated. As far as I can tell, here React is violating its own stated rules, and I expect this behavior to frustrate building other stateful components that are built around vanilla inputs.
Edit: I corrected the generated names for the radios, and updated the demo to reflect it. Apologies to anyone who started chasing that stuff down.
I've edited it to not use class properties, and are not able to reproduce:
Code:
class Widget extends React.Component {
constructor(props) {
super(props);
this.state = {
currentValue: null // tracks currently-selected choice, by its value
};
this.onClickOptionRadio = this.onClickOptionRadio.bind(this)
this.onConfirm = this.onConfirm.bind(this)
}
onClickOptionRadio (event) {
this.setState({
currentValue: String(event.currentTarget.value)
});
}
onConfirm (event) {
if(!this.props.onChange) return;
this.props.onChange(this.state.currentValue);
};
render() {
let choices = this.props.choices;
let currentValue = this.state.currentValue;
return (
<div className="Widget">
<ol className="choices">
{
choices.map((choice, i) => {
// decide whether to mark radio as checked:
// - if no current choice, check first radios
// - otherwise, check radio matching current choice
let noCurrentChoice = (currentValue === null);
let drawingFirstChoice = (i === 0);
let thisChoiceIsSelected = (String(choice.value) === currentValue);
let checkThisOption = thisChoiceIsSelected || (noCurrentChoice && drawingFirstChoice);
return (
<li key={i}>
<input type="radio" name="choices"
value={choice.value}
onChange={this.onClickOptionRadio}
checked={checkThisOption?'checked':''}
data-ischecked={checkThisOption}
/>
<label>{choice.label}</label>
{' '}
{checkThisOption ? 'CHECKED' : ''}
</li>
);
})
}
</ol>
<button onClick={this.onConfirm}>Confirm choice</button>
</div>
);
}
}
class Owner extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
let choices = [
{ value: 10, label: 'First' },
{ value: 20, label: 'Second' },
{ value: 30, label: 'Third' }
];
return (
<div className="Owner">
<Widget
choices={choices}
/>
</div>
);
}
}
ReactDOM.render(
<Owner />,
document.getElementById('container')
);
My browser is Chrome 47. Here is the jsfiddle.
I have a table with some data and each element in the table is a React class component. It looks like this:
All i want is to have one checkbox for "check all" feature (top left checkbox). The thing is I don't know how to solve that because of props and state.
I have code like that in single element component:
getInitialState: function() {
return { component: this.props.data };
},
render: function() {
var data = this.state.component;
data = data.set('checked', this.props.data.get('checked'));
...
}
And I know I shouldn't get checked param from props but it is just temporary.
What I have problem with is: When I update checked param in parent it doesn't update state, because getInitialState isn't called after refresh (yep, i know it should be like that).
My question is: can I somehow update state of child component? Or it is better way to achieve that.
With functional components:
An easy way to refresh the children internal state when props provided by parent change is through useEffect():
In the children:
const [data, setData] = useState(props.data);
useEffect( () => {
setData(props.data);
}, [props.data]);
In this way everytime the props.data change the useEffect will be triggered and force to set a new status for some data and therefore the component will "refresh".
My approach is that you should have structure something like this in parent's render:
<ParentView>
{ this.props.rows.map(function(row) {
<ChildRow props={row.props} />
}).bind(this)
}
</ParentView>
And then on row.props you have the information whether current row item is checked or not. When parent checkbox is toggled, you populate all the row.props with the status.
On child, you will receive those with componentWillReceiveProps and you do the magic (e.g. set the correct state) when checkbox is toggled:
componentWillReceiveProps: function(props) {
this.setState({isChecked: props.isChecked});
}
(Info from the React's docs: Calling this.setState() within this function will not trigger an additional render.)
Child element's render would be something like:
<div>
<input type='checkbox' checked={this.state.isChecked} />
<label>{this.props.label}</label>
</div>
You can solve this by storing the checked state of all child elements in the parent only. The children set their checked status based on props only (they don't use state for this) and call a callback supplied by the parent to change this.
E.g., in the child:
render: function() {
//... not showing other components...
<input type="checkbox"
value={this.props.value}
checked={this.props.checked}
onClick={this.props.onClick}>
}
The parent supplies the onClick, which changes the checked status of the child in its state and passes this back to the child when it re-renders.
In the parent:
getInitialState: function() {
return {
allChecked: false,
childrenChecked: new Array(NUM_CHILDREN) // initialise this somewhere (from props?)
}
},
render: function() {
return <div>
<input type="checkbox" checked={this.state.allChecked}>
{children.map(function(child, i) {
return <Child checked={this.state.childrenChecked[i]}
onClick={function(index) {
return function() {
// update this.state.allChecked and this.state.childrenChecked[index]
}.bind(this)
}.bind(this)(i)}
/>
}).bind(this)}
</div>;
}
-- not checked for typos etc.
Please see the react documentation on Lifting State Up.
In your child component, you need to use the props. To update the prop, you need to provide an update function from the parent.