Part 1: The Learn React Hooks reading notes

493 阅读5分钟

Introducing React and React Hooks

The fundamental principles of React

Declarative

Instead of telling React how to do things, we tell it what we want it to do.

imperative code

cont input = ['a','b','c'];
let result = [];
for(let i = 0;i < input.length;i ++ ){
    result.push(input[i] + input[i]);
}
console.log(result);// prints:['aa','bb','cc']

We need to tell the computer exactly what to do,step by step.

declarative code

cont input = ['a','b','c'];
let result = input.map(str => str + str);
console.log(result);// prints:['aa','bb','cc']

We simply tell the computer what we want.

Component-based

React encapsulates components that manage their own state and views, and then allows us to compose them in order to create complex user interfaces.

Function components

JavaScript functions that take the props as an argument, and return the user interface.

Class components

JavaScript classes that provide a render method, which returns the user interface.

Motivating the need for React Hooks

Confusing classes

this context

this is a special keyword in JavaScript that always refers to the object that it belongs to;

In a method, this refers to the class object

In an event handler, this refers to the element that received the event.

In a function or when standing alone, this refers to the global object. For example, in a browser, the global object is the Window object.

Sometimes we need to manually rebind it to the class object.

write code in multiple places

If we want to fetch data when the component renders, or the data updates, we need to do this using two methods: once in componentDidMount, and once in componentDidUpdate.

Wrapper hell

As you can imagine, using many contexts will result in a large tree with many sub-trees, also called wrapper hell. For example, when we want to use three contexts, the wrapper hell looks as follows:

<AuthenticationContext.Consumer>
    {
    	user=>{
    		<LanguageContext.Consumer>
    			{
                    language =>(
                        <StatusContext.Comsumer>
                            {
                                status =>(……)
                            }
    					</StatusContext.Comsumer>
                    )
                }
    		</LanguageContext.Consumer>
		}
	}
</AuthenticationContext.Consumer> 

Hooks to the rescue

The aforementioned problems with life cycle methods could be solved using Hooks,as follows:

function Example({name}){
	useEffect(() =>{
		// ....
	},[name])
}
const user = useContext(AuthenticationContext);
const language = useContext(LanguageContext);
const status = useContext(StatusContext);

Getting started with React Hooks

Initializing a project with create-react-app

npm i create-react-app

npx create-react-app chapter1_1

npm start

src/App.js

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

function App() {
  const [name, setName] = useState('');

  const handleInputChange = (e) => {
    setName(e.target.value)
  }

  return (
    <div className="App">
      <div>
        <h1>My name is:{name} </h1>
        <input onChange={handleInputChange}></input>
      </div>
    </div>
  );
}

export default App;

We can use destructuring to store these tow elements in separate variables, as follows:

const [name,setName] = useState('');

The previous code is equivalent to the following:

const nameHook = useState('');
const name = nameHook[0];
const setName = nameHook[1];

Rules of Hooks

  • Hooks can only be used in function components, not in class components
  • The order of Hook definitions matter, and needs to stay the same; thus, we cannot put Hooks in if condtiionals, loops, or nested functions

Giving an overview of various Hooks

  • useState
  • useEffect
  • useContext
  • useRef
  • useReducer
  • useMemo
  • useCallback
  • useLayoutEffect
  • useDebugValue

useState

It returns a stateful value (state) and a setter function (setState) in order to update the value.

We can use it as follows:

import { useState } from 'react'

const [state,setState] = useState(initialState);

useEffect

The useEffect Hook replaces the componentDidMount, componentDidUpdate, and componentWillUnmount methods.

import { useEffect } from 'react';

useEffect(didUpdate);

useContext

The useContext Hook replaces context consumers.

import { useContext } from 'react'

const value = useContext(MyContext)

useRef

The useRef Hook is used to deal with references to elements and components in React.

import { useRef } from 'react'

const refContainer = useRef(initialValue);

<ComponentName ref = {refContainer} />

useReducer

The useReducer Hook is used to deal with complex state logic.

import { useReducer } from 'react';

const [ state, dispatch ] = useReducer(reducer,initialArg,init);

useMemo

Memoization is an optimization technique where the result of a function call is cached, and is then returned when the same input occurs again. The useMemo Hook allows us to compute a value and memorize it.

import { useMemo } from 'react'
const memoizeValue = useMemo(()=>computeExpensivevalue(a,b),[a,b])

The useMemo Hook is useful for optimization when we want to avoid re-executing expensive operations.

useCallback

This Hook allows us to pass an in-line callback function, and an array of dependencies, and will return a memorized version of the callback function. We can use it as follows:

import { useCallback } from 'react'

const memoizedCallback = useCallback(()=>{
    doSomething(a,b)
},[a,b])

The useCallback Hook is useful when passing callbacks to optimized child components. It works similarly to the useMemo Hook, but for callback functions.

useLayoutEffect

This Hooks is identical to useEffect, but it only fires after all DOM mutations.

we can use it as follow:

import { useLayoutEffect } from 'react'

useLayoutEffect(didUpdate)

useDebugValue

This Hook can be used to display a label in React DevTools when creating custom Hooks. We can use it as follows:

import { useDebugValue } from 'react'

useDebugValue(value);

Using the State Hook

Reimplementing the useState function

We are now going to start reimplementing the State Hook:

Please note that this reimplementation is not exactly how React Hooks work internally. The actual implementation is similar, and thus, it has similar constraints. However, the real implementation is much more complicated than what we will be implementing here.

import React from 'react';
import ReactDOM from 'react-dom';
import './App.css';

let value;

function useState(initialState) {
  if (typeof value === 'undefined') value = initialState;
  function setState(nextValue) {
    value = nextValue;
    ReactDOM.render(<App />, document.getElementById('root'))
  }
  return [value, setState];
}

function App() {

  const [name, setName] = useState('initialName');
  const handleInputChange = (e) => {
    setName(e.target.value)
  }

  return (
    <div className="App">
      <div>
        <h1>My name is:{name} </h1>
        <input onChange={handleInputChange}></input>
      </div>
    </div>
  );
}

export default App;

Just like this, our Hook function works! However, if we wanted to add another Hook, we would run into another problems: all the Hooks write to the same global value variable!

Let's take a closer look at the problem by adding a second Hook to our component.

Implementing multiple Hooks

In order to implement multiple Hooks, instead of having a single global variable, we should have an array of Hooks values.

import React from 'react';
import ReactDOM from 'react-dom';
import './App.css';

let values = [];
let currentHook = 0;

function useState(initialState) {
  if (typeof values[currentHook] === 'undefined') values[currentHook] = initialState;
  let hookIndex = currentHook;
  function setState(nextValue) {
    values[hookIndex] = nextValue;
    ReactDOM.render(<App />, document.getElementById('root'))
  }
  return [values[currentHook++], setState];
}

function App() {

  currentHook = 0;

  const [name, setName] = useState('');
  const [age, setAge] = useState();

  console.log('values', values);

  return (
    <div className="App">
      <div>
        <h1>My name is:{name} </h1>
        <input onChange={(e) => setName(e.target.value)}></input>
        <br />
        <h1>My age is:{age} </h1>
        <input onChange={(e) => setAge(e.target.value)}></input>
      </div>
    </div>
  );
}

export default App;

As we can see, using a global array to store our Hook values solved the problems that we had when defining multiple Hooks.

Can we define conditional Hooks?

What if we wanted to add a checkbox that toggles the use of the firs name field?

It's show error in the page.

Inserting a new Hook in-between two existing Hooks makes the name Hooks steal the state from the next Hook because it now has the same index that the age Hook previously had.

Comparing our reimplementation with real Hooks

Our simple Hook implementation already gives us an idea about how Hooks work internally. However, in reality, Hooks do not use global variables. Instead, they store state within the React component. They also deal with the Hook counter internally, so we do not need to manually reset the count in our function component. Furthermore, real Hooks automatically trigger rerenders of our component when the state changes.

By reimplementing the useState Hook, we have learned a couple things:

Hooks are simply functions that access React features.

Hooks deal with side effects that persist across rerenders.

The order of Hook definitions matters.

The last point is especially important because it means that we cannot conditionally define Hooks. We should always have all the Hook definitions at the beginming of our function components, and never nest them within if or other construct.

Solving conditional Hooks

So, how do we implement conditional Hooks? Instead of making the Hook conditional, we can always define the Hook and use it whenever we need it. If this is not an option, we need to split up our components, which is usually better anyway!

We cannot do the following, as using an if conditional could change the order of the Hooks:

function UserInfo({username}){
	if(username){
        const info = useFetchUserInfo(username)
        return <div>{info}</div>
    }
    return <div> Not logged in </div>
}

Instead, we have to create a separate component for when the user is logged in, as follows:

function LoggedInUserInfo({username}){
	const info = useFetchUserInfo(username);
    return <div>{info}</div>
}

function UserInfo({username}){
	if(username){
        
        return <LoggedInUserInfo username={username} />
    }
    return <div> Not logged in </div>
}