Part 2: The Learn React Hooks reading notes

231 阅读13分钟

Understanding Hooks in Depth

Using the Reducer and Effect Hooks

Reducer Hooks versus State Hooks

We used State Hooks for both local and global states cases, which is fine for simple state changes. However, when our state logic becomes more complicated, we are going to need to ensure that we keep the state consistent. In order to do so, we should use a Reducer Hook instead of multiple State Hooks, because it is harder to maintain synchronicity between multiple State Hooks that depend on each other.

Problems with the State Hook

We are always going to have change the state directly, which means that we need to use a lot of spread syntax, in order to make sure that we are not overwriting other parts of the state. For example, imagine that we have a state object like this:

const [config,setConfig] = useState({filter:'all',expandPosts:true})

Now, we want to change the filter:

setConfig({filter:{byAuthor:'Daniel Bug',from Date:'2019-04-29'}})

If we simply ran the preceding code, we would be removing the expandPosts part of our state! So, we need to do the following:

setConfig({...config,filter:{byAuthor:'Daniel Bug',from Date:'2019-04-29'}})

Now, if we wanted to change the fromDate filter to a different date, we would need to use spread syntax twice, to avoid removing the byAuthor filter:

setConfig({...config,filter:{...config.filter,from Date:'2019-04-30'}})

As you can imagine, using spread syntax and changing the state object directly can become very tedious for larger state objects. Furthermore, we always need to make sure that we do not introduce any bugs, and we need to check for bugs in multiple places all across our app.

Actions

Instead of changing the state directly, we could make a function that deals with state changes, such a function would only allow state changes via certain actions, such as a CHANGE_FILER or TOGGLE_EXPAND action.

{type:'CHANGE_FILTER',all:true'} // {expandPosts:true,filter:'all'}
 
{type:'CHANGE_FILTER',fromDate:'2019-04-29'} // {expandPosts:true,filter:{fromDate:'2019-04-29'}}

Reducers

Now, we still need to define the function that handles these state changes. Such a function is known as a reducer function. It takes the current state and action as arguments, and returns a new state.

function reducer(state,action){
    switch(action.type){
        case 'TOGGLE_EXPAND':
            return {...state,expandPosts:!state.expandPosts}
        case 'CHANGE_FILTER':
            if(action.all){
                return {...state,filter:'all'}
            }
        // ....
    }
}

The Reducer Hook

Now that we have defined actions and the reducer function, we can create a Reducer Hook from the reducer. The signature for the useReducer Hook is as follows:

const initialState = { all:true };
const [ state,dispatch ] = useReducer(reducer,initialState);

dispatch({type:'TOGGLE_EXPAND'});
dispatch({type:'CHANGE_FILTER',fromDate:'2019-04-30'});

Replacing the user State Hook

Defining actions

Let's define the actions now:

{ type:'LOGIN',username:'Daniel Bugl',password:'notsosecure'}
{ type:'REGISTER',username:'Daniel Bugl',password:'notsosecure',passwordRepeat:'notsosecure'}
{type:'LOGOUT'}

Defining the reducer

function useReducer(state,action){
	switch(action.type){
        case 'LOGIN':
        case 'REGISTER':
            return action.username
        case 'LOGOUT':
            return ''
    }
}

Defining the Reducer Hook

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


export const handleReducer = (state, action) => {
  switch (action.type) {
    case 'LOGIN':
    case 'REGISTER':
      return action.username
    case 'LOGOUT':
      return ''
    default:
      throw new Error();
  }
}

function App() {

  const [user, diapatchUser] = useReducer(handleReducer, '');

  return (
    <div className="App">
      <div>
        username:{user}
      </div>
      <button onClick={() => diapatchUser({ type: 'REGISTER', username: 'admin' })}>注册</button>
      <button onClick={() => diapatchUser({ type: 'LOGIN', username: 'admin' })}>登录</button>
      <button onClick={() => diapatchUser({ type: 'LOGOUT' })}>退出</button>
    </div>
  );
}

export default App;

Merging Reducer Hooks

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


export const handleUserReducer = (state, action) => {
  switch (action.type) {
    case 'LOGIN1':
    case 'REGISTER1':
      return action.name
    case 'LOGOUT1':
      return ''
    default:
      return state;
  }
}

export const handleAvatarReducer = (state, action) => {
  switch (action.type) {
    case 'LOGIN':
    case 'REGISTER':
      return action.avatar
    case 'LOGOUT':
      return ''
    default:
      return state;
  }
}

export const appReducer = (state, action) => {
  return { name: handleUserReducer(state.name, action), avatar: handleAvatarReducer(state.avatar, action) };
}

function App() {

  const [state, diapatch] = useReducer(appReducer, { name: '', avatar: '' });
  const { name, avatar } = state;

  const handleRegister = () => {
    diapatch({ type: 'REGISTER1', name: 'admin' })
    diapatch({ type: 'REGISTER', avatar: 'login-image' })
  }

  const handleLogin = () => {
    diapatch({ type: 'LOGIN1', name: 'admin' })
    diapatch({ type: 'REGISTER', avatar: 'login-image' })
  }

  const handleLogout = () => {
    diapatch({ type: 'LOGOUT1' });
    diapatch({ type: 'REGISTER', avatar: '' })
  }

  return (
    <div className="App">
      <div>
        name:{name}
      </div>
      <div>
        user-image:{avatar}
      </div>
      <button onClick={handleRegister}>注册</button>
      <button onClick={handleLogin}>登录</button>
      <button onClick={handleLogout}>退出</button>
    </div>
  );
}

export default App;

Using Effect Hooks

Using the Effect Hook, we can perform side effects from our components, such as fetching data when the component mounts or updates.

componentDidiMount and componentDidupdate

we can set the document title to a given prop as follows, using a React class component.

import React from 'react'

class App extends React.Component{
    componentDidMount(){
        const { title } = this.props;
        document.title = title;
    }
    
    componentDidiUpdate(preProps){
        const { title } = this.props;
        if(title !== prevProps.title){
            document.title = title;
        }
    }
    
    render(){
        return <div>Test App</div>
    }
}

you might have noticed that we are writing almost the same code twice., and we need to add an if condition to componentDidUpdate, in order to avoid changing title when the title prop did not change.

Using an Effect Hook

In the world of Hooks, the componentDidMount and componentDidUpdate life cucle methods are combined in the useEffect Hook, which-when not specifying a dependency array - trggers whenever any props in the component change.

import React,{ useEffect } from 'react'

function App({title}){
    useEffect(()=>{
        document.title = title
    })
    return (<div>Test App</div>)
}

If we want to make sure that our effect function only gets called when the title prop changes, for example, for performance reasons, we can specify which values should trigger the changes, as a second argument to the useEffect Hook:

import React,{ useEffect } from 'react'

function App({title}){
    useEffect(()=>{
        document.title = title
    },[title])
    return (<div>Test App</div>)
}

And this is not just restricted to props, we can use any value here, even values from other Hooks, such as a State Hook or a Reducer Hook.

Trigger effect only on mount

If we want to replicate the behavior of only adding a componentDidMount life cycle method, without triggering when the props change, we can do this by passing an empty array as the second argument to the useEffect Hooks:

userEffect(()=>{
	document.title = title;
},[])

Cleaning up effects

Sometimes effects need to be cleaned up when the component unmounts. To do so, we can return a function from the effect function of the Effect Hook. The returned function works similarly to the componentWillUnmount life cycle method:

useEffect(()=>{
    const updateInterval = setInterval(()=>console.log('fetching update'),updateTime)
    return () => clearInterval(updateInterval)
},[updateTime])

The cleanup function will be called when the component unmounts and before running the effect again.

Implementing React Context

We are going to begin by learning what React context is, and what providers and consumers are. Then, we are going to use Context Hooks as a context context consumer, and discuss when context should be used. Finally, we are going to implement themes and global state via contexts.

Introducing React context

React context providers a solution to this cumbersome way of passing down props over multiple levels of components, by allowing us to share values between components, without having to explicitly pass them down via props.

React context is used to share values across a tree of React components. Usually, we want to share global values, such as the user state and the dispatch function, the theme of our app, or the chosen language.

React context consists of two parts:

Theprovider, which providers(sets) the value

Theconsumer, which consumers (users) the value

Defining the context

We simply use the React.createContext(defaultValue) function to create a new context object.

export const ThemeContext = React.createContext({primaryColor:'deepskyblue'})

That is all we need to do to define a context with React. Now we just need to define the consumer.

Defining the consumer

create a new src/Header.js file

import React from 'react';
import { ThemeContext } from './App';

const Header = ({ text }) => (
    <ThemeContext.Consumer>
        {theme => <h1 style={{ color: theme.primaryColor }}>{text}</h1>}
    </ThemeContext.Consumer>
)

export default Header

scr/App.js

import React from 'react';
import Header from './Header';
import './App.css';

function App() {
  return (
    <Header text="Hello World" />
  );
}

export default App;

export const ThemeContext = React.createContext({ primaryColor: 'deepskyblue' })

But, using components with render function props in this way clutters our UI tree, and makes our app harder to debug and maintain.

Using Hooks

A better way to use contexts is with the useContext Hook!

import React, { useContext } from 'react';
import { ThemeContext } from './App';

const Header = ({ text }) => {
    const theme = useContext(ThemeContext);
    return <h1 style={{ color: theme.primaryColor }}>{text}</h1>
}

export default Header

As we can see, using Hooks makes our context consumer code much more concise.

Defining the provider

Contexts use the default value that is passed to React.createContext, when there is no provider defined. This is useful from debugging the components when they are not embedded in the app.

But,in an app, we usually want to use a provider to provide the value for the context.

const App = () =>{
	<ThemeContext.Provider value={{primaryColor:'coral'}}>
    	<Header text="hellow world" />    
    </ThemeContext.Provider>
}

Preventing unnecessary re-rendering with React.memo

With class components we had shouldComponentUpdate, which would prevent components from re-rendering if the props did not change.

With function components, we can do the same using React.memo, which is a higher-order component. React.memo memorizes the result, which means that it will remember the last rendered result, and, in case where the props did not change, it will skip re-rendering the component:

const SomeComponent = () =>...

export default React.memo(SomeComponent)

It will only shallowly compare the props object. If we want to do a special comparison, we can pass a function as a second argument to React.memo:

export default React.memo(SomeComponent,(prevProps,nextProps)=>{
	// compare props and return true if the props are equal and we should not update
})

Unlike shouldComponentUpdate, the function that is passed to React.memo returns true when the props are equal, and thus it should not update, which is the opposite of how shouldComponentUpdate works!

Implementing lazy loading with React Suspense

React Suspense allows us to let components wait before rendering. At the moment, React Suspense only allows us to dynamically load components with React.lazy. In the future, Suspense will support other use cases, such as data fetching.

React.lazy is another form of performance optimization. It lets us load a component dynamically in order to reduce the bundle size.

Implementing React.Suspense

First, we have to specify a loading indicator, which will be shown when our lazily-loaded component is loading.

Edit src/App.js, and replace the component with the following code:

<React.Suspense fallback={'Loading……'}>
	<UserBar />    
</React.Suspense>

Implementing React.lazy

Next, we are going to implement lazy loading for the Logout component by wrapping it with React.lazy(), as follows:

Edit src/user/UserBar.js, and remove the import statement for the Logout component:

import Logout from './Logout'

Then, define the Logout component via lazy loading:

const Logout = React.lazy(() => import('./Logout'))

The import () function dynamically loads the Logout component from the Logout.js file. In contrast to the static import statement, this function only gets called when React.lazy triggers it, which means it will only be imported when the component is needed.

Using Community Hooks

First, we have to install the react-hookedup library in our blog app project:

npm install --save react-hookedup

Exploring the input handling Hook

A very common use case when dealing with Hooks, is to store the current value of an input field using State and Effect Hooks. We have already done this many times throughout this book.

import React from 'react'
import { useInput } from 'react-hookedup'

export default function App(){
    const { value, onChange } = useInput('')
    return <input value={value} onChange={onChang} />
}

This code will bind an onChange handler function and value to the input field. This means the whenever we enter text into the input field, the value will automatically be updated.

React life cycles with Hooks

The useOnMount Hook

The useOnMount Hook has a similar effect to the componentDidMount life cycle. It is used as follows:

import React from 'react'
import { useOnMount } from 'react-hookedup'

export default function UseOnMount(){
    useOnMount(()=>console.log('mounted'))
    
    return <div>look at the console :)</div>
}

Alternatively, we could just use a useEffect Hook with an empty array as the second argument, which will have the same effect:

import React,{ useEffect } from 'react'

export default function onMountWithEffect(){
    useEffect(()=>console.log('mounted with effect'),[]);
    
    return <div>look at the console:)</div>
}

The useOnUnmount Hook

The useOnUnmount Hook has a similar effect to the componentWillUnmount life cycle. It is used as follows:

import React from 'react'
import { useOnUnmoount } from 'react-hookedup'

export default function UseOnUnmount(){
    useOnUnmount(()=>console.log('unmounting'))
    
    return <div>click the 'numount' button above and look at the console</div>
}

Using the Reducer and Effect Hooks, we can return a cleanup function from the useEffect Hook, which will be called when the component unmounts. This means that we could alternatively implement the useOnMount Hook using useEffect, as follows:

import React, { useEffect } from 'react'

export default function OnUnmountWithEffect(){
    useEffect(()=>{
        return () => console.log('unmounting with effect')
    },[])
    
    return <div>click the 'unmount'  button aove and look at the console</div>
}

The useMergeState Hook

The useMergeState Hook works similarly to the useState Hook. However, it does not replace the current state, but instead merges the current state with the new state, just like this.setState() works in React class components.

import React from 'react'
import { useMergeState } from 'react=hookedup'

export default function UseMergeState(){
    const [ state, setState ] = useMergeState({ loaded:true,counter:0 })
}

The usePrevious Hook

The usePrevious Hooks is a simple Hook that lets us get the previous value of a prop or Hook value. It will always store and return the previous value of any given variable, and it works as follows:

import React, { useState } from 'react'
import { usePrevious } from 'react-hookedup'

export default function UsePrevious(){
    const [ count,setCount ] = useState(0);
    const prevCount = usePrevious(count);
    
    function handleClick(){
        setCount(count+1)
    }
    
    return (<div>Count was {preCount} and is { count} now,
        <button onClick={handleClick}+1></button>
        </div>)
}

Timer Hooks

The react-hookedup library also provides Hooks for dealing with timers. If we simply create a timer using setTimeout or setInterval in our component, it will get instantiated again every time the component is re-renderd. This not only causes bugs and unpredictability, but can also cause a memory leak if the old times are not freed properly. Using timer Hooks, we can avoid these problems completely, and easily use intervals and timeouts.

The useInterval Hook

import React,{ useState } from 'react'
import { useInterval } from 'react-hookedup'

export default function UseInterval(){
    const [ count, setCount ] = useState(0);
    
    useInterval(()=>setCount(count+1),1000);
    
    return <div>{count} seconds passed </div>
}

Alternatively, we could use an Effect Hook in combination with setInterval, instead of the useInterval Hook, as follows:

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

export default function IntervalWithEffect(){
    const [ count,setCount ] = useState(0);
    useEffect(()=>{
        const interval = setInterval(()=>setCount(count+1),1000);
        return () => clearInterval(interval)
    })
    
    return <div>{count} seconds passed </div>
}

As we can see, the useInterval Hook makes our code more concise and easily readable.

useTimeout Hook

import React,{ useState } from 'react'
import { useTimeout } from 'react-hookedup'

export default function UseTimeout(){
    const [ready,setReady] = useState(false);
    
    useTimeout(()=>setReady(true),10000)
    
    return <div>{ready?'ready':'waiting……'}</div>
}

The Online Status Hook

In some web apps, it makes sense to implement an off-line mode; for example, if we want to be able to edit and save drafts fro post locally, and sync them to the server whenever we are online again. To be able to implements this use case , we can use the useOnlineStatus Hook.

The Online Status Hook returns an object with an online value, which contains true if the client is online; otherwise, it contains false. It works as follows:

import React from 'react'
import { useOnlineStatus } from 'react-hookedup'

export default function App(){
    const { online } = useOnlineStatus()
    
    return <div>You are { online?'online':'offline'}</div>
}

We could then use a Previous Hook, in combination with an Effect Hook, in order to sync data to the server when we are online again. To be able to implement this use case, we can use the useOnlineStatus Hook.

The Online Status Hook returns an object with an onlline value, which contains true if the client is online; otherwise, it contains false. It works as follow:

import React from 'react'
import { useOnlineStatus } from 'react-hookedup'

export default function App(){
    const { online } = useOnlineStatus();
    
    return <div> You are { online? 'online':'offline'}
}

We could then use a Previous Hook, in combination with an Effect Hook, in order to sync data to the server when we are online again:

import React,{ useEffect } from 'react'
import { useOnlineStatus, usePrevious } from 'react-hookedup'

export default function App(){
    const { online } = useOnlineStatus();
    const prevOnline = usePrevious(online);
    
    useEffect(() => {
        if(prevOnline === false && online === true){
            alert('syncing data');
        }
    },[prevOnline, online])
    
    return <div>You are { online? 'online':'offline'} </div>
}

Now, we have an Effect Hook that triggers whenever the value of online changes. It then checks whether the previous value of online was false, and the current one is true. If that is the case, it means we were offline, and are now online again, so we need to sync our updated data to the server.

The useBoolean Hook

The Hook returns an object with the following:

value: The current value of the boolean

toggle: A function to toggle the current value( sets true if currently false, and false if currently true)

setTrue: Sets the current value to true

setFalse: Sets the current value to false

import React from 'react'
import { useBoolean } from 'react-hookedup'

export default function UseBoolean(){
    const { toggle, value } = useBoolean(false)
    
    return (
    	<div>
        	<button onClick={ toggle}>{ value?'on':'off' }</button>
        </div>
    )
}

The useArray Hook

The useArray Hook is used to easily deal with arrays, without having to use the res/spread syntax.

The Array Hook returns an object with the following:

value: The current array

setValue: Sets a new array as the value

add:Adds a given element to the array

clear: Removes all elements from the array

removeIndex: Removes an element from the array by its index

removeById: Removes an element from the array by its id(assuming that the elements in the array are objects with an id key)

It works as follows:

import React from 'react'
import { useArray } from 'react-hookedup'

export default function UseArray(){
    const { value, add, clear, removeIndex } = useArray(['one','two','three'])
    
    return (
    	<div>
        	<p>current array:{JSON.stringfy(value)}</p>
			<button onClick={() =>add('test')}>add element</button>
			<button onClick={() =>removeIndex(0)}>remove first element</button>
			<button onClick={() =>clear()}>clear elements</button>
        </div>
    )
}

The useCounter Hook

The useCounter Hook can be used to define various kinds of counters. We can define a lower/upper limit, specify whether the counter should loop or not, and specify the step amount by which we increase/derease the counter. Futhermore, the Counter Hook provides functions in order to increase/decrease the counter.

It returns the following object:

value: The current value of our counter

setValue: Sets the current value of our counter

increase: Increase the value by a given step amount. If no amount is specified, then the default step amount is used.

decrease: Decreases the value by a given step amount. If no a amount is specified, then the default step amount is used.

import React from 'react'
import { useCounter } from 'react-hookedup'

export default function UseCounter(){
	const { value,increase,decrease } = useCounter(0,{upperLimit:3,lowerLimit:0,loop:true})
    
    return (
    	<div>
        	<b>{value}</b>
        	<button onClick={increase}>+</button>
			<button onClick={decrease}>-</button>
        </div>
    )
}

The useFocus Hook

In order to know whether an element is currently focused, we can use the useFocus Hook as follows:

First, we import the useFocus Hook:

import React from 'react'
import { useFocus } from 'react-hookedup'

export default function UseFocus(){
    const { focused, bind } = useFocus();
    
    return (
    	<div>
        	<input {...bind} value={focused?'focused':'not focused'} />
        </div>
    )
}

The useHover Hook

In order to know whether the user is currently hovering over an element, we can use the useHover Hook, as follows:

import React from 'react'
import { useHover } from 'react-hookedup'

export default function UseHover(){
    const { hovered,bind } = useHover();
    return (
    <div {...bind}>Hover me {hovered && 'THANKS!!'}</div>
    )
}

Rules of Hooks

Calling Hooks

Hooks should only be called in React function components or custom Hooks. They cannot be used in class components or regular JavaScript functions.

Order of Hooks

Only call Hook sat the top level/beginning of function components or custom Hooks.

Do not call Hooks inside conditions, loops, or nested functions - doing so changes the order of Hooks, which causes bugs.

Names of Hooks

There is a convention that Hook functions should always be prefixed with use, followed by the Hook name starting with a capital letter; for example: useState, useEffect, and useResource. This is important, because otherwise we would not know which JavaScript functions are Hooks, and which are not. Especially when enforcing the rules of Hooks, we need to know which functions are Hooks so that we can make sure they are not being called conditionally or in loop.

Building Your Own Hooks

Creating a useTheme Hook

As you might have noticed, we often do the following:

import { ThemeContext } from '../contexts'

export default function SomeComponent(){
    const theme = useContext(ThemeContext);
    // ...
}

we could abstract this functionality into a useTheme Hook, which will get the theme object from the ThemeContext.

Create a new src/hooks/useTheme.js file.

In the newly created file, we first import the useContext Hook and the ThemeContext as follows:

import { useContext } from 'react'
import { ThemeContext } from '../contexts'

export default function useTheme(){
    return useContext(ThemeContext)
}

Remember, Hooks are just functions prefixed with the use keyword.

Using the useTheme Hook

import { useTheme } from './hooks'

const { primaryCollor } = useTheme()

Creating a local Register Effect Hook

function useRegisterEffect(user,dispatch){
	useEffect(() => {
        if(user && user.data){
            dispatch({type:'REGISTER',username:user.data.username})
        }
    },[dispatch,user])
}
useRegisterEffect(user,dispatch)

As we can see, extracting an effect into a separate function makes our code easier to read and maintain.

Creating the useCounter Hook

The useCounter Hook is going to provide a current count and functions to increment and reset the counter.

import { useState, useCallback } from 'react'

export default function useCounter(initialCount = 0){
    const [ count, setCount ] = useState(initialCount);
    const increment = useCallback(()=>setCount(count+1),[])
    const reset = useCallback(()=>setCount(initialCount,[initialCount]))
    return { count,increment,reset }
}

Exploring the React Hooks API

The useRef Hook

The useRef Hook returns a ref object that can be assigned to a component or element via the ref prop. Refs can be used to deal with references to elements and components in React.

function AutoFocusField(){
	const inputRef = useRef(null)
    useEffect(() => inputRef.current.focus(),[]);
    return <input ref = {inputRef} type="text" />
}

It is important to note that mutating the current value of a ref does not cause a re-render. If this is needed, we should use a ref callback using useCallback as follows:

function WidthMeasure(){
    const [ width,setWidth ] = useState(0)
    
    const measureRef = useCallback(node =>{
        if(node !== null){
            setWidth(node.getBoundingClientRect().width)
        }
    },[])
    
    return <div ref={measureRef}> I am {Math.round(width)}px wide</div>
}

Refs can be used to access the DOM, but also to keep mutable value around, such as storing references to intervals:

function Timer(){
    const intervalRef = useRef(null);
    
    useEffect(()=>{
        intervalRef.current = setInterval(doSomething,100)
        return () => clearInterval(intervalRef.current)
    })
}

Using refs like in the previous example makes them similar to instance variables in classed such as this.intervalRef.

The useLayoutEffect Hook

The useLayoutEffect Hook is identical to the useEffect Hook, but it fires synchronously after all DOM mutations are completed and before the component is rendered in the browser. It can be used to read information from the DOM and adjust the appearance of components before rendering. Updated inside this Hook will be processed synchronously before the browser renders the component.

Do not use this Hook unless it is really needed, which is only in certain edge cased. useLayoutEffect will block visual updates in the browser, and thus, is slower than useEffect.

The rule here is to use useEffect first. If your mutation changes the appearance of the DOM node, which can cause it to flicker, you should use useLayoutEffect insteed.