This is a technology sharing speech I made in the group. I didn’t change much and just shared it.

Suspense

preface

React 16.6 adds a component that shows loading status during lazy loads.

const ProfilePage = React.lazy((a)= > import('./ProfilePage')); // Lazy-loaded

// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>
Copy the code

React later realized that Suspense for Data Fetching can also be used to wait for lazy load promises and for other things, such as requests for Data promises, so it has Fetching feature. It is still an experimental feature, and the documentation on the official website is mainly for library developers (for example, SWR is now available for Suspense). For most users, the React documentation still recommends using hooks to request data.

What is? Not what? What can you do?

Suspense is a “wait” mechanism that acts as a component that lets you explicitly state what should be rendered while waiting.

Suspense is not a request library. It itself is not responsible for creating and managing requests.

Suspense allows the library to be deeply integrated with React. The library can “tell” React that it is waiting for a response, without the need for users to manually manage the loading state.

How does it work?

function Post({ id }) {
  
  const post = getPost(id);
  
  return <article>{post}</article>;
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>} ><Post id={1} />
    </Suspense>
  );
}
Copy the code

The getPost(id) is… Synchronized?

You might find this intuitive but a little confusing, but that’s where the data comes in.

The way data is obtained

You may have seen this in the React documentation, but I’ll explain it in a slightly different way than the official documentation. There are two ways to get data:

  • Fetch after rendering (traditional way)
  • Render is Getting (Suspense way)
Post-render fetch

Fetch after render is the way we most often write, fetching data in componentDidMount or its equivalent useEffect

function Post({ id }) {
  const [post, setPost] = useState(null);
  
  useEffect((a)= > {
    fetchPost(id).then(data= >setPost(data)); } []);if(! post) {return <div>Loading...</div>;
  }
  
  return <article>{post}</article>;
}
Copy the code

In this way, the component makes the first render, and when it’s done (the componentDidMount phase) the request starts. Let’s look at another one.

Render as fetch

function Post({ id }) {
  
  const post = getPost(id); // just works
  
  return <article>{post}</article>;
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>} ><Post id={1} />
    </Suspense>
  );
}
Copy the code

In this way, the rendering is performed at the same time as the getPost request is issued. At the moment it looks like the render fetch approach is just a little bit earlier than the render fetch approach, but there’s a lot more to it than that.

The difference between the two approaches

Give up the state

You’ll notice that there is no component internal state in the render-and-fetch scheme. That’s why it’s clean, crisp and intuitive at first sight. The UI is really a mapping of its props, not a mapping of a state.

But is there only such utopian distinction as purer? Not really. Ditching state also avoids some of the problems you might have encountered.

1. Waterfall

Request waterfall. Look at the following example using internal state:

function Profile() {
  const [user, setUser] = useState(null);
  
  useEffect((a)= > {
    fetchUser().then(data= > setUser(data));
  }, [])
  
  if(! user) {return <div>Loading profile...</div>;
  }
  
  return (
    <div>
      <h1>{user.name}</h1>
      <Posts />
    </div>
  );
}

function Posts() {
  const [posts, setPosts] = useState();
  
  useEffect((a)= > {
    fetchPosts().then(data= >setPosts(data)); } []);if(! posts) {return <div>Loading posts...</div>;
  }
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
Copy the code

In this example, posts are not fetched until the user responds.

2. Race condition

Consider the following post-render fetch example:

function Post({ id }) {
  const [post, setPost] = useState(null);
  
  useEffect((a)= > {
    fetchPost(id).then(data= > setPost(data));
  }, [id]);
  
  if(! post) {return <div>Loading...</div>;
  }
  
  return <article>{post}</article>;
}

function App() {
  const [id, setId] = useState(1);
  return (
    <div>
      <button onClick={()= > setId(currentId => currentId + 1)}>Next post</button>
      <Post id={id} />
    </div>
  );
}
Copy the code

In this example,

has a state that represents the id of the article you are currently looking at, and a button that modifies this state. Suppose we clicked the button twice quickly to change the ID to 2 and then to 3. which article finally showed up? Since the response order of asynchronous requests is uncertain, there is no guarantee that we will see article 3 when ID is 3. Without the extra state, everything is smooth and correct.

Get the data earlier

In another case, rendering is fetching and can fetch data earlier. Didn’t we just talk about getting the data earlier? This is a different situation.

Consider the following example, again a button to change the ID, we use the render fetch component:

// Post.js
function Post({ post }) {
  return <article>{post}</article>;
}

// App.js
const Post = React.lazy((a)= > import('./Post'));

function App() {
  const [id, setId] = useState(1);
  
  return (
    <div>
      <button onClick={()= > setId(currentId => currentId + 1)}>Next post</button>
      <Post post={getPost(id)} />
    </div>
  );
}
Copy the code

In this example, we can get both the post content and the < POST > code at the same time, rather than waiting for the < POST > download to complete before starting to request the post content.

thisgetPost(id)Is that… Synchronized?

To come back to this problem, we have talked so much about “rendering is Fetching” which is recommended in Suspense for Data Fetching, but how to implement this!

function Post({ id }) {
  
  const post = getPost(id);
  
  return <article>{post}</article>;
}
Copy the code

Although the implementation isn’t found in Suspense>’s source code, we have a rough idea of how it works in the official sample source code:

// This is not the source code for the sample, but it is the internal meaning
let post;
let promise;

function getPost(id) {
  if (post) return post;
  if (promise) throw promise;
  
  promise = fetchPost(id).then(data= > post = data);
  throw promise;
}
Copy the code

Surprisingly, the getPost(ID) method may return different results multiple times. It either returns the contents of the POST or throws a Promise. After experimenting, it turns out that when your component throws a Promise, Suspense> assumes that the request is in progress and renders the fallback. The React scheduler then duly tries to render the component again.

But that’s a weird way to write it

That’s why the React documentation for this section is for request library authors, not React users.

SWR

preface

These scenarios and treatments are sometimes encountered in daily development:

The same URL (and parameters), caches the last result

Set some constants to identify different requests, and the results of the requests are cached in redux based on that identity, and fetched from Redux when fetched. This improves the responsiveness of the page and reduces the time you see empty pages.

In fact, SWR has done it for you.

What is? Not what?

SWR, named after the abbreviation for stale-while-revalidate, is a cache-control extension described in HTTP RFC 5861. The cached response is returned first, while a new response is requested in the background to speed up the response and reduce the wait time. Despite its name, SWR simply borrows its concept and the actual implementation has nothing to do with the stale-while-revalidate directive.

SWR is not exactly a “request library.” It is mainly aimed at the management of data acquisition, and data update, delete, it does not care.

SWR is a React Hooks library for remote data fetching.

usage

import useSWR from 'swr';

// fetch current user
const { data } = useSWR('/api/user');
Copy the code

You’ve probably seen examples of SWR and thought this is just like a normal request library hook. So let’s take a closer look at what SWR is.

API

const { data, error, isValidating, mutate } = useSWR(key, fetcher, options);
Copy the code
parameter
  • key: Identifies the request
  • fetcher: An asynchronous method that returns request data
  • options: More configuration items
The return value
  • data: identifieskeyCorresponding data
  • error: An error was thrown during data loading
  • isValidating: Whether data is being requested or revalidated
  • mutate(data? , shouldRevalidate): Used to modify cached data

useSWR

Data Fetching

import useSWR from 'swr';

async function fetchCurrentUser() {
  const { data } = await axios('/api/user');
  return data;
}

function Profile() {
  const { data: user } = useSWR('currentUser', fetchCurrentUser);
  
  if(! user) {return <div>Loading profile...</div>;
  }
  
  return <div>{user.name}</div>;
}
Copy the code

The useSWR method returns you cached data with the id currentUser, and fetchCurrentUser retrieves the updated data and stores it into the id.

Note that SWR is not a request library; it does not receive the URL as the first parameter and send the request to its address. As you can see from the example above, a key is not a URL, but is used to identify a resource that you need. However, rather than naming each of your resources, it’s easier and more accurate to use urls as identifiers.

const { data: user } = useSWR('/api/user', fetchCurrentUser);
Copy the code

The SWR passes the key as a parameter to the fetcher. Since we used the URL as the key, encapsulating a generic request method as a fetcher is also a good option.

async function request(url) {
  const { data } = await axios(url);
  return data;
}

const { data: user } = useSWR('/api/user', request);
Copy the code

Add in SWR’s support for global configuration default fetcher, and you end up with

const { data: user } = useSWR('/api/user');
Copy the code

Emmm, there’s an inside smell. It may seem like SWR is a request library, but you need to know that it is not 😂. This is important and will help you understand more about the use of SWR.

Conditional Fetching & Dependent Fetching

If the key is passed null, no data is requested.

const { data: posts } = useSWR(user ? `/api/users/${user.id}/posts` : null);
Copy the code

If you think this is counterintuitive, why does a NULL URL mean no request is sent? Then you fall into the trap mentioned above. We know that key is an identifier for fetching data, and passing in null means I’m not fetching data right now.

If it’s still hard to get around, let me write it a different way

async function fetchUserPosts(key) {
  const userId = key.match(/^posts by user (\d+)$/) [1];
  
  const { data } = await axios(`/api/users/${userId}/posts`);
  return data;
}

const { data: posts } = useSWR(user ? `posts by user ${user.id}` : null, fetchUserPosts);
Copy the code

You shouldn’t be confused by this point.

Now that we’ve worked so hard to understand this concept, let’s move on to the use of SWR. We just saw that a key can be a string, but a key can also accept a number of other types. For example, when it is a function, SWR uses its return value as the storage identifier.

// function as key
const { data: user } = useSWR((a)= > '/api/user');

// conditional
const { data: posts } = useSWR((a)= > user ? `/api/users/${user.id}/posts` : null);
Copy the code

SWR has a special handling of the function key that makes dependent fetching more beautiful and smooth:

const { data: user } = useSWR((a)= > '/api/user');
const { data: posts } = useSWR((a)= > `/api/users/${user.id}/posts`);
Copy the code

The posts key function throws an exception when the user is not finished loading. The SWR will not load the data, as if the key value were null. Once the user load is complete, posts will start loading.

Multiple Arguments

In addition to functions, a key can also receive an array. Like the DEps array of useCallback or useEffect, a key array whose values are equal in sequence is the same identifier. The values of the key array are passed in turn to the fetcher as arguments.

async function fetchUserPosts(_, userId) {
  return axios(`/api/users/${userId}/posts`);
}

const { data: posts } = useSWR(['posts by user', userId], fetchPostsByUser);
Copy the code

Mutate

Manually Revalidate

The previous examples were all getting the data for the first time, so how do you manually get the data for an identity again? SWR provides the Mutate method.

import useSWR, { mutate } from 'swr';

function Profile () {
  const { data: user } = useSWR('currentUser');
  
  return (
    <div>
      <h2>{user.name}</h2>
      <button onClick={()= > {
        mutate('currentUser')
      }}>
        Refresh
      </button>
    </div>)}Copy the code

When mutate(key) is called, SWR requests its corresponding data again and updates the cache. Note that data: user is always in the cache.

Alternatively, you can simply use the mutate method returned in useSWR to save writing the key again.

function Profile () {
  const { data: user, mutate } = useSWR('currentUser');
  
  return (
    <div>
      <h2>{user.name}</h2>
      <button onClick={()= > {
        mutate()
      }}>
        Refresh
      </button>
    </div>)}Copy the code

Mutation

We said earlier that SWR itself does not update data. But he does allow us to modify the cached data. Again, the mutate method is used, which takes a second argument to modify the cached data before rerequesting it.

function Todo({ id }) {
  const { data: todo, mutate } = useSWR((a)= > `/api/todos/${id}`);
  
  async function markTodoAsDone() {
    await axios.patch(`/api/todos/${todo.id}`, { done: true}); mutate({ ... todo,done: true }); // Update cached data while rerequesting data
  }
  
  if(! todo) {return <div>Loading...</div>;
  }
  
  return (
    <div>
      {todo.content}
      <button onClick={markTodoAsDoen}>
        Mark as done
      </button>
    </div>
  );
}
Copy the code

Many times, requests to update data will return the updated resource directly, and we may want to update the cache without having to revalidate the resource. Mutate takes a third parameter to allow control over whether to revalidate the resource.

const { data: updated } = await axios.patch(`/api/todos/${todo.id}`, { done: true });
mutate(updated, false); // shouldRevalidate=false shouldRevalidate=false
Copy the code

Alternatively, you can update the cache with promises, which means you don’t need to revalidate.

function updateTodo(id, data) {
  const { data: updated } = await axios.patch(`/api/todos/${todo.id}`, data);
  return updated;
}

mutate(updateTodo(todo.id, { done: true }));
Copy the code

Optimistic UI

You’ve probably heard of Optimistic UI. It describes how when I request to update/delete data, I can assume that the request is successful and update the UI accordingly; Wait for the request to complete and then update the UI based on the actual results to improve page response times. Combined with the above use of mutate, you actually get Optimistic UI.

mutate({ ... todo,done: true }, false); // Update the local cache optimistically without revalidation
mutate(updateTodo(todo.id, { done: true })); // Update the cache with the request result
Copy the code

More than that

The above has just covered some common uses of SWR’s apis, but SWR is capable of more than that.

Focus Revalidation

When you refocus on the page, SWR automatically revalidates the data. For example, if you open an app in two tabs and change your avatar on one TAB, when you switch to the other TAB, the new avatar is already loaded. This mechanic doesn’t require you to write any extra code.

Refetch on Interval

Enable periodic revalidation with a configuration item. This doesn’t sound too hard to do on your own, but don’t forget that the SWR takes care of it for you, restarting the timer after you manually update the data and pausing the timer when the page goes off-screen.

From the request to the SWR

The key of reuse

When I was developing my own applications, I used to encapsulate the requests in API or business logic into functions for reuse.

import { fetchUsers, updateUser } from '@/services/users';

function UserList() {
  
  const [users, setUsers] = useState();
  
  useEffect((a)= >{(async() = > {const { data } = await fetchUsers();
      setUsers(data);
    })();
  }, []);
  
  return (
    <UserTable
   		users={users}
     	onEditUser={openEditUserDrawer}
      // .
   	/>
  );
}
Copy the code

In an application that uses SWR, the key should be managed for reuse.

import * as resources from '@/constants/swr';

function UserList() {
  
  const { data: users } = useSWR(resources.users);
  
  return (
    <UserTable
   		users={users}
     	onEditUser={openEditUserDrawer}
      // .
   	/>
  );
}
Copy the code

As you can see from the previous usage of Conditional Fetching and Data Fetching, when a function is used as a key, it depends on variables/constants in the component, which requires reusable keys to allow arguments to return the correct key. As a result, some keys are strings and some functions, making managing keys less elegant.

export const user = '/api/user';
// conditional
export const userPosts = user= > user ? `/api/users/${user.id}/posts` : null;
// dependent
export const userPosts = user= >() = >`/api/users/${user.id}/posts`;

function App() {
  const { data: user } = useSWR(resources.users);
  const { data: posts } = useSWR(resources.userPosts(user));
}
Copy the code

The fetcher reuse

When you handle the reuse of keys, you will find that they do not completely replace the asynchronous method of reuse. You still need to manage fetcher, because global fetcher doesn’t handle all of your keys. In the Multiple Arguments example, the request parameters, such as query parameters, should be expanded into arrays, which requires fetcher’s separate support.

export const users = (page, pageSize, orderColumn, orderType, groupId) = > ['/api/users', page, pageSize, orderColumn, orderType, groupId];

async function queryUsers(url, page, pageSize, orderColumn, orderType, groupId) {
  return axios(url, {
    page, pageSize, orderColumn, orderType, groupId
  });
}

function UserList() {
  const { data: users } = useSWR(resources.users(page, pageSize, orderColumn, orderType, groupId/ *,... * /), queryUsers);
  
  return <UserTable users={users} />;
}
Copy the code

Here the FETcher URL parameter is also oddly coupled.

When you finally manage to reuse keys and fetcher, you find that the @/services directory has not disappeared, and the asynchronous functions that update resources are still there. At the end of the day, this reuse doesn’t help you write your application code more efficiently or elegantly.

Do you have workaround?

Not without it. Let’s look at Multiple Arguments for parameter list coupling. Why is this a problem? Because there’s no way to know what the parameters are in the key array. How to solve this, is not to separate the parameters. If an object is passed, the update will be triggered repeatedly.

export const users = (params) = > ['/api/users'.JSON.stringify(params)];

async function queryUsers(url, params) {
  return axios(url, { params: JSON.parse(params) });
}

function UserList() {
  const { data: users } = useSWR(resources.users(params), queryUsers);
  
  return <UserTable users={users} />;
}
Copy the code

Use json.stringify to turn the object into a string. You notice that the queryUsers parameter list is completely decouple from the caller, and that this part of the mechanism for handling params can be integrated into the global fetcher, no longer requiring a separate fetcher.

export const users = (params) = > ['/api/users'.JSON.stringify(params)];

function UserList() {
  const { data: users } = useSWR(resources.users(params));
  
  return <UserTable users={users} />;
}
Copy the code

Smart you may have noticed that my key to a JSON. The stringify, fetcher again in JSON. The parse, end up with a bigger spell it again after the stringify axios to URL, isn’t it a bit long-winded?

We can use QS instead of json.stringify, so that our key array forms a nice [URL, QueryString] paradigm, which is the best way to distinguish resources.

async function fetcher(url, querystring) {
  return axios(`${url}${querystring}`);
}

export const users = (params) = > ['/api/users', qs.stringify(params)];

function UserList() {
  const { data: users } = useSWR(resources.users(params));
  
  return <UserTable users={users} />;
}
Copy the code

But using paradigms comes at a slight cost. The same URL but different parameters will be considered as different resources, so when we modify query parameters in the table page, the original query results cannot be left in the interface.

Suspense

After all Suspense and SWR, what is the relationship between them? Let’s go back to the question:

The getPost(id) is… Synchronized?

function Post({ id }) {
  
  const post = getPost(id);
  
  return <article>{post}</article>;
}
Copy the code

As you may have noticed, SWR’s APIS implement this paradigm in Suspense, letting you get rid of state for managing asynchronous data. Also, SWR supports Suspense mode through configuration items:

function Post({ id }) {
  
  const { data: post } = useSWR(`/api/posts/${id}`, { suspense: true });
  
  return <article>{post}</article>;
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>} ><Post id={1} />
    </Suspense>
  );
}
Copy the code

When you turn on Suspense: True, SWR will raise a Promise in suspense at first data loading, triggering a fallback in suspense.

conclusion

Suspense and SWR are not meant to promote these two technologies and get people to use them. They are taken together because they introduce an update in thinking about asynchronous data use. Sometimes a problem is no longer a problem if you look at it from another Angle.

Refer to the link

  • Suspense for Data Fetching
  • zeit/swr: React Hooks library for remote data fetching
  • “useSWR” – React Hooks for Remote Data Fetching