fix(curriculum): use function form of this.setState (#36623)

* Fix solution

* Update description

* Add test for this.setState function syntax

* Update toggle element instructions

* Fix simple-counter solutions

* Fix bind-this challenge seed and solution

* Fix && challenge seed and solution

* Fix ternary challenge seed and solution

* Use the function form -> Pass a function

Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* Mention "passing a function" instead of "alternative form"

Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* Rephrase explanation

Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* clarify why functions should be used

Co-Authored-By: Randell Dawson <5313213+RandellDawson@users.noreply.github.com>

* Improve tests

* Fix \s escaping in []

* Improve regex

Co-Authored-By: Randell Dawson <5313213+RandellDawson@users.noreply.github.com>

* Add test for `this`

* Use block code

* Update bind-this challenge

* Fix last part of instructions

Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
SomeDer
2019-09-24 16:12:50 +01:00
committed by Randell Dawson
parent d08f009bd1
commit 5de58ad98d
5 changed files with 68 additions and 40 deletions

View File

@ -15,9 +15,9 @@ One common way is to explicitly bind <code>this</code> in the constructor so <co
## Instructions ## Instructions
<section id='instructions'> <section id='instructions'>
The code editor has a component with a <code>state</code> that keeps track of an item count. It also has a method which allows you to increment this item count. However, the method doesn't work because it's using the <code>this</code> keyword that is undefined. Fix it by explicitly binding <code>this</code> to the <code>addItem()</code> method in the component's constructor. The code editor has a component with a <code>state</code> that keeps track of the text. It also has a method which allows you to set the text to <code>"You clicked!"</code>. However, the method doesn't work because it's using the <code>this</code> keyword that is undefined. Fix it by explicitly binding <code>this</code> to the <code>handleClick()</code> method in the component's constructor.
Next, add a click handler to the <code>button</code> element in the render method. It should trigger the <code>addItem()</code> method when the button receives a click event. Remember that the method you pass to the <code>onClick</code> handler needs curly braces because it should be interpreted directly as JavaScript. Next, add a click handler to the <code>button</code> element in the render method. It should trigger the <code>handleClick()</code> method when the button receives a click event. Remember that the method you pass to the <code>onClick</code> handler needs curly braces because it should be interpreted directly as JavaScript.
Once you complete the above steps you should be able to click the button and see the item count increment in the HTML. Once you complete the above steps you should be able to click the button and see <code>You clicked!</code>.
</section> </section>
## Tests ## Tests
@ -27,10 +27,10 @@ Once you complete the above steps you should be able to click the button and see
tests: tests:
- text: <code>MyComponent</code> should return a <code>div</code> element which wraps two elements, a button and an <code>h1</code> element, in that order. - text: <code>MyComponent</code> should return a <code>div</code> element which wraps two elements, a button and an <code>h1</code> element, in that order.
testString: assert(Enzyme.mount(React.createElement(MyComponent)).find('div').length === 1 && Enzyme.mount(React.createElement(MyComponent)).find('div').childAt(0).type() === 'button' && Enzyme.mount(React.createElement(MyComponent)).find('div').childAt(1).type() === 'h1'); testString: assert(Enzyme.mount(React.createElement(MyComponent)).find('div').length === 1 && Enzyme.mount(React.createElement(MyComponent)).find('div').childAt(0).type() === 'button' && Enzyme.mount(React.createElement(MyComponent)).find('div').childAt(1).type() === 'h1');
- text: 'The state of <code>MyComponent</code> should initialize with the key value pair <code>{ itemCount: 0 }</code>.' - text: 'The state of <code>MyComponent</code> should initialize with the key value pair <code>{ text: "Hello" }</code>.'
testString: 'assert(Enzyme.mount(React.createElement(MyComponent)).state(''itemCount'') === 0);' testString: 'assert(Enzyme.mount(React.createElement(MyComponent)).state(''text'') === ''Hello'');'
- text: Clicking the <code>button</code> element should run the <code>addItem</code> method and increment the state <code>itemCount</code> by <code>1</code>. - text: Clicking the <code>button</code> element should run the <code>handleClick</code> method and set the state <code>text</code> to <code>"You clicked!"</code>.
testString: 'async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ itemCount: 0 }); return waitForIt(() => mockedComponent.state(''itemCount'')); }; const second = () => { mockedComponent.find(''button'').simulate(''click''); return waitForIt(() => mockedComponent.state(''itemCount'')); }; const firstValue = await first(); const secondValue = await second(); assert(firstValue === 0 && secondValue === 1); };' testString: 'async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ text: ''Hello'' }); return waitForIt(() => mockedComponent.state(''text'')); }; const second = () => { mockedComponent.find(''button'').simulate(''click''); return waitForIt(() => mockedComponent.state(''text'')); }; const firstValue = await first(); const secondValue = await second(); assert(firstValue === ''Hello'' && secondValue === ''You clicked!''); };'
``` ```
@ -46,15 +46,15 @@ class MyComponent extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
itemCount: 0 text: "Hello"
}; };
// change code below this line // change code below this line
// change code above this line // change code above this line
} }
addItem() { handleClick() {
this.setState({ this.setState({
itemCount: this.state.itemCount + 1 text: "You clicked!"
}); });
} }
render() { render() {
@ -63,7 +63,7 @@ class MyComponent extends React.Component {
{ /* change code below this line */ } { /* change code below this line */ }
<button>Click Me</button> <button>Click Me</button>
{ /* change code above this line */ } { /* change code above this line */ }
<h1>Current Item Count: {this.state.itemCount}</h1> <h1>{this.state.text}</h1>
</div> </div>
); );
} }
@ -93,20 +93,20 @@ class MyComponent extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
itemCount: 0 text: "Hello"
}; };
this.addItem = this.addItem.bind(this); this.handleClick = this.handleClick.bind(this);
} }
addItem() { handleClick() {
this.setState({ this.setState({
itemCount: this.state.itemCount + 1 text: "You clicked!"
}); });
} }
render() { render() {
return ( return (
<div> <div>
<button onClick = {this.addItem}>Click Me</button> <button onClick = {this.handleClick}>Click Me</button>
<h1>Current Item Count: {this.state.itemCount}</h1> <h1>{this.state.text}</h1>
</div> </div>
); );
} }

View File

@ -51,9 +51,9 @@ class MyComponent extends React.Component {
this.toggleDisplay = this.toggleDisplay.bind(this); this.toggleDisplay = this.toggleDisplay.bind(this);
} }
toggleDisplay() { toggleDisplay() {
this.setState({ this.setState(state => ({
display: !this.state.display display: !state.display
}); }));
} }
render() { render() {
// change code below this line // change code below this line
@ -95,9 +95,9 @@ class MyComponent extends React.Component {
this.toggleDisplay = this.toggleDisplay.bind(this); this.toggleDisplay = this.toggleDisplay.bind(this);
} }
toggleDisplay() { toggleDisplay() {
this.setState({ this.setState(state => ({
display: !this.state.display display: !state.display
}); }));
} }
render() { render() {
// change code below this line // change code below this line

View File

@ -74,9 +74,9 @@ class CheckUserAge extends React.Component {
}); });
} }
submit() { submit() {
this.setState({ this.setState(state => ({
userAge: this.state.input userAge: state.input
}); }));
} }
render() { render() {
const buttonOne = <button onClick={this.submit}>Submit</button>; const buttonOne = <button onClick={this.submit}>Submit</button>;
@ -140,9 +140,9 @@ class CheckUserAge extends React.Component {
}); });
} }
submit() { submit() {
this.setState({ this.setState(state => ({
userAge: this.state.input userAge: state.input
}); }));
} }
render() { render() {
const buttonOne = <button onClick={this.submit}>Submit</button>; const buttonOne = <button onClick={this.submit}>Submit</button>;

View File

@ -8,13 +8,37 @@ forumTopicId: 301421
## Description ## Description
<section id='description'> <section id='description'>
You can use <code>state</code> in React applications in more complex ways than what you've seen so far. One example is to monitor the status of a value, then render the UI conditionally based on this value. There are several different ways to accomplish this, and the code editor shows one method. Sometimes you might need to know the previous state when updating the state. However, state updates may be asynchronous - this means React may batch multiple <code>setState()</code> calls into a single update. This means you can't rely on the previous value of <code>this.state</code> or <code>this.props</code> when calculating the next value. So, you should not use code like this:
```js
this.setState({
counter: this.state.counter + this.props.increment
});
```
Instead, you should pass <code>setState</code> a function that allows you to access state and props. Using a function with <code>setState</code> guarantees you are working with the most current values of state and props. This means that the above should be rewritten as:
```js
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
```
You can also use a form without `props` if you need only the `state`:
```js
this.setState(state => ({
counter: state.counter + 1
}));
```
Note that you have to wrap the object literal in parentheses, otherwise JavaScript thinks it's a block of code.
</section> </section>
## Instructions ## Instructions
<section id='instructions'> <section id='instructions'>
<code>MyComponent</code> has a <code>visibility</code> property which is initialized to <code>false</code>. The render method returns one view if the value of <code>visibility</code> is true, and a different view if it is false. <code>MyComponent</code> has a <code>visibility</code> property which is initialized to <code>false</code>. The render method returns one view if the value of <code>visibility</code> is true, and a different view if it is false.
Currently, there is no way of updating the <code>visibility</code> property in the component's <code>state</code>. The value should toggle back and forth between true and false. There is a click handler on the button which triggers a class method called <code>toggleVisibility()</code>. Define this method so the <code>state</code> of <code>visibility</code> toggles to the opposite value when the method is called. If <code>visibility</code> is <code>false</code>, the method sets it to <code>true</code>, and vice versa. Currently, there is no way of updating the <code>visibility</code> property in the component's <code>state</code>. The value should toggle back and forth between true and false. There is a click handler on the button which triggers a class method called <code>toggleVisibility()</code>. Pass a function to <code>setState</code> to define this method so that the <code>state</code> of <code>visibility</code> toggles to the opposite value when the method is called. If <code>visibility</code> is <code>false</code>, the method sets it to <code>true</code>, and vice versa.
Finally, click the button to see the conditional rendering of the component based on its <code>state</code>. Finally, click the button to see the conditional rendering of the component based on its <code>state</code>.
<strong>Hint:</strong>&nbsp;Don't forget to bind the <code>this</code> keyword to the method in the constructor! <strong>Hint:</strong>&nbsp;Don't forget to bind the <code>this</code> keyword to the method in the constructor!
</section> </section>
@ -30,6 +54,10 @@ tests:
testString: assert.strictEqual(Enzyme.mount(React.createElement(MyComponent)).state('visibility'), false); testString: assert.strictEqual(Enzyme.mount(React.createElement(MyComponent)).state('visibility'), false);
- text: Clicking the button element should toggle the <code>visibility</code> property in state between <code>true</code> and <code>false</code>. - text: Clicking the button element should toggle the <code>visibility</code> property in state between <code>true</code> and <code>false</code>.
testString: 'async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ visibility: false }); return waitForIt(() => mockedComponent.state(''visibility'')); }; const second = () => { mockedComponent.find(''button'').simulate(''click''); return waitForIt(() => mockedComponent.state(''visibility'')); }; const third = () => { mockedComponent.find(''button'').simulate(''click''); return waitForIt(() => mockedComponent.state(''visibility'')); }; const firstValue = await first(); const secondValue = await second(); const thirdValue = await third(); assert(!firstValue && secondValue && !thirdValue); }; ' testString: 'async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ visibility: false }); return waitForIt(() => mockedComponent.state(''visibility'')); }; const second = () => { mockedComponent.find(''button'').simulate(''click''); return waitForIt(() => mockedComponent.state(''visibility'')); }; const third = () => { mockedComponent.find(''button'').simulate(''click''); return waitForIt(() => mockedComponent.state(''visibility'')); }; const firstValue = await first(); const secondValue = await second(); const thirdValue = await third(); assert(!firstValue && secondValue && !thirdValue); }; '
- text: An anonymous function should be passed to <code>setState</code>.
testString: const paramRegex = '[a-zA-Z$_]\\w*(,[a-zA-Z$_]\\w*)?'; const noSpaces = code.replace(/\s/g, ''); assert(new RegExp('this\\.setState\\((function\\(' + paramRegex + '\\){|([a-zA-Z$_]\\w*|\\(' + paramRegex + '\\))=>)').test(noSpaces));
- text: <code>this</code> should not be used inside <code>setState</code>
testString: assert(!/this\.setState\([^}]*this/.test(code));
``` ```
@ -101,9 +129,9 @@ class MyComponent extends React.Component {
this.toggleVisibility = this.toggleVisibility.bind(this); this.toggleVisibility = this.toggleVisibility.bind(this);
} }
toggleVisibility() { toggleVisibility() {
this.setState({ this.setState(state => ({
visibility: !this.state.visibility visibility: !state.visibility
}); }));
} }
render() { render() {
if (this.state.visibility) { if (this.state.visibility) {

View File

@ -104,14 +104,14 @@ class Counter extends React.Component {
}); });
} }
increment() { increment() {
this.setState({ this.setState(state => ({
count: this.state.count + 1 count: state.count + 1
}); }));
} }
decrement() { decrement() {
this.setState({ this.setState(state => ({
count: this.state.count - 1 count: state.count - 1
}); }));
} }
render() { render() {
return ( return (