Search code examples
reactjsclassconstantssetstate

I moved a function outside the class and made const. Now the function does not work


I moved the function addONe outside the class Counter. After doing this the function addOne does not work anymore, but on the other side I dont receive any error. My question is what I did wrong that the const addOne doesnt work?

import React from 'react';

const addOne = () => {
        this.setState((prevState) => {
            return {
                count: prevState.count + 1
            }
        })
    }


export default class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.minusOne = this.minusOne.bind(this);
        this.reset = this.reset.bind(this);
        this.state = {
            count: 0
        }
    }


    // addOne() {
    //     this.setState((prevState) => {
    //         return {
    //             count: prevState.count + 1
    //         }
    //     })
    // }

    minusOne() {
        this.setState((prevState) => {
            return {
                count: prevState.count - 1
            }
        })
    }

    reset() {
        this.setState(() => {
            return {
               count: 0// count: 0
            }
        })
    }

    render() {
        return (
            <div>
                <h1>Count: {this.state.count}</h1>
                <button onClick={this.addOne}>+1</button>
                <button onClick={this.minusOne}>-1</button>
                <button onClick={this.reset}>reset</button>
            </div>
        )
    } 
}

Solution

  • It cannot work.

    You are accessing this.setState inside your function, but you don't have any setState method in your scope, because it is proper of the React.Component class.

    The real question is: why you want to move out the class the addOne function?

    Edit due to your comment

    If you are learning, you need a more technical explanation :)

    First, you need to access the setState method of React.Component class outside of it, in your addOne function. We need to bind to this function a context where the this.setState exists. What better than the context of the component itself?

    Let's update the render method replacing this line:

    <button onClick={this.addOne}>+1</button>
    

    with this one:

    <button onClick={addOne.bind(this)}>+1</button>
    

    What's happening here? The function addOne is receiving an external context (in this case, this). This context will be used as the this keyword inside the function. Technically, binding another context creates a new bounded function, but this is another matter that we don't care here.

    If you try this little edit it still won't work.

    That's because you are declaring addOne as a lambda function expression. In ES6, lambda functions can't have other contexts bound to them.

    In fact, if you transform your addOne from a lambda function expression to a function declaration, like this:

    function addOne() {
      this.setState(prevState => ({
        count: prevState.count + 1
      }));
    }
    

    ...you'll see that the increase works properly :)

    Hope it helps you understant what's going on here.

    Keeping ES6 syntax

    Now, using ES6 lambda function expressions is not impossibile, it just move the same problem a little sideway. We cannot bind another context, but we can keep a closure on it calling a method that resides or has the right context.

    For instance we can transform the addOne function expression into a higher order function, that takes the setState and return the click handler you need:

    const addOne = setState => () =>
      setState(prevState => ({
        count: prevState.count + 1
      }))
    

    Now, in the render method, we may think to use it like:

    <button onClick={addOne(this.setState)}>+1</button>
    

    passing the setState as a function reference.

    The subtle problem now is that the setState inside the addOne function doesn't have it's original context bound!

    We can solve it with:

    <button onClick={addOne(this.setState.bind(this))}>+1</button>
    

    Edit due to your last comment

    These syntaxes behaves exactly the same:

    const addOne = setState => () =>
      setState(prevState => ({
        count: prevState.count + 1
      }))
    
    const addOne = setState => () =>
      setState(prevState => {
        return { count: prevState.count + 1 }
      })
    

    The way they work the same, is because with lambda function expressions, if you have a single statement as the body of your function, the result of the evaluation of that statement is automatically returned.

    This means that a lambda function can be as short as:

    const five = () => 5
    // five() returns immediate value 5
    

    Said that, when you have:

    prevState => {
      return { count: prevState.count + 1 }
    }
    

    you are creating a body with a single statement that returns a plain object.

    Do you see the similarity? You are doing the same thing: having a single statement that returns something.

    The first thing that may come up to your mind might be to rewrite it omitting the return keyword like:

    prevState => {
      count: prevState.count + 1
    }
    

    But this won't work. It will give you a syntax error due to the colon character that shouldn't be there. It shouldn't be there because if a lambda function expression body is defined as a block (wrapping everything with {}), it is supposed to have more than one statement and, maybe, a return keyword.

    To make it work, we need to treat the plain object definition as a single statement, and we do that by wrapping it into rounded parenthesis, like:

    prevState => ({
      count: prevState.count + 1
    })
    

    Again, hope it helps :)