DEV Community

Hayk Safaryan
Hayk Safaryan

Posted on

React Hooks Re-intro

React Hooks Re-intro

The original post is in this repo
https://github.com/hayk94/react-hooks-intro-coffeeshop

where each branch has a readme.md explaining over the code.

This repository is for React Hooks introduction.

It consists of several numbered branches.

In each branch readme file goes over the code,

explaining the advantages and caveats.

Here in the master branch is a plain CRA app.

There are some additional configs for eslint using eslint-config-fbjs

with eslint-plugin-react-hooks.
And prettier config.

Disclaimer

Most stuff in this repo and posts have already been discussed,

by react team in the docs, by Dan Abramov in his amazing talk and blog,

and other people.

This repo is just a summation of my knowledge about hooks I gathered so far.
Just putting stuff in my own words.

Resources

https://reactjs.org/docs/hooks-intro.html

https://dev.to/dan_abramov/making-sense-of-react-hooks-2eib

https://overreacted.io/react-as-a-ui-runtime/

https://overreacted.io/making-setinterval-declarative-with-react-hooks/

https://overreacted.io/how-are-function-components-different-from-classes/

https://overreacted.io/a-complete-guide-to-useeffect/

https://overreacted.io/writing-resilient-components/

App Intro

Imagine we are writing a coffeeshop menu app.

Users can choose the product they want and order it.

Good Old Classes

First we make aMenu.js component.

import React, {Component} from 'react';

class Menu extends Component {
  render() {
    return (
      <div>
        Menu
      </div>
    );
  }
}

export default Menu;

And make it render in our App.js component.

import React, { Component } from 'react';
import './App.css';
import Menu from './Menu';

class App extends Component {
  render() {
    return (
      <div className="App">
        <section>
          <Menu/>
        </section>
      </div>
    );
  }
}

export default App;

Then we add a select to our menu app, with some products.

Add a button for ordering.

import React, {Component} from 'react';

class Menu extends Component {
  render() {
      return (
        <div>
          <b>Order: </b>
          <select>
            <option value="Purple Haze">Purple Haze</option>
            <option value="Amnesia">Amnesia</option>
            <option value="GoGreen">GoGreen</option>
          </select>
          <div>
            <button>Order</button>
          </div>
        </div>
      );
    }
}

export default Menu;

Apparently this is what they sell at coffeeshops, isn't it?

Now we need some state for the selected product.

import React, {Component} from 'react';

class Menu extends Component {
  constructor(props) {
      super(props);
      this.state = {
        selected: 'Purple Haze',
      };
  }

  render() {
      return (
        <div>
          <b>Order: </b>
          <select>
            <option value="Purple Haze">Purple Haze</option>
            <option value="Amnesia">Amnesia</option>
            <option value="GoGreen">GoGreen</option>
          </select>
          <div>
            <button>Order</button>
          </div>
        </div>
      );
    }
}

export default Menu;

Finally we need methods to select the product and order it.

import React, {Component} from 'react';

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: 'Purple Haze',
    };
  }

  onChange(e) {
    this.setState({selected: e.target.value});
  }

  onOrder() {
    alert(`You ordered ${this.state.selected}`);
  }
  render() {
    return (
      <div>
        <b>Order: </b>
        <select onChange={this.onChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
        <div>
          <button onClick={this.onOrder}>Order</button>
        </div>
      </div>
    );
  }
}

export default Menu;

And here we made our app that's it.

Now lets go and order some stuff.

And...

Oops...

Right...

We get a big nice error message, guess what?

Turn to the next branch...

And now this!

So now we have an error! Or rather the error...

You know it right?

Let's fix it!

import React, {Component} from 'react';

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: 'Purple Haze',
    };
    this.onChange = this.onChange.bind(this);
    this.onOrder = this.onOrder.bind(this);
  }

  onChange(e) {
    this.setState({selected: e.target.value});
  }

  onOrder() {
    alert(`You ordered ${this.state.selected}`);
  }
  render() {
    return (
      <div>
        <b>Order: </b>
        <select onChange={this.onChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
        <div>
          <button onClick={this.onOrder}>Order</button>
        </div>
      </div>
    );
  }
}

export default Menu;

We needed to bind this.

New Requirements

Suddenly we got new requirements from the client...

They want the page title to be the selected item of the user.

So we need something like this.

import React, {Component} from 'react';

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: 'Purple Haze',
    };
    this.onChange = this.onChange.bind(this);
    this.onOrder = this.onOrder.bind(this);
  }

  componentDidUpdate() {
    document.title = `Selected - ${this.state.selected}`;
  }

  onChange(e) {
    this.setState({selected: e.target.value});
  }

  onOrder() {
    alert(`You ordered ${this.state.selected}`);
  }
  render() {
    return (
      <div>
        <b>Order: </b>
        <select onChange={this.onChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
        <div>
          <button onClick={this.onOrder}>Order</button>
        </div>
      </div>
    );
  }
}

export default Menu;

Lifecycle methods yay!

Now we got it once user selects an item the document title changes accordingly.

Yet another requirement

And now we've been given another task based on the new requirements.
Users should be able to tell us how many products they want to order.

Pretty easy, right?

import React, {Component} from 'react';

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: 'Purple Haze',
      count: 0,
    };

    this.onProductChange = this.onProductChange.bind(this);
    this.onOrder = this.onOrder.bind(this);
    this.onCountChange = this.onCountChange.bind(this);
  }

  componentDidUpdate() {
    document.title = `Selected - ${this.state.selected}`;
  }

  onProductChange(e) {
    this.setState({selected: e.target.value});
  }

  onOrder() {
    alert(`You ordered ${this.state.count} ${this.state.selected}`);
  }

  onCountChange(e) {
    this.setState({count: e.target.value});
  }

  render() {
    return (
      <div>
        <div>
          <b>Product: </b>
          <select onChange={this.onProductChange}>
            <option value="Purple Haze">Purple Haze</option>
            <option value="Amnesia">Amnesia</option>
            <option value="GoGreen">GoGreen</option>
          </select>
        </div>
        <div>
          <b>Count: </b>
          <input
            type="number"
            min={0}
            value={this.state.count}
            onChange={this.onCountChange}
          />
        </div>
        <div>
          <button onClick={this.onOrder}>Order</button>
        </div>
      </div>
    );
  }
}

export default Menu;

We add a number input, count to our state, and an onCountChange method.

Oh and right, we need to bind this.

Great we accomplished a lot today and feel proud.

Oh no, a bug was just reported

Whoops... We just barely finished the previous task,

yet a bug was reported from our previous feature.

They say when the users enter the page first time,
the page title doesn't show the selected product.

But it's not a bug! The user didn't select any product yet!

Oh really? It's a bug and you should fix it!

Anyway it needs to be done.

So after thinking a while, the best place for this would be componentDidMount.

Yay another lifecycle method to the rescue!

import React, {Component} from 'react';

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: 'Purple Haze',
      count: 0,
    };

    this.onProductChange = this.onProductChange.bind(this);
    this.onOrder = this.onOrder.bind(this);
    this.onCountChange = this.onCountChange.bind(this);
  }

  componentDidMount() {
    document.title = `Selected - ${this.state.selected}`;
  }

  componentDidUpdate() {
    document.title = `Selected - ${this.state.selected}`;
  }

  onProductChange(e) {
    this.setState({selected: e.target.value});
  }

  onOrder() {
    alert(`You ordered ${this.state.count} ${this.state.selected}`);
  }

  onCountChange(e) {
    this.setState({count: e.target.value});
  }

  render() {
    return (
      <div>
        <div>
          <b>Product: </b>
          <select onChange={this.onProductChange}>
            <option value="Purple Haze">Purple Haze</option>
            <option value="Amnesia">Amnesia</option>
            <option value="GoGreen">GoGreen</option>
          </select>
        </div>
        <div>
          <b>Count: </b>
          <input
            type="number"
            min={0}
            value={this.state.count}
            onChange={this.onCountChange}
          />
        </div>
        <div>
          <button onClick={this.onOrder}>Order</button>
        </div>
      </div>
    );
  }
}

export default Menu;

Phew... it's been a tough day, but we managed! Hooray!

Turn to the next branch...

App grows! Performance issues come up!

As our app grows, we are encountering some performance issues here and there.

To identify them we start using some debugging tools.

So you decide to put some loggers in your Menu.js.

import React, {Component} from 'react';

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: 'Purple Haze',
      count: 0,
    };

    this.onProductChange = this.onProductChange.bind(this);
    this.onOrder = this.onOrder.bind(this);
    this.onCountChange = this.onCountChange.bind(this);
  }

  componentDidMount() {
    // eslint-disable-next-line
    console.log('logger', this.state, this.props);
    document.title = `Selected - ${this.state.selected}`;
  }

  componentDidUpdate() {
    // eslint-disable-next-line
    console.log('logger', this.state, this.props);
    document.title = `Selected - ${this.state.selected}`;
  }

  onProductChange(e) {
    this.setState({selected: e.target.value});
  }

  onOrder() {
    alert(`You ordered ${this.state.count} ${this.state.selected}`);
  }

  onCountChange(e) {
    this.setState({count: e.target.value});
  }

  render() {
    return (
      <div>
        <div>
          <b>Product: </b>
          <select onChange={this.onProductChange}>
            <option value="Purple Haze">Purple Haze</option>
            <option value="Amnesia">Amnesia</option>
            <option value="GoGreen">GoGreen</option>
          </select>
        </div>
        <div>
          <b>Count: </b>
          <input
            type="number"
            min={0}
            value={this.state.count}
            onChange={this.onCountChange}
          />
        </div>
        <div>
          <button onClick={this.onOrder}>Order</button>
        </div>
      </div>
    );
  }
}

export default Menu;

So we put loggers in componentDidUpdate and componentDidMount.

Now you see the proper log after componentDidMount.

You select a product and see the proper log in componentDidUpdate, with new state and props.

You change the product count and see new state and props logged by componentDidUpdate.

But wait a minute...

Doesn't that mean the document.title = newTitle code executes on every update,

even though the selected product didn't change?

We need to fix that. And this logger tool is really helpful we should implement it for other components too in our app.

So maybe we fix the issue with an if check. And make a HOC for the logger.

As you are thinking about the solution a new high priority requirement arrives.

Turn to the next branch...

New Requirement with lots async stuff

So suddenly a new requirement arrives.

To implement it you added lots of async stuff to your on order function, so now it looks like this.

import React, {Component} from 'react';

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: 'Purple Haze',
      count: 0,
    };

    this.onProductChange = this.onProductChange.bind(this);
    this.onOrder = this.onOrder.bind(this);
    this.onCountChange = this.onCountChange.bind(this);
  }

  componentDidMount() {
    // eslint-disable-next-line
    console.log('logger', this.state, this.props);
    document.title = `Selected - ${this.state.selected}`;
  }

  componentDidUpdate() {
    // eslint-disable-next-line
    console.log('logger', this.state, this.props);
    document.title = `Selected - ${this.state.selected}`;
  }

  onProductChange(e) {
    this.setState({selected: e.target.value});
  }

  onOrder() {
    setTimeout(() => {
      alert(`You ordered ${this.state.count} ${this.state.selected}`);
    }, 3000);
  }

  onCountChange(e) {
    this.setState({count: e.target.value});
  }

  render() {
    return (
      <div>
        <div>
          <b>Product: </b>
          <select onChange={this.onProductChange}>
            <option value="Purple Haze">Purple Haze</option>
            <option value="Amnesia">Amnesia</option>
            <option value="GoGreen">GoGreen</option>
          </select>
        </div>
        <div>
          <b>Count: </b>
          <input
            type="number"
            min={0}
            value={this.state.count}
            onChange={this.onCountChange}
          />
        </div>
        <div>
          <button onClick={this.onOrder}>Order</button>
        </div>
      </div>
    );
  }
}

export default Menu;

Everything seems to be working just fine, but then you get a bug report.

Wrong item is being ordered sometimes.

After trying to reproduce it for a while, you find the problem!

When you order a product then select another product before the order message appeared,

you get a wrong product in the message as it appears.

Hmmm...
Why that would happen? Nothing seems to be wrong with our code.

Turn to the next branch...

Ugh not this again!

So why that bug happens?

To give you a hint, lets take a look at the solution first.

import React, {Component} from 'react';

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: 'Purple Haze',
      count: 0,
    };

    this.onProductChange = this.onProductChange.bind(this);
    this.onOrder = this.onOrder.bind(this);
    this.onCountChange = this.onCountChange.bind(this);
  }

  componentDidMount() {
    // eslint-disable-next-line
    console.log('logger', this.state, this.props);
    document.title = `Selected - ${this.state.selected}`;
  }

  componentDidUpdate() {
    // eslint-disable-next-line
    console.log('logger', this.state, this.props);
    document.title = `Selected - ${this.state.selected}`;
  }

  onProductChange(e) {
    this.setState({selected: e.target.value});
  }

  onOrder() {
    const {count, selected} = this.state;
    setTimeout(() => {
      alert(`You ordered ${count} ${selected}`);
    }, 3000);
  }

  onCountChange(e) {
    this.setState({count: e.target.value});
  }

  render() {
    return (
      <div>
        <div>
          <b>Product: </b>
          <select onChange={this.onProductChange}>
            <option value="Purple Haze">Purple Haze</option>
            <option value="Amnesia">Amnesia</option>
            <option value="GoGreen">GoGreen</option>
          </select>
        </div>
        <div>
          <b>Count: </b>
          <input
            type="number"
            min={0}
            value={this.state.count}
            onChange={this.onCountChange}
          />
        </div>
        <div>
          <button onClick={this.onOrder}>Order</button>
        </div>
      </div>
    );
  }
}

export default Menu;

So we only changed this piece.

  onOrder() {
    const {count, selected} = this.state;
    setTimeout(() => {
      alert(`You ordered ${count} ${selected}`);
    }, 3000);
  }

Just one line of code fixes our issue.

Let's dive deep and understand what happens here.

The "this" is mutable

  onOrder() {
    setTimeout(() => {
      alert(`You ordered ${this.state.count} ${this.state.selected}`);
    }, 3000);
  }

Let's take a look at this buggy code first.

We select a product - "GoGreen". State changes to it. Component re-renders.

this.state.selected === "GoGreen"

Click the order button. The onOrder method fires.

setTimeout starts. 3 seconds pass. The callback is executed.

We read this.state.selected and get "GoGreen".

Here everything is working great. Now let's see how the bug happens.

We select a product - "Amnesia". State changes to it. Component re-renders.

this.state.selected === "Amnesia"

Click the order button. The onOrder method fires.

setTimeout starts. Before 3 seconds pass, we select another product - "GoGreen".

State changes to it. Component re-renders.

this.state.selected === "GoGreen"

3 seconds pass. setTimeout callback runs. We read this.state.selected and get "GoGreen".

However this time we clicked the order button when we selected the "Amnesia" product.

The problem here is the this. It changes during the scope of the onOrder.

Solution

Now let's take a look at the solution.

   onOrder() {
     const {count, selected} = this.state;
     setTimeout(() => {
       alert(`You ordered ${count} ${selected}`);
     }, 3000);
   }

We select a product - "Amnesia". State changes to it. Component re-renders.

this.state.selected === "Amnesia"

Click the order button. The onOrder method fires.
We read the this.state.selected and assign it to the new variable selected in the function scope.

selected === "Amnesia"
setTimeout starts. Before 3 seconds pass, we select another product - "GoGreen".

State changes to it. Component re-renders.

this.state.selected === "GoGreen"

3 seconds pass. setTimeout callback runs. We read the selected of the function scope not from the this. And get "Amnesia".

The this changed/mutated but function scope and variables in it were still the same.

So we solved the this problem by function scope.

2 Dimensions

One way that I find easy to think about the this and function scope, is to think about them like dimensions.

dimensions

We have these 2 dimensions where we store our data.

The this can change during the scope.

dimensions

So you need to be aware of the 2 dimensions where our data reside. And how they interact.

Fucking bring hooks already!

Fine! Fine...

So you heard about this next hot thing that's called hooks.

Now you want to refactor your Menu.js component to use hooks.

You were going to refactor it anyway because of performance issues in branch 3, so lets refactor straight to hooks.

Create a new simple functional component MenuFC.js.

import React from 'react';

const MenuFc = () => {
  return (
    <div>
      <div>
        <b>Product: </b>
        <select>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <button>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

So plain and beautiful. Simple function that returns some jsx.

Now what do you think, wouldn't it be nice if we could do something like this?

import React from 'react';

const MenuFc = () => {
  const state = 'Purple Haze';

  return (
    <div>
      <div>
        <b>Product: </b>
        <select value={state}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <button>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

Wow, our state to be a simple variable in function scope. That's crazy man!

But we need to somehow be able to change it, right? Otherwise it's not useful as state.

Imagine if we had setState as simple function in scope.

<select onChange={setState} value={state}>
  <option value="Purple Haze">Purple Haze</option>
  <option value="Amnesia">Amnesia</option>
  <option value="GoGreen">GoGreen</option>
</select>

What do you think? It's so nice and clean.

So how do we accomplish this with hooks?

import React, {useState} from 'react';

const MenuFc = () => {
  const [selected, setSelected] = useState('Purple Haze');
  const onProductChange = (e) => {
    setSelected(e.target.value);
  };

  return (
    <div>
      <div>
        <b>Product: </b>
        <select onChange={onProductChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <button>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

I know what you are thinking. "I liked you at first, but now, what kind of black magic is this?"

const [selected, setSelected] = useState('Purple Haze');

Please just give me a moment. It is a simple ES6 Array Destructuring.

You learnt the big and verbose class syntax, surely this tiny syntax won't hurt you.

Now let's look at benefits

Our selected and setSelected are just plain variables in our function scope.

useState function imported from react gets one argument - the initial state.
It returns an array.

The first element is the value of our state.

The second is a function to change the value.

We use that function to create a callback to execute when a user selects different product.

So take a look at this beauty once more.

import React, {useState} from 'react';

const MenuFc = () => {
  const [selected, setSelected] = useState('Purple Haze');
  const onProductChange = (e) => {
    setSelected(e.target.value);
  };

  return (
    <div>
      <div>
        <b>Product: </b>
        <select value={selected} onChange={onProductChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <button>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

"But you are creating a new callback function on each render, it's bad for performance!".

Some of you might say.

Well it turns out... No.

Here is what react official docs say about that.

No. In modern browsers, the raw performance of closures compared to classes doesn’t differ significantly except in extreme scenarios

Moreover

Hooks avoid a lot of the overhead that classes require, like the cost of creating class instances and binding event handlers in the constructor.

So if we just separate out politics from these sentences, the raw meaning strictly equals

As React components classes do so much shit that if we just throw them away and use functions,

we get a lot of performance benefit compared to which creating a new callback function at every render at most cases is nothing

And you can find some performance tips and tricks mentioned there. We'll dive into these later.

Now let's add the ordering functionality.

import React, {useState} from 'react';

const MenuFc = () => {
  const [selected, setSelected] = useState('Purple Haze');
  const onProductChange = (e) => {
    setSelected(e.target.value);
  };

  const onOrder = () => {
      setTimeout(() => {
        alert(`You ordered ${count} ${selected}`);
      }, 3000);
    };

  return (
    <div>
      <div>
        <b>Product: </b>
        <select value={selected} onChange={onProductChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <button onClick={onOrder}>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

Again. Simple as that, just a plain function.

Notice in this case we don't need to put the onOrder in the component scope.

We can declare it outside and use it inside the component by passing arguments.

const onOrder = (count, selected) => {
  setTimeout(() => {
    alert(`You ordered ${count} ${selected}`);
  }, 3000);
};

Here is another great thing about hooks. You can have as many of them as you'd like.

Let's implement the count.

import React, {useState} from 'react';

const MenuFc = () => {
  const [selected, setSelected] = useState('Purple Haze');
  const onProductChange = (e) => {
    setSelected(e.target.value);
  };

  const [count, setCount] = useState(0);
  const onCountChange = (e) => {
    setCount(e.target.value);
  };

  const onOrder = () => {
    setTimeout(() => {
      alert(`You ordered ${count} ${selected}`);
    }, 3000);
  };

  return (
    <div>
      <div>
        <b>Product: </b>
        <select onChange={onProductChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <b>Count: </b>
        <input
          type="number"
          min={0}
          value={count}
          onChange={onCountChange}
        />
      </div>
      <div>
        <button onClick={onOrder}>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

What a beauty! And we don't have any this problems.

No bindings! No async bugs! Now it's just a plain function and only a scope.

One Dimension Only

Now let's get acquainted with the next hook.
Turn to the next branch.

Side effects!

Where did we perform side effects in class components?

Lifecycle methods.

To perform side effects in functional components now we have useEffect.

In our Menu.js class component we have a side effect for changing our document title.

Here is how we do it in MenuFC.js

import React, {useState, useEffect} from 'react';

const MenuFc = () => {
  const [selected, setSelected] = useState('Purple Haze');
  const onProductChange = e => {
    setSelected(e.target.value);
  };

  const [count, setCount] = useState(0);
  const onCountChange = e => {
    setCount(e.target.value);
  };

  const onOrder = () => {
    setTimeout(() => {
      alert(`You ordered ${count} ${selected}`);
    }, 3000);
  };

  useEffect(() => {
    document.title = `Selected - ${selected}`;
  });

  return (
    <div>
      <div>
        <b>Product: </b>
        <select onChange={onProductChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <b>Count: </b>
        <input type="number" min={0} value={count} onChange={onCountChange} />
      </div>
      <div>
        <button onClick={onOrder}>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

useEffect accepts a function as an argument.

There we perform our side effects.

It runs on every render.
It behaves

as componentDidMount, componentDidUpdate, and componentWillUnmount combined.

https://reactjs.org/docs/hooks-effect.html

We also had a logger in our class component. Let's add it.

import React, {useState, useEffect} from 'react';

const MenuFc = () => {
  const [selected, setSelected] = useState('Purple Haze');
  const onProductChange = e => {
    setSelected(e.target.value);
  };

  const [count, setCount] = useState(0);
  const onCountChange = e => {
    setCount(e.target.value);
  };

  const onOrder = () => {
    setTimeout(() => {
      alert(`You ordered ${count} ${selected}`);
    }, 3000);
  };

  useEffect(() => {
    // eslint-disable-next-line
    console.log('logger', selected, count);
    document.title = `Selected - ${selected}`;
  });

  return (
    <div>
      <div>
        <b>Product: </b>
        <select onChange={onProductChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <b>Count: </b>
        <input type="number" min={0} value={count} onChange={onCountChange} />
      </div>
      <div>
        <button onClick={onOrder}>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

Everything looks great right? Before we go deeper one thing...

useEffect is a whole new concept

Do not think of useEffect as a new lifecycle method.

It's a whole new concept.

It's not a lifecycle method, it behaves similar to them.

useEffect timing is different.

https://reactjs.org/docs/hooks-reference.html#timing-of-effects

Old problems

In our class component we had a few issues.

As state changes our component re-renders.

componentDidUpdate fires and document.title = this.state.selected was running,

even though we changed only the count and not the title. With classes we'd put some if check.

Also we wanted to make our logger functionality reusable. With classes we'd make a HOC.

The same problems we have now here with our useEffect hook.

At the moment it's as bad as lifecycle methods.

Let's see how it's actually better.

Turn to the next branch.

It's useEffect not effects

Take a look at this code.

useEffect(() => {
    // eslint-disable-next-line
    console.log('logger', selected, count);
    document.title = `Selected - ${selected}`;
});

While it may seem okay, conceptually it is not.

In the callback function, the 2 lines of code have different concerns.

They do stuff unrelated to each other.

We need better separation of concerns.

So in fact this is much more better.

import React, {useState, useEffect} from 'react';

const MenuFc = () => {
  const [selected, setSelected] = useState('Purple Haze');
  const onProductChange = e => {
    setSelected(e.target.value);
  };

  const [count, setCount] = useState(0);
  const onCountChange = e => {
    setCount(e.target.value);
  };

  const onOrder = () => {
    setTimeout(() => {
      alert(`You ordered ${count} ${selected}`);
    }, 3000);
  };

  useEffect(() => {
    document.title = `Selected - ${selected}`;
  });

  useEffect(() => {
    // eslint-disable-next-line
    console.log('logger', selected, count);
  });

  return (
    <div>
      <div>
        <b>Product: </b>
        <select onChange={onProductChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <b>Count: </b>
        <input type="number" min={0} value={count} onChange={onCountChange} />
      </div>
      <div>
        <button onClick={onOrder}>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

Deps

Now we need our document.title effect run only when the selected state changed.

In usual componentDidUpdate you'd do some prevProps comparisons and so on.

Guess what? useEffect now will do it for you! You just need to tell it what it needs to check.

How do we tell it what variables to check? Just pass a second argument to it.

import React, {useState, useEffect} from 'react';

const MenuFc = () => {
  const [selected, setSelected] = useState('Purple Haze');
  const onProductChange = e => {
    setSelected(e.target.value);
  };

  const [count, setCount] = useState(0);
  const onCountChange = e => {
    setCount(e.target.value);
  };

  const onOrder = () => {
    setTimeout(() => {
      alert(`You ordered ${count} ${selected}`);
    }, 3000);
  };

  useEffect(() => {
    document.title = `Selected - ${selected}`;
  });

  useEffect(() => {
    // eslint-disable-next-line
    console.log('logger', selected, count);
  });

  return (
    <div>
      <div>
        <b>Product: </b>
        <select onChange={onProductChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <b>Count: </b>
        <input type="number" min={0} value={count} onChange={onCountChange} />
      </div>
      <div>
        <button onClick={onOrder}>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

Yes simple like that. useEffect gets second argument. It's an array.

In that array you put any variable which change should trigger the effect.

In case those variables don't change. The effect will not run.

Just add another console.log to that effect and you will see it now runs only when the selected changes.

Re-usable hooks

Let's dive right into coding.

We create a new hooker.js file.

import {useEffect} from 'react';

// updateDocumentTitle name is bad the custom hook name should start with "use"
export const useDocumentTittle = title => {
  useEffect(() => {
    document.title = title;
  }, [title]);
};

export const useLogger = (...args) => {
  useEffect(() => {
    // eslint-disable-next-line
    console.log('logger', ...args);
  });
};

Then we do this in our MenuFC.js

import React, {useState} from 'react';
import {useDocumentTittle, useLogger} from './hooker';

const MenuFc = () => {
  const [selected, setSelected] = useState('Purple Haze');
  const onProductChange = e => {
    setSelected(e.target.value);
  };

  const [count, setCount] = useState(0);
  const onCountChange = e => {
    setCount(e.target.value);
  };

  const onOrder = () => {
    setTimeout(() => {
      alert(`You ordered ${count} ${selected}`);
    }, 3000);
  };

  useDocumentTittle(`Selected - ${selected}`);
  useLogger(selected, count);

  return (
    <div>
      <div>
        <b>Product: </b>
        <select onChange={onProductChange}>
          <option value="Purple Haze">Purple Haze</option>
          <option value="Amnesia">Amnesia</option>
          <option value="GoGreen">GoGreen</option>
        </select>
      </div>
      <div>
        <b>Count: </b>
        <input type="number" min={0} value={count} onChange={onCountChange} />
      </div>
      <div>
        <button onClick={onOrder}>Order</button>
      </div>
    </div>
  );
};

export default MenuFc;

That's right!
Now we can use any of these hooks in any React Functional Component.

That's how simple sharing logic can be.

Some general rules for hooks

Remember that custom hooks should be named as native hooks. The use word should be used.

Hooks cannot be in conditions. But you can have conditions in your useEffect callback

And so on... You definitely should check eslint-plugin-react-hooks

To be continued...

And that's it for hooks intro!

Soon I'll dive deeper into more specific cases...

So keep up with me!
See you soon!

Oldest comments (0)