I am following React Doc. In 'Handling Events' section, the below code segment is there.
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// This binding is necessary to make `this` work in the callback
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
in handleClick() function, how state is available for it to access? Why not this.state?
This is how this.setState works, its an updater callback passed for this.setState(), as per react documentation:
Passing an update function allows you to access the current state
value inside the updater. Since setState calls are batched, this lets
you chain updates and ensure they build on top of each other instead
of conflicting
more information can be found here as well.
Because it's a function parameter. In this code:
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
This part, is an arrow function, which takes previous state of the component, as a parameter:
state => ({
isToggleOn: !state.isToggleOn
})
And returns a new state, which triggers re-render and so on.
Why not this.state?
The rule of thumb: If your next state depends on previous state, you must use this approach to update your state. So you don't run into a race condition with this.setState calls because is asynchronous function.
Hope it helps :)
Related
I am following along with a video tutorial on using React. The presenter is currently detailing how to add a toggle button to a UI. They said to give it a go first before seeing how they do it, so I implemented it myself. My implementation was a little different to theirs, just the handler was different; but it does seem to work.
Can anyone with more experience using React tell me, is my toggleSideDrawerHandler wrong in some way? Or is it a valid shorter way of setting the state that depends on a previous state?
My implementation:
//Layout.js
class Layout extends Component {
state = {
showSideDrawer: false
};
toggleSideDrawerHandler = prevState => {
let newState = !prevState.showSideDrawer;
this.setState({ showSideDrawer: newState });
};
closeSideDrawerHandler = () => {
this.setState({ showSideDrawer: false });
};
render() {
return (
<Fragment>
<Toolbar drawerToggleClicked={this.toggleSideDrawerHandler} />
<SideDrawer
open={this.state.showSideDrawer}
close={this.closeSideDrawerHandler}
/>
<main className={styles.Content}>{this.props.children}</main>
</Fragment>
);
}
}
//Toolbar.js
const toolbar = props => (
<header className={styles.Toolbar}>
<DrawerToggle clicked={props.drawerToggleClicked} />
<div className={styles.Logo}>
<Logo />
</div>
<nav className={styles.DesktopOnly}>
<NavItems />
</nav>
</header>
);
Tutorial implementation:
toggleSideDrawerHandler = () => {
this.setState(prevState => {
return { showSideDrawer: !prevState.showSideDrawer };
});
};
Your solution works, but I guess in the part, where you call the toggleSideDrawerHandler you probably call it like
() => this.toggleSideDrawerHandler(this.state)
right?
If not, can you please paste the rest of your code (especially the calling part) to see where you get the prevState from?
This works, because you pass the old state to the method.
I would personally prefer the tutorials implementation, because it takes care of dependencies and the "user" (the dev using it) doesn't need to know anything about the expected data.
With the second implementation all you need to do is call the function and not think about getting and passing the old state to it.
Update after adding the rest of the code:
I think the reason, why it works is because the default value for your parameter is the one passed by the event by default, which is an event object.
If you use prevState.showSideDrawer you are calling an unknown element on this event object, that will be null.
Now if you use !prevState.showSideDrawer, you are actually defining it as !null (inverted null/false), which will be true.
This is why it probably works.
Maybe try to toggle your code twice, by showing and hiding it again.
Showing it will probably work, but hiding it again will not.
This is why the other code is correct.
You should stick to the tutorial implementation. There is no point in passing component state to the children and then from them back to the parents. Your state should be only in one place (in this case in Layout).
Child components should be only given access to the information they need which in this case is just showSideDrawer.
You are using this:
toggleSideDrawerHandler = prevState => {
let newState = !prevState.showSideDrawer;
this.setState({ showSideDrawer: newState });
};
This is a conventional way to update state in react, where we are defining the function and updating state inside. Though you are using term prevState but it doesn't holds any value of components states. When you call toggleSideDrawerHandler method you have to pass value and prevState will hold that value. The other case as tutorial is using:
toggleSideDrawerHandler = () => {
this.setState(prevState => {
return { showSideDrawer: !prevState.showSideDrawer };
});
};
This is called functional setStae way of updating state. In this function is used in setState methods first argument. So prevState will have a value equal to all the states in the component.Check the example below to understand the difference between two:
// Example stateless functional component
const SFC = props => (
<div>{props.label}</div>
);
// Example class component
class Thingy extends React.Component {
constructor() {
super();
this.state = {
temp: [],
};
}
componentDidMount(){
this.setState({temp: this.state.temp.concat('a')})
this.setState({temp: this.state.temp.concat('b')})
this.setState({temp: this.state.temp.concat('c')})
this.setState({temp: this.state.temp.concat('d')})
this.setState(prevState => ({temp: prevState.temp.concat('e')}))
this.setState(prevState => ({temp: prevState.temp.concat('f')}))
this.setState(prevState => ({temp: prevState.temp.concat('g')}))
}
render() {
const {title} = this.props;
const {temp} = this.state;
return (
<div>
<div>{title}</div>
<SFC label="I'm the SFC inside the Thingy" />
{ temp.map(value => ( <div>Concating {value}</div> )) }
</div>
);
}
}
// Render it
ReactDOM.render(
<Thingy title="I'm the thingy" />,
document.getElementById("react")
);
<div id="react"></div>
<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>
So depending on requirement you will use one of the two ways to update the state.
So I have some confusion regarding the async nature of setState in ReactJS.
As per React docs, you shouldn't use this.state inside setState(). But if I have a counter as a state and i want to update it on click like this:
class App extends React.Component {
state = { counter: 0 }
onClick = () => {
this.setState({counter: this.state.counter + 1})
}
render() {
return (
<div>
<div>{this.state.counter}</div>
<p onClick={this.onClick}>Click me</p>
</div>
)
}
}
This works as expected. So why is this code wrong?
UPDATE: I know that setState is async, and it accepts a callback which has previous state as an argument, but I am not sure why I should use it here? I want to refer to the old state inside setState, so why should I use the callback function in this case? Whenever this.setState() is executed, this.state inside it will always refer to the old state, and its value will be changed to the new state only after setState has finished executing, not while it is executing.
You have access to prevState from within your setState call:
this.setState((prevState) => ({
counter: prevState.counter +1
}))
That will allow you to safely increment the current state value.
The React documentation summarises why you cannot rely on this.state to be accurate during update: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
setState accepts a function as a parameter with previous state as argument.
Refacto as follows to avoid competition problems:
onClick = () => {
this.setState(prevState => ({counter: prevState.counter + 1}))
}
This question already has answers here:
Why calling setState method doesn't mutate the state immediately?
(3 answers)
Closed 4 years ago.
I have simple component
class App extends Component {
handleClick() {
let banana = {message: 'banana triggered'};
this.setState({banana});
console.log(this); // banana is set in state!!!!
console.log(this.state); // state is null :O
setTimeout(() => {
console.log(this.state); // banana is set!
}, 5)
}
render() {
const {state, actions} = this.props;
return (
<div>
{this.state && this.state.banana.message} <br />
<button onClick={() => this.handleClick()}>Test</button>
{state.alert.message && <p>{state.alert.message}</p>}
<p onClick={() => actions.alert.success("This is not")}>
This is magic
</p>
</div>
)
};
}
export default connect(
state => (
{
state: {...state}
}
),
dispatch => (
{
actions: {
dispatch: dispatch,
alert: {
success: text => dispatch(alert.success(text))
}
}
}
)
)(App);
problem is what i need to add this.state && in my JSX rendering to check if this.state exists at all, i understand what in JavaScript it's normal, but is not normal in React.js? Should he react to state change instantly? Also what get me confused, is what from two console.logs, first (this) have banana set in state, and second one is empty. How?
Image below:
p.s. there is no such problem with Redux, only local component state
react's docs mention that state updates are asynchronous.
In order to act based on the change of the state, react setState function provides a callback which you can use as follows:
this.setState({banana}, () => {
console.log(this.state);
});
In regards to your comment, the value of the state didn't actually exist when it was printed. the value was calculated only after you clicked the expand arrow in the console see this for more deatils
According to react docs, setState() is asynchronous, and multiple calls during the same cycle may be batched together.
If you check the updated state value, you can add a callback method
this.setState({ banana }, ()=> {
// console.log(this.state);
// Here's the updated state
});
In your case, the first console.log(this) doesn't set the banana. See your code in Sandbox. It looks like first two console logs don't show any state as the initial state is null and after the timeout when the asynchronous call has finished it set the state with banana.
I want to use the 'compare' button to toggle the compare state to true or false.
Next I want to pass this compare state to pivot as props.
I am literally using the same code as in the react documentation when looking at the Toggle class. https://facebook.github.io/react/docs/handling-events.html
The only thing I changed is the name isToggleOn to compare.
When looking at the console client side I get following error every time the component renders:
modules.js?hash=5bd264489058b9a37cb27e36f529f99e13f95b78:3941 Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.`
My code is following:
class Dashboard extends Component {
constructor(props) {
super(props);
this.state = { compare: true };
this.handleClick = this.handleClick.bind(this);
}
handleClick(button) {
if (button === 'compare') {
this.setState(prevState => ({
compare: !prevState.compare,
}));
}
}
render() {
return (
<Grid>
<div className="starter-template">
<h1>This is the dashboard page.</h1>
<p className="lead">
Use this document as a way to quickly start any new project.<br />{' '}
All you get is this text and a mostly barebones HTML document.
</p>
</div>
<ButtonToolbar>
<button onClick={this.handleClick('compare')}>
{this.state.compare ? 'AGGREGATE' : 'COMPARE'}
</button>
</ButtonToolbar>
<PivotTable
ready={this.props.isReady}
data={this.props.gapData}
compare={this.state.compare}
/>
</Grid>
);
}
}
export default (DashboardContainer = createContainer(() => {
// Do all your reactive data access in this method.
// Note that this subscription will get cleaned up when your component is unmounted
const handle = Meteor.subscribe('weekly-dashboard');
return {
isReady: handle.ready(),
gapData: WeeklyDashboard.find({}).fetch(),
};
}, Dashboard));
Any advice on how to fix this?
The reason is this line
<button onClick={this.handleClick('compare')}>
This will call the handleClick function while executing render function. You can fix by:
<button onClick={() => this.handleClick('compare')}>
Or
const handleBtnClick = () => this.handleClick('compare');
...
<button onClick={this.handleBtnClick}>
...
I prefer the latter
I want to make a loading indicator on user login, but for some reason the JSX conditional element does not update:
class LoginPage extends Component {
constructor (props) {
super(props)
this.state = {
waitingOnLogin: false,
...
}
this.handleLogin = this.handleLogin.bind(this)
}
handleLogin (event) {
event.preventDefault()
this.state.waitingOnLogin = true
this.props.userActions.login(this.state.email, this.state.password)
}
render() {
return (
<div className='up'>
<form onSubmit={e => this.handleLogin(e)}> ... </form>
{this.state.waitingOnLogin && <Spinner message='Logging you in'/>} // This does not appear
</div>
)
}
}
Why does the waitingOnLogin is being ignored by the JSX?
Don't mutate state directly use setState. setState calls for rerender and hence after that your change will reflect but with direct assignment no rerender occurs and thus no change is reflected. Also you should always use setState to change state
handleLogin (event) {
event.preventDefault()
this.setState({waitingOnLogin:true});
this.props.userActions.login(this.state.email, this.state.password)
}
Always use setState to update the state value, never mutate the state values directly, Use this:
handleLogin (event) {
event.preventDefault()
this.setState({ waitingOnLogin: true });
this.props.userActions.login(this.state.email, this.state.password)
}
As per DOC:
Never mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
Check the details about setState.