From 5de58ad98d5047e6a00e9a298fcf2f01f83e5da6 Mon Sep 17 00:00:00 2001 From: SomeDer <48731521+SomeDer@users.noreply.github.com> Date: Tue, 24 Sep 2019 16:12:50 +0100 Subject: [PATCH] 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 * Mention "passing a function" instead of "alternative form" Co-Authored-By: Oliver Eyton-Williams * Rephrase explanation Co-Authored-By: Oliver Eyton-Williams * 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 --- .../bind-this-to-a-class-method.english.md | 34 ++++++++--------- ...-for-a-more-concise-conditional.english.md | 12 +++--- ...ssion-for-conditional-rendering.english.md | 12 +++--- .../use-state-to-toggle-an-element.english.md | 38 ++++++++++++++++--- .../react/write-a-simple-counter.english.md | 12 +++--- 5 files changed, 68 insertions(+), 40 deletions(-) diff --git a/curriculum/challenges/english/03-front-end-libraries/react/bind-this-to-a-class-method.english.md b/curriculum/challenges/english/03-front-end-libraries/react/bind-this-to-a-class-method.english.md index 88416bcfb1..8e29ff086c 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/bind-this-to-a-class-method.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/bind-this-to-a-class-method.english.md @@ -15,9 +15,9 @@ One common way is to explicitly bind this in the constructor so -The code editor has a component with a state 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 this keyword that is undefined. Fix it by explicitly binding this to the addItem() method in the component's constructor. -Next, add a click handler to the button element in the render method. It should trigger the addItem() method when the button receives a click event. Remember that the method you pass to the onClick 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. +The code editor has a component with a state that keeps track of the text. It also has a method which allows you to set the text to "You clicked!". However, the method doesn't work because it's using the this keyword that is undefined. Fix it by explicitly binding this to the handleClick() method in the component's constructor. +Next, add a click handler to the button element in the render method. It should trigger the handleClick() method when the button receives a click event. Remember that the method you pass to the onClick 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 You clicked!. ## Tests @@ -27,10 +27,10 @@ Once you complete the above steps you should be able to click the button and see tests: - text: MyComponent should return a div element which wraps two elements, a button and an h1 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'); - - text: 'The state of MyComponent should initialize with the key value pair { itemCount: 0 }.' - testString: 'assert(Enzyme.mount(React.createElement(MyComponent)).state(''itemCount'') === 0);' - - text: Clicking the button element should run the addItem method and increment the state itemCount by 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({ 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); };' + - text: 'The state of MyComponent should initialize with the key value pair { text: "Hello" }.' + testString: 'assert(Enzyme.mount(React.createElement(MyComponent)).state(''text'') === ''Hello'');' + - text: Clicking the button element should run the handleClick method and set the state text to "You clicked!". + 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) { super(props); this.state = { - itemCount: 0 + text: "Hello" }; // change code below this line // change code above this line } - addItem() { + handleClick() { this.setState({ - itemCount: this.state.itemCount + 1 + text: "You clicked!" }); } render() { @@ -63,7 +63,7 @@ class MyComponent extends React.Component { { /* change code below this line */ } { /* change code above this line */ } -

Current Item Count: {this.state.itemCount}

+

{this.state.text}

); } @@ -93,20 +93,20 @@ class MyComponent extends React.Component { constructor(props) { super(props); this.state = { - itemCount: 0 + text: "Hello" }; - this.addItem = this.addItem.bind(this); + this.handleClick = this.handleClick.bind(this); } - addItem() { + handleClick() { this.setState({ - itemCount: this.state.itemCount + 1 + text: "You clicked!" }); } render() { return (
- -

Current Item Count: {this.state.itemCount}

+ +

{this.state.text}

); } diff --git a/curriculum/challenges/english/03-front-end-libraries/react/use--for-a-more-concise-conditional.english.md b/curriculum/challenges/english/03-front-end-libraries/react/use--for-a-more-concise-conditional.english.md index ed9238d774..4f82ee2bb5 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/use--for-a-more-concise-conditional.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/use--for-a-more-concise-conditional.english.md @@ -51,9 +51,9 @@ class MyComponent extends React.Component { this.toggleDisplay = this.toggleDisplay.bind(this); } toggleDisplay() { - this.setState({ - display: !this.state.display - }); + this.setState(state => ({ + display: !state.display + })); } render() { // change code below this line @@ -95,9 +95,9 @@ class MyComponent extends React.Component { this.toggleDisplay = this.toggleDisplay.bind(this); } toggleDisplay() { - this.setState({ - display: !this.state.display - }); + this.setState(state => ({ + display: !state.display + })); } render() { // change code below this line diff --git a/curriculum/challenges/english/03-front-end-libraries/react/use-a-ternary-expression-for-conditional-rendering.english.md b/curriculum/challenges/english/03-front-end-libraries/react/use-a-ternary-expression-for-conditional-rendering.english.md index 018f8b768c..75f332cef3 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/use-a-ternary-expression-for-conditional-rendering.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/use-a-ternary-expression-for-conditional-rendering.english.md @@ -74,9 +74,9 @@ class CheckUserAge extends React.Component { }); } submit() { - this.setState({ - userAge: this.state.input - }); + this.setState(state => ({ + userAge: state.input + })); } render() { const buttonOne = ; @@ -140,9 +140,9 @@ class CheckUserAge extends React.Component { }); } submit() { - this.setState({ - userAge: this.state.input - }); + this.setState(state => ({ + userAge: state.input + })); } render() { const buttonOne = ; diff --git a/curriculum/challenges/english/03-front-end-libraries/react/use-state-to-toggle-an-element.english.md b/curriculum/challenges/english/03-front-end-libraries/react/use-state-to-toggle-an-element.english.md index 45e262a485..b3d05c17c9 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/use-state-to-toggle-an-element.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/use-state-to-toggle-an-element.english.md @@ -8,13 +8,37 @@ forumTopicId: 301421 ## Description
-You can use state 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 setState() calls into a single update. This means you can't rely on the previous value of this.state or this.props 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 setState a function that allows you to access state and props. Using a function with setState 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.
## Instructions
MyComponent has a visibility property which is initialized to false. The render method returns one view if the value of visibility is true, and a different view if it is false. -Currently, there is no way of updating the visibility property in the component's state. 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 toggleVisibility(). Define this method so the state of visibility toggles to the opposite value when the method is called. If visibility is false, the method sets it to true, and vice versa. +Currently, there is no way of updating the visibility property in the component's state. 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 toggleVisibility(). Pass a function to setState to define this method so that the state of visibility toggles to the opposite value when the method is called. If visibility is false, the method sets it to true, and vice versa. Finally, click the button to see the conditional rendering of the component based on its state. Hint: Don't forget to bind the this keyword to the method in the constructor!
@@ -30,6 +54,10 @@ tests: testString: assert.strictEqual(Enzyme.mount(React.createElement(MyComponent)).state('visibility'), false); - text: Clicking the button element should toggle the visibility property in state between true and false. 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 setState. + 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: this should not be used inside setState + testString: assert(!/this\.setState\([^}]*this/.test(code)); ``` @@ -101,9 +129,9 @@ class MyComponent extends React.Component { this.toggleVisibility = this.toggleVisibility.bind(this); } toggleVisibility() { - this.setState({ - visibility: !this.state.visibility - }); + this.setState(state => ({ + visibility: !state.visibility + })); } render() { if (this.state.visibility) { diff --git a/curriculum/challenges/english/03-front-end-libraries/react/write-a-simple-counter.english.md b/curriculum/challenges/english/03-front-end-libraries/react/write-a-simple-counter.english.md index ec8ce2f050..bbf7f8ddb9 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/write-a-simple-counter.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/write-a-simple-counter.english.md @@ -104,14 +104,14 @@ class Counter extends React.Component { }); } increment() { - this.setState({ - count: this.state.count + 1 - }); + this.setState(state => ({ + count: state.count + 1 + })); } decrement() { - this.setState({ - count: this.state.count - 1 - }); + this.setState(state => ({ + count: state.count - 1 + })); } render() { return (