In the article why React Needs Hooks, we discussed why the React development team added hooks to Function Component. In this article, I will provide you with a comprehensive practical guide to React Hook, including the following aspects:

  • React Hook
  • Common Hook introduction
    • useState
    • useEffect
    • useRef
    • useCallback
    • useMemo
    • useContext
    • useReducer
  • Customize the Hook

React Hook

React Hook is a new property added after React 16.8. In simplest terms, React Hook is a set of built-in functions provided by React. These functions allow the Function Component to have the same Component state and side effects as the Class Component.

Common Hook introduction

Next, I will introduce some commonly used hooks. For each Hook, I will cover the following aspects:

  • role
  • usage
  • Matters needing attention

useState

role

UseState is simple to understand. Like this.state in the Class Component, useState manages Component state. Before React Hook, Function Component was also called Functional Stateless Component (FSC), This is because Function Component generates a new Function scope every time it executes, so state cannot be shared between different render versions of the same Component. Therefore, developers need to change Function Component to Class Component whenever they need to introduce state into a Component, which makes the developer experience very bad. UseState solves this problem by allowing the Function Component to persist its state to a location in the Memory cell of the React Runtime, from which it can be retrieved each time the Component is rerendered. And when the state is updated, the component is also re-rendered.

usage

const [state, setState] = useState(initialState)
Copy the code

UseState accepts an initialState variable as the initial value of the state, and the return value is an array. The first element of the array represents the latest value of the current state, and the second element is a function used to update the state. It is important to note that the names of the state and setState variables are not fixed. You should choose different names for your business, such as text and setText, or width and setWidth. (If you are not familiar with the above array deconstruction assignment, please take a look at the introduction of MDN.)

In real development, a component may have more than one state. If the component has more than one state, useState can be called multiple times within the component. Here is a simple example:

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const App = (a)= > {
  const [counter, setCounter] = useState(0)
  const [text, setText] = useState(' ')

  const handleTextChange = (event) = > {
    setText(event.target.value)
  }

  return (
    <>
      <div>Current counter: {counter}</div>
      <button
        onClick={() => setCounter(counter + 1)}
      >
        Increase counter
      </button>
      <input
        onChange={handleTextChange}
        value={text}
      />
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
Copy the code

Similar to the Class Component’s this.setState API, setCounter and setText can both take a function called updater as an argument. The updater takes the latest value of the current state and returns the next state. For example, the setCounter argument can be changed to a function:

<button
  onClick={() => {
    setCounter(counter => counter + 1)
  }}
>
  Increase counter
</button>
Copy the code

The initialState of useState can also be a function that generates the initial value of the state, mainly to avoid the need for the initialState to be recomputed each time the component renders. Here’s a simple example:

const [state, setState] = useState((a)= > {
  const initialState = someExpensiveComputation(props)
  return initialState
})
Copy the code

Matters needing attention

SetState is full substitution

An important difference between Function Component setState and Class Component this.setState is that this.setState shallowly merges the currently setState Merge) to the old state operation. The setState function replaces the old state (replace) directly with the new state. Therefore, when we write Function Component, we should divide state properly to avoid managing unrelated states together. For example, the following is a bad design:

const [state, setState] = useState({ left: 0.top: 0.width: 0.height: 0 })
Copy the code

In the above code, we bind the DOM position {left: 0, top: 0} and the DOM size {width: 0, height: 0} in the same state, so we need to maintain the other state when updating either state:

const handleContainerResize = ({ width, height }) = >{ setState({... state, width, height}) }const handleContainerMove = ({ left, top }) = >{ setState({... state, left, top}) }Copy the code

This is inconvenient and bug-prone, so it makes more sense to keep the location and size information in two different states to avoid having to manually maintain the other state when updating one state:

// separate state into position and size states
const [position, setPosition] = useState({ left: 0.top: 0 })
const [size, setSize] = useState({ width: 0.height: 0})

const handleContainerResize = ({ width, height }) = > {
  setSize({width, height})
}

const handleContainerMove = ({ left, top }) = > {
  setPosition({left, top})
}
Copy the code

If you do want to group multiple separate states together, it is recommended that you use useReducer to manage your states so that your code is more maintainable.

If you set the same state value, setState willbailing out of update

If the new state received by setState is the same as the current state (determined by object.is), React will not re-render child components or trigger side effects. Note that React does not render child components, but it does re-render the current component. If your component rendering is computationally expensive, consider using useMemo to optimize performance.

SetState has no callback function

Both useState and Class Component this.setState are called asynchronously, meaning that each time a Component calls them, it does not get the latest state value. To solve this problem, the Class Component’s this.setState allows you to retrieve the latest state value using a callback function:

this.setState(newState, state => {
  console.log("I get new state", state)
})
Copy the code

The setState Function of the Function Component does not have a callback to get the latest state, but we can use useEffect to achieve the same effect, as discussed in StackOverflow.

useEffect

role

UseEffect is used to enable Function Components to have side effects as well. So what are side effects? We can start by looking at wikipedia’s definition:

In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation.

In layman’s terms, a side effect of a function is any effect that the function has on the environment other than the value it returns. For example, if every time we execute a function that operates on a global variable, operations on global variables are a side effect of that function. In the React world, side effects can be roughly divided into two categories: one is to call the browser API, such as using addEventListener to addEventListener functions, and the other is to initiate a request for server data, such as asynchronously obtaining user information when a user card is mounted. Before React, if you needed to implement side effects in a Component, you would write it as a Class Component, and then write side effects in the Component’s lifecycle function. This would cause a lot of code design problems. Once hooks are out, developers can use useEffect to define side effects in Function Component. UseEffect can override all scenarios in which componentDidMount, componentDidUpdate, And componentWillUnmount are used together. However, there are fundamental differences between useEffect and life cycle function design concepts. If we use useEffect in the same way as life cycle function, some strange problems may arise. If you are interested, check out this article: A Complete Guide to useEffect, which describes A more correct mental model to use useEffect.

usage

useEffect(effect, dependencies?)
Copy the code

The first argument to useEffect is the side effect function to be executed. It can be any user-defined function in which the user can operate on some browser API or interact with the external environment. This function is called every time a component is rendered.

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'

const UserDetail = ({ userId }) = > {
  const [userDetail, setUserDetail] = useState({})

  useEffect((a)= > {
    fetch(`https://myapi/users/${userId}`)
      .then(response= > response.json())
      .then(user= > setUserDetail(userDetail))
  })

  return (
    <div>
      <div>User Name: {userDetail.name}</div>
    </div>
  )
}

ReactDOM.render(<UserDetail />, document.getElementById('root'))
Copy the code

The side effect of getting user details defined above is that the UserDetail component executes after each rendering, so when the component is first mounted it makes a request to the server to get user details and then updates the value of the UserDetail, The first mount here is analogous to the Class Component’s componentDidMount. However, if you try to run the above code, you will find that the code is in an endless loop: the component keeps making requests to the server. The reason for this endless loop is that useEffect calls setUserDetail, which updates the value of userDetail and causes the component to re-render. UseEffect’s effect continues after re-rendering, and the component re-renders again… To avoid repeated side effects, useEffect allows us to limit when a side effect is executed with the second parameter Dependencies: We specify the side effect of dependencies, which is executed only when the value of the element in the dependencies array changes. To avoid this loop, we need to specify the userId of the dependencies we define for the side effect:

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'

const UserDetail = ({ userId }) = > {
  const [userDetail, setUserDetail] = useState({})

  useEffect((a)= > {
    fetch(`https://myapi/users/${userId}`)
      .then(response= > response.json())
      .then(user= > setUserDetail(userDetail))
  }, [userId])

  return (
    <div>
      <div>User Name: ${userDetail.name}</div>
    </div>
  )
}

ReactDOM.render(<UserDetail />, document.getElementById('root'))
Copy the code

In addition to making server requests, we often need to call the browser API in useEffect, such as using addEventListener to add listener functions for browser events. Once we use addEventListener, we must call removeEventListener when appropriate to remove the listener from the event, otherwise there will be performance issues. UseEffect allows us to return a cleanup function in the side effect function. This function will be executed before the component is re-rendered. We can remove the event listening from the function returned. Here is an example:

import React, { useEffect } from 'react'
import ReactDOM from 'react-dom'

const WindowScrollListener = (a)= > {
  useEffect((a)= > {
    const handleWindowScroll = (a)= > console.log('yean, window is scrolling! ')
    window.addEventListener('scroll', handleWindowScroll)

    // this is clean up function
    return (a)= > {
      window.removeEventListener(handleWindowScroll)
    }
  }, [])

  return (
    <div>
      I can listen to the window scroll event!
    </div>
  )
}

ReactDOM.render(<WindowScrollListener />, document.getElementById('root'))
Copy the code

In the code above we register a function that listens for page scrolling events after the first rendering of the WindowScrollListener component and remove the listener before the next rendering of the component. Because we specify an empty array as the dependencies for this side effect, this side effect is only executed once when the component first renders, and its cleanup function is only executed when the component is unmounted, which avoids frequent registration of page listeners that affect page performance.

Matters needing attention

Avoid using “old” variables

The most common problem we may encounter when using useEffect is that when our effect function is called, we get some state, props or variables that are not the latest variable but the old one. The reasons for this problem are: Is in fact the side effects of we define a function, and JS scope is lexical scope, so the function is used to determine when a variable’s value is that it is defined, in the simplest terms, is that the useEffect effect can remember when it is defined the value of the variable outside, so it is called the value may not be used the latest values. There are two ways to solve this problem. One is to store variables in a ref that you want to get the latest value every time effect is called, and update the ref value every time the component renders:

const [someState, setSomeState] = useState()
const someStateRef = useRef()

someStateRef.current = someState

useEffect((a)= >{... const latestSomeState = someStateRef.currentconsole.log(latestSomeState) }, [otherDependencies...] )Copy the code

It’s not very elegant, but it solves our problem. If you haven’t seen useRef before, check out the useRef section of this article. Another recommended solution to this problem is to add all the variables used in the effect dependencies section. In practice, we can use facebook’s own ESlint-plugin-react-hooks’ Enumer-deps rule to implement the code constraint. After you add this constraint to your project, Add “someState” to “useEffect” dependencies. If you add “someState” to “useEffect” dependencies, “add” someState “to” useEffect “dependencies.

const [someState, setSomeState] = useState()

useEffect((a)= >{... console.log(someState) }, [otherDependencies..., someState])Copy the code

useRef

role

UseRef is used to share data between Component renderings in the same way we assign this in the Class Component.

usage

const refObject = useRef(initialValue)
Copy the code

UseRef receives initialValue as the initialValue, and its return value is a ref object whose.current property is the latest value for that data. The simplest way to use useRef is to store a reference to a DOM object in a Function Component, as in this example:

import { useRef, useEffect } from 'react'
import ReactDOM from 'react-dom'

const AutoFocusInput = (a)= > {
  const inputRef = useRef(null)

  useEffect((a)= > {
    // auto focus when component mount
    inputRef.current.focus()
  }, [])

  return (
    <input ref={inputRef} type='text' />
  )
}

ReactDOM.render(<AutoFocusInput />, document.getElementById('root'))
Copy the code

In the above code, inputRef is a {current: inputDomInstance} object, but it ensures that the component gets the same object every time it renders.

Matters needing attention

Updating the REF object does not trigger component rerendering

The ref object returned by useRef is reassigned without causing the component to rerender. Use useState to store data if you need to.

useCallback

role

With the advent of hooks, developers are increasingly using Function Components to develop requirements. When defining a Function Component, developers usually define inline functions inside the Function body. These functions are redefined each time the Component is re-rendered. If they are passed to the child Component as props, Even if the values of the other props are not changed, it causes the child components to be re-rendered, and the re-rendering of useless components can cause performance problems. Another problem with generating a new inline function is that when we pass an inline function as a dependency into the useEffect dependencies array, the effect in useEffect is called frequently because the function is frequently regenerated. To solve the above problem, React allows us to use useCallback to memoize the currently defined functions and return the previously defined functions instead of using the newly defined functions the next time a component renders.

usage

const memoizedCallback = useCallback(callback, dependencies)
Copy the code

UseCallback takes two parameters: the first parameter is the function to remember and the second parameter is the function dependencies. UseCallback returns the function only when the value of the elements in the dependencies array changes. Otherwise useCallback will return the previously defined function. Here is a simple example of using useCallback to optimize frequent rendering of subcomponents:

import React, { useCallback } from 'react'
import useSearch from 'hooks/useSearch'
import ReactDOM from 'react-dom'

// this list may contain thousands of items, so each re-render is expensive
const HugeList = ({ items, onClick }) = > {
  return (
    <div>
      {
        items.map((item, index) => (
          <div
            key={index}
            onClick={()= > onClick(index)}
          >
            {item}
          </div>))}</div>)}const MemoizedHugeList = React.memo(HugeList)

const SearchApp = ({ searchText }) = > {
  const handleClick = useCallback(item= > {
    console.log('You clicked', item)
  }, [])
  const items = useSearch(searchText)

  return (
    <MemoizedHugeList
      items={items}
      onClick={handleClick}
    />
  )
}

ReactDOM.render(<SearchApp />, document.getElementById('root'))
Copy the code

In the example above, I defined a HugeList component. Since this component needs to render a large list of items, each re-rendering is very performance expensive, so I used the react. memo function to render the component only when the onClick function and the items array change. If you’re not familiar with react. memo, check out this article I wrote. I then used MemoizedHugeList in SearchApp, and since I wanted to avoid repeating the rendering of the component, I used useCallback to remember the handleClick function defined so that when rendering behind the component, The handleClick variables all point to the same function, so MemorizedHugeList will only be rerendered if items change. Note that my handleClick function does not use any external dependencies, so its dependencies are an empty array. If your function uses external dependencies, put them in the dependencies parameter of useCallback. Otherwise there will be bugs.

Matters needing attention

Avoid using “old” variables in functions

Like useEffect, we need to write all the external variables used in the callback of useCallback into the Dependencies array, otherwise we might use the “old” external variables in the callback call.

Not all functions need to use useCallback

Performance optimizations are not free. They ALWAYS come with a cost but do NOT always come with a benefit to offset that cost.

Any optimization has a cost, and so does useCallback. When we call useCallback in Function Component, we need to do some calculations behind React to ensure that the dependencies do not change and we get the same Function. Therefore, if we abuse useCallback, Does not result in the desired performance optimization, but rather affects our performance, such as the following example is a bad example of using useCallback:

import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'

const DummyButton = (a)= > {
  const handleClick = useCallback((a)= > {
    console.log('button is clicked')}, [])return (
    <button onClick={handleClick}>
      I'm super dummy
    </button>
  )
}

ReactDOM.render(<DummyButton />, document.getElementById('root'))
Copy the code

The useCallback used in the above example does nothing to optimize the performance of the code, because the code above actually executes as follows:

import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'

const DummyButton = (a)= > {
  const inlineClick = (a)= > {
    console.log('button is clicked')}const handleClick = useCallback(inlineClick, [])

  return (
    <button onClick={handleClick}>
      I'm super dummy
    </button>
  )
}

ReactDOM.render(<DummyButton />, document.getElementById('root'))
Copy the code

As we can see from the above code, even if we use useCallback, the browser will need to create a new inline function inlineClick when executing the DummyButton function. This will have the same effect as if we did not use useCallback, and besides, The optimized code consumes more computational resources because it also calls useCallback, and consumes more memory resources because the useCallback function stores additional variables (such as the previous dependencies). So we can’t just wrap all embedded functions in useCallback, only those functions that really need to be remembered.

useMemo

role

UseMemo is very similar to useCallback, except that it allows you to remember any type of variable (not just functions).

usage

const memoizedValue = useMemo((a)= > valueNeededToBeMemoized, dependencies)
Copy the code

MemoizedValue is used when the element in the dependencies array, the second parameter of useMemo, does not change. Here’s an example:

import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'

const RenderPrimes = ({ iterations, multiplier }) = > {
  const primes = React.useMemo((a)= > calculatePrimes(iterations, multiplier), [
    iterations,
    multiplier
  ])

  return (
    <div>
      Primes! {primes}
    </div>
  )
}

ReactDOM.render(<RenderPrimes />, document.getElementById('root'))
Copy the code

In the example above, the calculatePrimes are used to calculate prime numbers, so each call to it consumes a lot of computing resources. To improve the performance of component rendering, we can use useMemo to remember the calculated results. When iterations and multipliers remain unchanged, we do not need to re-execute the calculatePrimes function to recalculate, but simply use the last result.

Matters needing attention

Not all variables need to be wrapped in useMemo

Just like useCallback, we only use useMemo to encapsulate variables that really need to be remembered. Remember not to abuse useMemo. Here is an example of abusing useMemo:

import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'

const DummyDisplay = (a)= > {
  const items = useMemo((a)= > ['1'.'2'.'3'], [])
  
  return (
    <>
      {
        items.map(item => <div key={item}>{item}</div>)}</>
  )
}

ReactDOM.render(<DummyDisplay />, document.getElementById('root'))
Copy the code

It would be better to define items directly outside the component in the above example:

import React from 'react'
import ReactDOM from 'react-dom'

const items = ['1'.'2'.'3']

const DummyDisplay = (a)= > {  
  return (
    <>
      {
        items.map(item => <div key={item}>{item}</div>)}</>
  )
}

ReactDOM.render(<DummyDisplay />, document.getElementById('root'))
Copy the code

useContext

role

As we know, the way of transferring parameters between components in React is props. If some states are defined in the parent component, and these states need to be used in the deep-seated nested child components of the component, these states need to be passed layer by layer in the form of props drilling, which causes the problems of props drilling. To solve this problem, React allows us to use Context to pass state between the parent component and its children at any level below. We can use the useContext Hook to use the context in Function Component.

usage

const value = useContext(MyContext)
Copy the code

UseContext takes a context object as an argument, which is generated by the react. createContext function. The return value of useContext is the value of the current context, which is determined by the nearest < myContext.provider >. Once useContext is used in a component, that component subscris to changes to the context. When the value of the most recent < myContext. Provider> context changes, the child components that use that context are rerendered. And they’re going to get the latest value of the context. Here’s a concrete example:

import React, { useContext, useState } from 'react'
import ReactDOM from 'react-dom'

// define context
const NumberContext = React.createContext()

const NumberDisplay = (a)= > {
  const [currentNumber, setCurrentNumber] = useContext(NumberContext)

  const handleCurrentNumberChange = (a)= > {
    setCurrentNumber(Math.floor(Math.random() * 100))}return (
    <>
      <div>Current number is: {currentNumber}</div>
      <button onClick={handleCurrentNumberChange}>Change current number</button>
    </>
  )
}

const ParentComponent = () => {
  const [currentNumber, setCurrentNumber] = useState({})

  return (
    <NumberContext.Provider value={[currentNumber, setCurrentNumber]}>
      <NumberDisplay />
    </NumberContext.Provider>
  )
}

ReactDOM.render(<ParentComponent />, document.getElementById('root'))
Copy the code

Matters needing attention

Avoid useless rendering

As we mentioned above, if a Function Component uses useContext(SomeContext), it subscribing to the SomeContext change, so that when the value of the somecontext.provider changes, The component will be re-rendered. One of the problems here is that we might have a lot of different data in the same context, and different subcomponents might only care about a certain part of the context, and when any value in the context changes, they get rerendered whether they use the data or not, This can cause some performance problems. Here’s a simple example:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const AppContext = React.createContext()

const ChildrenComponent = (a)= > {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme

  return (
    <div>
      <ExpensiveTree theme={theme} />
    </div>
  )
}

const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})

  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
Copy the code

In the example above, the ChildrenComponent only uses the appContext’s. Theme property, but when other appContext properties such as Configuration are updated, the ChildrenComponent will also be rerendered. The ChildrenComponent calls a costly ExpensiveTree component, so this useless rendering affects the performance of the ExpensiveTree page.

Break up the Context

This approach is the most recommended. Like useState, we can split contexts that do not need to be changed at the same time into different contexts, making their responsibilities more distinct, so that components subscribe only to those contexts that they need to subscribe to to avoid useless rerendering. For example, the above code could look like this:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const ThemeContext = React.createContext()
const ConfigurationContext = React.createContext()

const ChildrenComponent = (a)= > {
  const [themeContext] = useContext(ThemeContext)

  return (
    <div>
      <ExpensiveTree theme={themeContext} />
    </div>
  )
}

const App = () => {
  const [themeContext, setThemeContext] = useState({ color: 'red' })
  const [configurationContext, setConfigurationContext] = useState({ showTips: false })

  return (
    <ThemeContext.Provider value={[themeContext, setThemeContext]}>
      <ConfigurationContext.Provider value={[configurationContext, setConfigurationContext]}>
        <ChildrenComponent />
      </ConfigurationContext.Provider>
    </ThemeContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
Copy the code
Break up your components and use Memo to optimize components that consume performance

If for some reason you cannot split the context, you can still optimize the performance-consuming component by separating it from the rest of the parent component and using the Memo function. For example, the above code could be changed to:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const AppContext = React.createContext()

const ExpensiveComponentWrapper = React.memo(({ theme }) = > {
  return( <ExpensiveTree theme={theme} /> ) }) const ChildrenComponent = () => { const [appContext] = useContext(AppContext) const theme = appContext.theme return ( <div> <ExpensiveComponentWrapper theme={theme} /> </div> ) } const App = () => {  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }}) return ( <AppContext.Provider value={[appContext, setAppContext]}> <ChildrenComponent /> </AppContext.Provider> ) } ReactDOM.render(<App />, document.getElementById('root'))Copy the code
Components can also be optimized using useMemo without splitting them

Of course, we can also use useMemo to optimize the above code without splitting the components. The code is as follows:

import React, { useContext, useState, useMemo } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const AppContext = React.createContext()

const ChildrenComponent = (a)= > {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme

  return useMemo((a)= > (
      <div>
        <ExpensiveTree theme={theme} />
      </div>
    ),
    [theme]
  )
}

const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})

  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
Copy the code

useReducer

role

In its simplest terms, useReducer allows us to manage the state transition of our Component reducers and actions in Function Component as we do with Redux.

usage

const [state, dispatch] = useReducer(reducer, initialArg, init?)
Copy the code

Similar to useState, useReducer is used to manage component state. Unlike useState’s setState function, useReducer returns a dispatch function that triggers actions that change state rather than setting state directly. How different actions generate new state values is defined in the Reducer. The useReducer receives three parameters:

  • Reducer: This is a function whose signature is(currentState, action) => newStateFrom its function signature, you can see that it receives the current state and currentdispatchtheactionIs an argument, and then returns the next state, which is responsible for the state transition.
  • InitialArg: If the caller does not provide a thirdinitParameter, this parameter represents thisreducerThe initial state of theta, ifinitIf parameters are specified,initialArgWill be passed in as a parameterinitFunction to generate the initial state.
  • Init: This is a function that generates the initial state, and its function signature is(initialArg) => initialState, and its function signature indicates that it will receiveuseReducerThe second argument toinitialArgAs a parameter, and generate an initial stateinitialState. The following isuseReducerA simple example of…
import React, { useState, useReducer } from 'react'

let todoId = 1

const reducer = (currentState, action) = > {
  switch(action.type) {
    case 'add':
      return [...currentState, {id: todoId++, text: action.text}]
    case 'delete':
      return currentState.filter(({ id }) = >action.id ! == id)default:
      throw new Error('Unsupported action type')}}const Todo = ({ id, text, onDelete }) = > {
  return (
    <div>
      {text}
      <button
        onClick={()= > onDelete(id)}
      >
        remove
      </button>
    </div>)}const App = (a)= > {
  const [todos, dispatch] = useReducer(reducer, [])
  const [text, setText] = useState(' ')

  return (
    <>
      {
        todos.map(({ id, text }) => {
          return (
            <Todo
              text={text}
              key={id}
              id={id}
              onDelete={id => {
                dispatch({ type: 'delete', id })
              }}
            />
          )
        })
      }
      <input onChange={event => setText(event.target.value)} />
      <button
        onClick={() => {
          dispatch({ type: 'add', text })
          setText('')
        }}
      >
        add todo
      </button>
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
Copy the code

Matters needing attention

useReducer vs useState

Both useReducer and useState can be used to manage component states. The biggest difference between them is that useReducer manages state and state changes in the Reducer function, which is very convenient for us to debug some complicated state management because it is closed to state changes. And since the setState returned by useState can set the value of our state directly anywhere, it is difficult to debug when the state transition logic of our component is complex because it is open for state management. In general, we can refer to the following principles when choosing useReducer and useState:

  • Use in the following casesuseState
    • stateThe value primitives is a JS data type, such asnumber.stringandbooleanEtc.
    • stateThe conversion logic is simple
    • The different states within a component are not related, and they can use multiple independent onesuseStateTo manage it individually.
  • Use in the following casesuseReducer
    • stateThe value isobjectorarray
    • stateThe conversion logic is complex and needs to be usedreducerFunction to unify management
    • Multiple within a componentstateIt’s correlated, changing one state requires changing the other, putting them in the same placestateReducer is used for unified management
    • State is defined in the parent component, but it needs to be used and changed in a deeply nested child component, which can be used simultaneouslyuseReduceranduseContextTwo hooks, willdispatchMethods are put into context to avoid componentprops drilling
    • If you want your status management to be predictable and maintainable, pleaseuseReducer
    • If you want your status changes to be tested, useuseReducer

Customize the Hook

React’s built-in hooks have been introduced. Now let’s look at how to write our own hooks.

role

The purpose of custom hooks is to allow us to encapsulate some non-UI logic that can be shared between different components to improve our efficiency in developing business code.

What is a custom Hook

Remember that a Hook is a function, so a custom Hook is a function that uses the React built-in Hook or some other custom Hook. Although we can name our custom hooks any way we want, we need to start our hooks with use in order to make our code easier for other developers to understand and for development tools such as eslint-plugin-react-hooks to give us better tips. And use hump names such as useLocation, useLocalStorage, useQueryString, and so on.

example

Here is a simple example of a custom hook:

import React, { useState, useCallback } from 'react'
import ReactDOM from 'react-dom'

const useCounter = (a)= > {
  const [counter, setCounter] = useState(0)
  
  const increase = useCallback((a)= > setCounter(counter= > ++counter), [])
  const decrease = useCallback((a)= > setCounter(counter= > --counter), [])

  return {
    counter,
    increase,
    decrease
  }
}

const App = (a)= > {
  const { counter, increase, decrease } = useCounter()

  return (
    <>
      <div>Counter: {counter}</div>
      <button onClick={increase}>increase</button>
      <button onClick={decrease}>decrease</button>
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
Copy the code

conclusion

In this article, I introduce you to some common built-in hooks and how to define our own Hook. React Hook is a powerful function in general. Properly using it can improve the reuse rate of code and the development efficiency of business code. However, it also has many hidden bugs. My personal advice is to use the eslint-plugin-react-hooks plugin as much as possible, as it really does catch bugs in code as it is being developed, but sometimes trying to get rid of them is annoying :).

In the next article in this series I’ll show you how to test our custom hooks to improve the quality of our code, so stay tuned.

reference

  • When to useMemo and useCallback
  • Preventing rerenders with React.memo and useContext hook
  • React Hook Reference
  • useReducer vs useState in React

Personal Technology dynamics

The article started on my personal blog

Welcome to pay attention to the public number of green Onions to learn and grow together