React Hooks 2 - A mindset change

I developed in React a lot.

And you can call me a stubborn class component developer. I used to be pretty happy about the status quo, well, the previous status quo technically, and I still use class components a lot.

I like declaring functions/methods and properties and accessing scope with this. I also struggle a bit with componentDidMount, componentWillUnmount, and componentWillReceiveProps occationally.

However, I realized recently that maybe I was just too busy implementing business features, staying in my comfort zone, and somehow ignoring the challenges and pain from class component.

I decided to finally have a deep dive for the new recommended functional component.

Here I am creating another post to explain React Hooks, with my own thinking on the road to change my mindset from class component to functional component.

Most of following comes as I reading Making-setinterval-declarative-with-react-hooks. I was really confused about the closure in his second example, as he said:

this is a common source of mistakes if you’re not very familiar with JavaScript closures. I reckoned I understand closure pretty well, while I did not find anything wrong in the closure.

I try to test with my example.

var savedCallback; 

callEffect = (func) => savedCallback = func();

var count = 1;

call(() => {
  let f = () => {
    console.log(count);
  };
  let id = setInterval(f, 1000);
  return f;
});

// Interval logs
// 1
// 1
// 1

savedCallBack(); // 1

count = count + 1;

// Interval logs
// 2
// 2
// 2
savedCallBack(); // 2

Everything changes fine for me. I spent quite some time for it and found out the evil devil at last. It's the functional component scope.

In class component, we always get state and props from this scope, which is the class scope. class scope doesn't change in the same life cycle, while for functional component, invoked every time for re-render like render() function, its scope keeps refreshing.

So here are some tips first:

  1. Functional component runs for every update, which means the variable inside can change at any time (variable got assigned everytime). There is no scope called this anymore. Hooks keeps dispatch, setState, and unset state are garanteed to be stable across re-render, but be really careful.

  2. Treat functional component as a single render() function.

  3. useState() returns new variables everytime so avoiding use them in a closure. (only unset state are stable/persistent)

  4. Be really careful with closure in useEffect() because it will not be called for every re-render, and the variablr binding/assigning inside would only happen/retrigger when closure being created

  5. If it's a function defined in the functional component, because it will be called for every re-render, the things will be always bound to latest variables(state).

  6. Treat useRef() as property instance live with the the component/node's lifecycle , as this.ClassProperty in a class component, use it as a persistent property (an escape hatch) across every re-render (when necessary)!

E.G.

function MessageThread() {
  const [message, setMessage] = useState('');

  // Keep track of the latest value.
  // -------------------------------
  const latestMessage = useRef('');
  useEffect(() => {
    latestMessage.current = message;
  });
  // -------------------------------

  const showMessage = () => {
    alert('You said: ' + latestMessage.current);
  };

  return <div>Sth</div>;
}

To make the 1st tip more clear, here is the code.

import React, { useState, useEffect, useReducer } from "react";
import ReactDOM from "react-dom";

var countSaved;
var setCountSaved;
var dispatchSaved;

function Counter() {
  const [count, setCount] = useState({ a: 0 });
  const [flag, setFlag] = useState(false);
  const [s, dispatch] = useReducer(() => {}, {});

  useEffect(() => {
    let f = () => {
      // the closure created only once (componentDidMount),
      // thus, `count` is bind to THE INITIAL `count` generated by useState() from FIRST render

      countSaved = count;
    };
    let id = setInterval(f, 1000);
    return () => clearInterval(id);
  }, []);

  useEffect(() => {
    setCountSaved = setCount;
    dispatchSaved = dispatch;
    console.log('Save initial setCount and dispatch.');
  }, []);

  console.log("count === countSaved: " + (count === countSaved));
  // YES, if did not call setCount

  if (count === countSaved) {
    console.log("Update without call setCount(), count state persists");
  }

  console.log("setCount === setCounSaved: " + (setCount === setCountSaved)); // YES, it's ALWAYS persistent

  console.log("dispatch === dispatchSaved: " + (dispatch === dispatchSaved)); // YES, it's ALWAYS persistent

  return (
    <div>
      <h1>{count.a}</h1>
      <button onClick={() => setFlag(!flag)}>Flip flag</button>
      <button onClick={() => setCount({ a: count.a + 1 })}>
        Increment count
      </button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

// Logs for each interval
// {a: 1}
// {a: 0}
// Is count returned as ths same variable: false

This principle I found in Dan's blog is pretty helpful:

The best mental rule I’ve found so far with hooks is ”code as if any value can change at any time”. -- Fredrik Höglund

Last updated