preface

Since the release of 16.8, there has been a lot of discussion and use of Reack Hooks. I wonder if you are using them in your company

Todolist is a fully functional implementation of Todolist using React Hooks.

Features:

  • Manage requests with custom hooks
  • Code organization and logical separation using hooks

Interface to preview

Experience the address

Codesandbox. IO/s/react – hoo…

Code,

interface

First, we introduced ANTD as a UI library to save some irrelevant logic and quickly build our page skeleton


const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
  [TAB_ALL]: "All",
  [TAB_FINISHED]: "Done",
  [TAB_UNFINISHED]: "To be done"
};

function App() {
  const [activeTab, setActiveTab] = useState(TAB_ALL);
  
  return (
    <>
      <Tabs activeKey={activeTab} onChange={setActiveTab}>
        <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} />
        <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} />
        <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} />
      </Tabs>
      <div className="app-wrap">
        <h1 className="app-title">Todo List</h1>
        <Input />
        <TodoList />
      </div>
    </>
  );
}
Copy the code

Data acquisition

Once you have an interface, the next step is to get the data.

Simulation of the API

Here I created a new api.js that simulates the interface to fetch data, but the logic here is just a quick look at it, so don’t worry too much about it.

const todos = [
  {
    id: 1.text: "todo1".finished: true
  },
  {
    id: 2.text: "todo2".finished: false
  },
  {
    id: 3.text: "todo3".finished: true
  },
  {
    id: 4.text: "todo4".finished: false
  },
  {
    id: 5.text: "todo5".finished: false}];const delay = time= > new Promise(resolve= > setTimeout(resolve, time));
// Delay the method by 1 second
const withDelay = fn= > async(... args) => {await delay(1000);
  returnfn(... args); };/ / get todos
export const fetchTodos = withDelay(params= > {
  const { query, tab } = params;
  let result = todos;
  // TAB page classification
  if (tab) {
    switch (tab) {
      case "finished":
        result = result.filter(todo= > todo.finished === true);
        break;
      case "unfinished":
        result = result.filter(todo= > todo.finished === false);
        break;
      default:
        break; }}// Query with parameters
  if (query) {
    result = result.filter(todo= > todo.text.includes(query));
  }

  return Promise.resolve({
    tab,
    result
  });
});
Copy the code

Here we encapsulate a withDelay method to wrap the function and simulate the delay of the asynchronous request interface, which is convenient for us to demonstrate the loading function later.

Basic data acquisition

The most traditional way to retrieve data is to use useEffect in a component to complete the request and declare a dependency value to retrieve data if certain conditions change.

import { fetchTodos } from './api'

const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
  [TAB_ALL]: "All",
  [TAB_FINISHED]: "Done",
  [TAB_UNFINISHED]: "To be done"
};

function App() {
  const [activeTab, setActiveTab] = useState(TAB_ALL);
  
  
  // Get data
  const [loading, setLoading] = useState(false)
  const [todos, setTodos] = useState([])
  useEffect((a)= > {
    setLoading(true)
    fetchTodos({tab: activeTab})
        .then(result= > {
            setTodos(todos)
        })
        .finally((a)= > {
            setLoading(false)
        })
  }, [activeTab])
  
  
  return( <> <Tabs activeKey={activeTab} onChange={setActiveTab}> <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} /> <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} /> <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} /> </Tabs> <div className="app-wrap"> <h1 className="app-title">Todo List</h1> <Input /> <Spin spinning={loading} tip=" wait a moment ~"> <! - convey todos to component - > < TodoList todos = {todos} / > < / Spin > < / div > < / a >). }Copy the code

That’s great, and it’s what my colleagues are writing about in the new startup project at the company, but there are a few small problems with getting the data that way.

  • Use useState to set up loading state each time
  • UseState is used to establish the state of the request result each time
  • It is not easy to handle requests that have some higher level of encapsulation.

So here you encapsulate a custom hook specifically for the request.

Custom hook (Data acquisition)

A custom hook is a way of tiling the contents of the useXXX method inside the component after it is executed.

useTest() {
    const [test, setTest] = useState(' ')
    setInterval((a)= > {
        setTest(Math.random())
    }, 1000)
    return {test, setTest}
}

function App() {
    const {test, setTest} = useTest()
    
    return <span>{test}</span>
}

Copy the code

This code is equivalent to:

function App() {
    const [test, setTest] = useState(' ')
    setInterval((a)= > {
        setTest(Math.random())
    }, 1000)
    
    return <span>{test}</span>
}

Copy the code

It’s easy to hook a custom hook. With this in mind, let’s encapsulate the useRequest method we need.

export const useRequest = (fn, dependencies) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false); This method automatically manages loading const request = () => {setLoading(true);
    fn()
      .then(setData)
      .finally(() => {
        setLoading(false); }); }; UseEffect (() => {request()}, dependencies);return{// Request data, // loading state loading, // request method encapsulation request}; };Copy the code

With this custom hook, we can simplify the code inside the component.

import { fetchTodos } from './api'
import { useRequest } from './hooks'

const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
  [TAB_ALL]: "All",
  [TAB_FINISHED]: "Done",
  [TAB_UNFINISHED]: "To be done"
};

function App() {
  const [activeTab, setActiveTab] = useState(TAB_ALL);
  
  // Get data
  const {loading, data: todos} = useRequest((a)= > {
      return fetchTodos({ tab: activeTab });
  }, [activeTab]) 
  
  return( <> <Tabs activeKey={activeTab} onChange={setActiveTab}> <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} /> <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} /> <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} /> </Tabs> <div className="app-wrap"> <h1 className="app-title">Todo List</h1> <Input /> <Spin spinning={loading} tip=" wait a moment ~"> <! - convey todos to component - > < TodoList todos = {todos} / > < / Spin > < / div > < / a >). }Copy the code

Sure enough, the boilerplate code is much less, the waist is not sore and the leg is not painful, can send 5 requests in one breath!

Eliminate dirty data caused by frequent TAB switching

One of the scenarios we’re particularly likely to encounter in real development is when tab-switching does not change the view, but instead requests new list data. In this case, we might run into a problem. In the case of todolist, for example, when we switch from a full TAB to a completed TAB, we request data. But if we haven’t made a request in the completed TAB of the data is complete, go to click to complete the TAB page, and then consider a problem, an asynchronous request response time is uncertain, would we launched the first request has been finished finally takes 5 s, the second request pending final time-consuming 1 s, this second request return of data, After rendering the page, the data from the first request was returned a few seconds later, but our TAB was still waiting for the second request, which caused the dirty data bug.

This problem can be solved by using the useEffect feature in useRequest encapsulation.


export const useRequest = (fn, dependencies, defaultValue = []) = > {
  const [data, setData] = useState(defaultValue);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const request = (a)= > {
    // Define the cancel flag bit
    let cancel = false;
    setLoading(true);
    fn()
      .then(res= > {
        if(! cancel) { setData(res); }else {
          // Print the test text after the request is successfully cancelled.
          const { tab } = res;
          console.log(`request with ${tab} canceled`);
        }
      })
      .catch((a)= > {
        if(! cancel) { setError(error); } }) .finally((a)= > {
        if(! cancel) { setLoading(false); }});// The requested method returns a method that cancels the request
    return (a)= > {
      cancel = true;
    };
  };

  UseEffect returns a function to cancel the request
  // The next call to useEffect will cancel the previous request first.
  useEffect((a)= > {
    const cancelRequest = request();
    return (a)= > {
      cancelRequest();
    };
    // eslint-disable-next-line
  }, dependencies);

  return { data, setData, loading, error, request };
};
Copy the code

In fact, the cancellation request implemented in the request is just the cancellation simulated by us. In real situations, different encapsulation can be done by using the methods provided by axios and other request libraries. This is mainly about ideas. The function returned by useEffect is actually called a cleanup function, and the cleanup function is executed every time a new useEffect is executed. By using this feature, we can successfully make useEffect always render the page with only the latest request results.

You can go to the preview address and quickly click on the TAB page and see what the console prints.

Encapsulation of unsolicited requests

Now you need to add a function, click on the item in the list, and then switch to the finished state. UseRequest seems to be inappropriate because useRequest is essentially a wrapper around useEffect, and useEffect is used to initiate and rely on changes. However, this new requirement is actually to initiate requests in response to user clicks. Do we need to manually write redundant code such as setLoading? The answer, of course, is no. We use the idea of higher-order functions to encapsulate a custom hook: useWithLoading

UseWithLoading code implementation

export function useWithLoading(fn) {
  const [loading, setLoading] = useState(false);

  const func = (. args) = > {
    setLoading(true);
    returnfn(... args).finally((a)= > {
      setLoading(false);
    });
  };

  return { func, loading };
}
Copy the code

It essentially wraps a layer around the method passed in and changes the loading state before and after execution. Use:

 // Complete the todo logic
  const { func: onToggleFinished, loading: toggleLoading } = useWithLoading(
    async id => {
      awaittoggleTodo(id); });<TodoList todos={todos} onToggleFinished={onToggleFinished} />
      
Copy the code

Code organization

Add a new feature that allows input placeholder to switch text according to TAB page transitions. Note that we provide an example of a mistake made by people who have just moved from Vue2. X and React Class Component.

❌ Error Example

import { fetchTodos } from './api'
import { useRequest } from './hooks'

const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
  [TAB_ALL]: "All",
  [TAB_FINISHED]: "Done",
  [TAB_UNFINISHED]: "To be done"
};

function App() {
  // state is put together
  const [activeTab, setActiveTab] = useState(TAB_ALL);
  const [placeholder, setPlaceholder] = useState("");
  const [query, setQuery] = useState("");
  
  // Side effects are put together
  const {loading, data: todos} = useRequest((a)= > {
      return fetchTodos({ tab: activeTab });
  }, [activeTab]) 
  useEffect((a)= > {
    setPlaceholder(` in${tabMap[activeTab]}Search within `);
  }, [activeTab]);
  const { func: onToggleFinished, loading: toggleLoading } = useWithLoading(
    async id => {
      awaittoggleTodo(id); });return( <> <Tabs activeKey={activeTab} onChange={setActiveTab}> <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} /> <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} /> <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} /> </Tabs> <div className="app-wrap"> <h1 className="app-title">Todo List</h1> <Input /> <Spin spinning={loading} tip=" wait a moment ~"> <! - convey todos to component - > < TodoList todos = {todos} / > < / Spin > < / div > < / a >). }Copy the code

Note that in the previous vUE and React development, because the vUE code was organized based on options (based on options such as Data, methods, computed organization), react also initialized state in one place. The class then defines a bunch of XXX methods, which makes the logic very difficult to read for anyone new to the code.

Which is why hooks solve the problem that we can based on logical concerns about how our code is organized: stop grouping useState useEffect into neat but useless categories. !

Here’s a comparison of one of the components in the @vue/ UI library from the previous vue Composition API introduction

The code organization advocated by Vue Composition API is to divide logic into customized hook functions one by one, which is consistent with react Hook.

export default {
  setup() { // ...}}function useCurrentFolderData(nextworkState) { // ...
}

function useFolderNavigation({ nextworkState, currentFolderData }) { // ...
}

function useFavoriteFolder(currentFolderData) { // ...
}

function useHiddenFolders() { // ...
}

function useCreateFolder(openFolder) { // ...
}
Copy the code

✔️ Correct Example

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import TodoInput from "./todo-input";
import TodoList from "./todo-list";
import { Spin, Tabs } from "antd";
import { fetchTodos, toggleTodo } from "./api";
import { useRequest, useWithLoading } from "./hook";

import "antd/dist/antd.css";
import "./styles/styles.css";
import "./styles/reset.css";

const { TabPane } = Tabs;

const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
  [TAB_ALL]: "All",
  [TAB_FINISHED]: "Done",
  [TAB_UNFINISHED]: "To be done"
};

function App() {
  const [activeTab, setActiveTab] = useState(TAB_ALL);

  // Data acquisition logic
  const [query, setQuery] = useState("");
  const {
    data: { result: todos = [] },
    loading: listLoading
  } = useRequest((a)= > {
    return fetchTodos({ query, tab: activeTab });
  }, [query, activeTab]);

  // placeHolder
  const [placeholder, setPlaceholder] = useState("");
  useEffect((a)= > {
    setPlaceholder(` in${tabMap[activeTab]}Search within `);
  }, [activeTab]);

  // Complete the todo logic
  const { func: onToggleFinished, loading: toggleLoading } = useWithLoading(
    async id => {
      awaittoggleTodo(id); });constloading = !! listLoading || !! toggleLoading;return( <> <Tabs activeKey={activeTab} onChange={setActiveTab}> <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} /> <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} /> <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} /> </Tabs> <div className="app-wrap"> <h1 className="app-title">Todo List</h1> <TodoInput placeholder={placeholder} OnSetQuery ={setQuery} /> <Spin spinning={loading} tip=" wait a moment ~"> <TodoList todos={todos} onToggleFinished={onToggleFinished} /> </Spin> </div> </> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);Copy the code

conclusion

React Hook provides a new way to better organize the logical code inside components, making large, complex components easier to maintain. In addition, the function of custom Hook is very powerful. In the company’s project, I have encapsulated many useful custom hooks, such as UseTable, useTreeSearch, useTabs, etc., which can be combined with the component library and UI interaction requirements used by the company to encapsulate some logic with finer granularity. Use your imagination! useYourImagination!