Read Hooks Numbers - swr Source

Posted by Danaldinho on Mon, 11 Nov 2019 05:01:20 +0100

1 Introduction

Pickups are an important part of the front-end business and have evolved several times:

  • fetch Compatibility with $.post is good enough to replace all kinds of decimal encapsulations.
  • I've been using it for a long time, and it's good to find a more scalable, ssr-enabled isomorphic number scheme, such as isomorphic-fetch,axios.
  • For data-driven scenarios, data streams gradually encapsulate fetches, while data isLoading error encapsulates data-driven state change management.
  • Hooks make components more Reactive, and we find that counts are gracefully returned to components. swr A textbook example.

swr Submitted on October 29, 2019, 4,000+ stars were saved in just 12 days, yielding an average of 300+ star s a day!This week's intensive reading examines the functionality and source code of this library to see why and What it takes from this React Hooks library.

2 Overview

The function of swr is introduced first.

To distinguish it from official documents, the author introduces it in an exploratory way, but the examples are taken from official documents.

2.1 Why use Hooks to count

First, answer the basic question: Why do you use Hooks instead of fetch or data stream counting?

Because Hooks can reach the UI life cycle, counting is essentially a part of UI presentation or interaction.The Hooks numbers are as follows:

import useSWR from "swr";

function Profile() {
  const { data, error } = useSWR("/api/user", fetcher);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

The first thing you see is that the asynchronous logic is described in synchronous writing because rendering is performed twice.

useSWR receives three parameters, the first parameter being the number key, which is passed in as the first parameter of the second parameter fetcher, the URL in normal scenarios, and the third parameter being the configuration item.

Hooks is more powerful than that. The short lines of code above also have the following features:

  1. Can refresh automatically.
  2. Local caching is preferred when components are destroyed and rendered.
  3. Browser fallback automatically remembers the scrollbar position on the list page.
  4. When tabs are switched, the focus tab recalculates.

Of course, automatic refresh or recalculation is not necessarily what we want. swr Allow custom configuration.

2.2 Configuration

As mentioned above, useSWR also has a third parameter as a configuration item.

Independent Configuration

Configure each useSWR independently with the third parameter:

useSWR("/api/user", fetcher, { revalidateOnFocus: false });

Configuration items can be referenced File.

You can configure suspense mode, focus recalculation, recalculation interval/on, failure recalculation, timeout, callback function at the time of success/failure/retry, and so on.

If the second parameter is of type object, the effect is a configuration item. The second fetcher is provided for convenience only and can also be configured in the objectconfiguration item.

Global Configuration

SWRConfig can modify configuration in bulk:

import useSWR, { SWRConfig } from "swr";

function Dashboard() {
  const { data: events } = useSWR("/api/events");
  // ...
}

function App() {
  return (
    <SWRConfig value={{ refreshInterval: 3000 }}>
      <Dashboard />
    </SWRConfig>
  );
}

Standalone configuration takes precedence over global configuration and is described in the intensive reading section.

The most important configuration item is the fetcher, which determines how numbers are taken.

2.3 Custom Numbering

Custom fetch logic can be divided into several abstract granularities, such as a custom fetch url or a custom whole fetch function. swr A custom fetcher with relative intermediate granularity has been adopted:

import fetch from "unfetch";

const fetcher = url => fetch(url).then(r => r.json());

function App() {
  const { data } = useSWR("/api/data", fetcher);
  // ...
}

So fetcher itself is an extension point. We can not only customize the fetch function, customize business processing logic, but also customize the fetch protocol:

import { request } from "graphql-request";

const API = "https://api.graph.cool/simple/v1/movies";
const fetcher = query => request(API, query);

function App() {
  const { data, error } = useSWR(
    `{
      Movie(title: "Inception") {
        releaseDate
        actors {
          name
        }
      }
    }`,
    fetcher
  );
  // ...
}

This answers the reason why the first parameter is called Key, which is a grammatical description under graphql.

At this point, we can customize the counting function, but we can't control when the counting occurs because the Hooks notation combines the timing of counting with the timing of rendering. swr The conditional number mechanism can solve this problem.

2.4 Conditional Number

The so-called conditional number, which terminates when the first parameter of useSWR is null, can be made dynamic by using a ternary operator or function as the first parameter:

// conditionally fetch
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);

// ...or return a falsy value
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);

In the example above, the number is not taken when shouldFetch is false.

The first parameter is recommended as a callback function, so swr Catches internal exceptions, such as:

// ... or throw an error when user.id is not defined
const { data, error } = useSWR(() => "/api/data?uid=" + user.id, fetcher);

If the user object does not exist, the call to user.id fails, and the error is caught and thrown into the error object.

In fact, user.id is also a dependent counting scenario that needs to be re-counted when user.id changes.

2.5 Dependent Access

If one fetch depends on the result of another, a new fetch will not be triggered until the end of the first data, which is in swr You don't need to be particularly concerned. Just write the useSWR in the order you depend on it:

function MyProjects() {
  const { data: user } = useSWR("/api/user");
  const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);

  if (!projects) return "loading...";
  return "You have " + projects.length + " projects";
}

swr Dependent requests are sent as concurrently as possible, and dependent counts are sent at a time in the order of dependencies.

As you can imagine, if you manage fetches manually, when dependencies are complex, you will need to carefully adjust the recursive nesting structure of fetches to ensure maximum parallelism. swr In an environment where only sequential writing is required, this is a very efficient improvement.Optimizations are described in detail in the Source Interpretation section below.

Dependent fetching is a scenario in which fetching is automatically triggered again. swr Manual trigger recalculation is also supported.

2.6 Manual Trigger Count

Trigger can trigger the number manually through Key:

import useSWR, { trigger } from "swr";

function App() {
  return (
    <div>
      <Profile />
      <button
        onClick={() => {
          // set the cookie as expired
          document.cookie =
            "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";

          // tell all SWRs with this key to revalidate
          trigger("/api/user");
        }}
      >
        Logout
      </button>
    </div>
  );
}

This is not necessary in most scenarios, because the data and dependencies determine the triggering of the request, but the need to take a number is not determined by the parameters but by the timing, which requires the ability to do so manually.

2.7 Optimistic

Especially in form scenarios, data changes are expected, when data-driven scenarios can only wait for the back-end to return results, which can be optimized to modify the data locally and refresh it after the back-end results are returned:

import useSWR, { mutate } from "swr";

function Profile() {
  const { data } = useSWR("/api/user", fetcher);

  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button
        onClick={async () => {
          const newName = data.name.toUpperCase();
          // send a request to the API to update the data
          await requestUpdateUsername(newName);
          // update the local data immediately and revalidate (refetch)
          mutate("/api/user", { ...data, name: newName });
        }}
      >
        Uppercase my name!
      </button>
    </div>
  );
}

With mutate, results can be returned under a Key temporarily modified locally, especially in poor network environments.An optimistic number indicates that the result is optimistic and predictable, so that the result can be predicted and modified before it returns.

2.8 Suspense mode

In React Suspense mode, all submodules can be lazily loaded, including code and requests, and can be waited as long as the suspense property is turned on:

import { Suspense } from "react";
import useSWR from "swr";

function Profile() {
  const { data } = useSWR("/api/user", fetcher, { suspense: true });
  return <div>hello, {data.name}</div>;
}

function App() {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Profile />
    </Suspense>
  );
}

2.9 Error Handling

onErrorRetry can handle errors uniformly, including retrieving the number after the error occurs, and so on:

useSWR(key, fetcher, {
  onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
    if (retryCount >= 10) return;
    if (error.status === 404) return;

    // retry after 5 seconds
    setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000);
  }
});
  1. Local mutation
  2. suspense mode
  3. error handling
// conditionally fetch
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);

// ...or return a falsy value
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);

// ... or throw an error when user.id is not defined
const { data } = useSWR(() => "/api/data?uid=" + user.id, fetcher);

3 Intensive reading

3.1 Global Configuration

In the Hooks scenario, global configuration can be achieved by wrapping a layer of custom contexts.

First SWRConfig is essentially a custom Context Provider:

const SWRConfig = SWRConfigContext.Provider;

Merge the current configuration and global configuration in useSWR, and get the global configuration through useContext:

config = Object.assign({}, defaultConfig, useContext(SWRConfigContext), config);

Some details of 3.2 useSWR

You can see more details from the source, and useSWR is really much better than calling fetch manually.

Compatibility

The useSWR body code is in useEffect, but in order to advance the request time, it is placed before the UI rendering (useLayoutEffect) and compatible with the server-side scenarios:

const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect;

Non-blocking

When the browser is idle, the request function is wrapped by requestIdleCallback:

window["requestIdleCallback"](softRevalidate);

softRevalidate is a revalidate with weighting turned on:

const softRevalidate = () => revalidate({ dedupe: true });

That is, duplicates with the same parameters within the default 2s will be cancelled.

performance optimization

Because swr Data states such as data, isValidating, and so on are managed separately using useState:

let [data, setData] = useState(
  (shouldReadCache ? cacheGet(key) : undefined) || config.initialData
);
// ...
let [isValidating, setIsValidating] = useState(false);

data is often updated with isValidating when the fetch state changes. To trigger an update only once, unstable_batchedUpdates is used to merge the updates into one:

unstable_batchedUpdates(() => {
  setIsValidating(false);
  // ...
  setData(newData);
});

There are other solutions, such as using useReducer to manage data to achieve the same performance results.

3.3 Initial Cache

When the page switches, you can temporarily replace the fetch result with the last data, that is, initialization data is taken from the cache:

const shouldReadCache = config.suspense || !useHydration();

// stale: get from cache
let [data, setData] = useState(
  (shouldReadCache ? cacheGet(key) : undefined) || config.initialData
);

During the initialization of useSWR, the above code uses Hydration to indicate whether it was initially loaded:

let isHydration = true;

export default function useHydration(): boolean {
  useEffect(() => {
    setTimeout(() => {
      isHydration = false;
    }, 1);
  }, []);

  return isHydration;
}

3.4 Supports suspense

Suspense is divided into two functions: loading code asynchronously and loading data asynchronously, and now refers to the ability to load data asynchronously.

Suspense requires code suspended, which throws a Promise exception that can be caught and renders the component after that Promise has finished.

Core code in this section throws out the number of Promise s:

throw CONCURRENT_PROMISES[key];

Wait until the number is complete before returning to the structure defined by the useSWR API:

return {
  error: latestError,
  data: latestData,
  revalidate,
  isValidating
};

If there is no throw above, the component will be rendered before the count is taken, so throwing the requested Promise enables the request function to support Suspense.

3.5 Dependent Requests

I flipped over the code and found no logic to deal with circular dependencies in particular. Then I read the official documentation and realized that it was through try/catch + onErrorRetry mechanism that dependency fetches were achieved.

Look at this code:

const { data: user } = useSWR("/api/user");
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);

How do I do smart requests in order of dependency?Let's look at the main logic of the useSWR function:

try {
  // Set isValidation to true
  // Number, onSuccess callback
  // Set isValidation to false
  // Set Cache
  // unstable_batchedUpdates
} catch (err) {
  // Undo fetches, caches, etc.
  // Call onErrorRetry
}

Visible fetch logic is occupied by try, then user.id in useSWR("/api/user") without Ready must throw an exception, then automatically enter onErrorRetry logic to see if user.id has Ready the next time fetch.

So when is it your turn to take the next count?This is the time:

const count = Math.min(opts.retryCount || 0, 8);
const timeout =
  ~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval;

The retry time basically increases exponentially by 2.

therefore swr Numbers will be taken in parallel first, and dependent fetches will be retried until the upstream Ready.This simple pattern slightly loses some performance (not retrying downstream in time after an upstream Ready), but is a clever solution, and maximizing parallelism makes most scenarios perform better than handwriting.

4 Summary

The author left two questions for students who read this article carefully:

  • What do you think about Hooks or data streams?
  • Is there any better way to improve swr's method of resolving dependent numbers?

The discussion address is: Read Hooks Numbering - swr Source. Issue #216. dt-fe/weekly

If you want to participate in the discussion, please click here , with new themes every week, released on weekends or Mondays.Front End Intensive Reading - helps you filter the contents of your profile.

Focus on front-end reading WeChat Public Number

<img width=200 src="https://img.alicdn.com/tfs/TB...;>

Copyright Notice: Free Reproduction - Non-Commercial - Non-Derived - Preserve Signature ( Creative Sharing 3.0 License)

Topics: Javascript React axios JSON network