I don’t think I know React any better than the people who wrote React, so I won’t give you any more philosophical thoughts about React and discuss the pros and cons of the traditional way. Please read this official document by yourself. This article only introduces the usage and principles of React cache.

Suspense

Suspense is not limited to loading asynchronous components, but has a more general scope. In order to better understand the react-cache principle, we need to know how Suspense works in advance.

Error Boundaries

The low-level implementation of Suspense relies on the Error Boundaries component. As we know from the description, Error Boundaries are components that can easily be generated. Any class component that implements the static getDerivedStateFromError() static method is an error bound component.

The main purpose of the Error boundary component is to catch errors thrown by children (excluding itself), as shown in the following example

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so that the next rendering can display the degraded UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also report error logs to the server
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can customize the degraded UI and render it
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; }}Copy the code

Error boundaries allow us to render alternate UIs instead of error UIs when sub-component trees crash, so what does this have to do with Suspense?

An Error bound component can catch anything thrown by a child component (excluding itself) with an Error ****. Suspense can be thought of as a special error-bound component where Suspense suspends the Promise rendering fallback UI when it catches a Promise thrown by a child component and rerenders when it’s Resolved.

react-cache

React cache is experimental. It’s a new way of thinking about how react gets data

Let’s take a quick look at how it works

// app.jsx

import { getTodos, getTodoDetail } from './api';
import { unstable_createResource as createResource } from 'react-cache';
import { Suspense, useState } from 'react';

const remoteTodos = createResource(() = > getTodos());
const remoteTodoDetail = createResource((id) = > getTodoDetail(id));

const Todo = (props) = > {
  const [showDetail, setShowDetail] = useState(false);

  if(! showDetail) {return (
      <li onClick={()= >{ setShowDetail(true); }} ><strong>{props.todo.title}</strong>
      </li>
    );
  }

  const todoDetail = remoteTodoDetail.read(props.todo.id);

  return (
      <li>
          <strong>{props.todo.title}</strong>
          <div>{todoDetail.detail}</div>
      </li>
  );
};

function App() {
  const todos = remoteTodos.read();

  return (
    <div className="App">
      <ul>
        {todos.map(todo => (
          <Suspense key={todo.id} fallback={<div>loading detail...</div>} ><Todo todo={todo} />
          </Suspense>
        ))}
      </ul>
    </div>
  );
}

export default App;

// index.jsx

ReactDOM.render(
  <React.StrictMode>
    <Suspense fallback={<div>fetching data...</div>} ><App />
    </Suspense>
  </React.StrictMode>.document.getElementById('root'));Copy the code

Results demonstrate

API

  • unstable_createResource

React-cache has two apis, but the core function is unstable_createResource, which we use to create data pull functions remoteTodos and remoteTodoDetail for Suspense.

Unstable_createResource takes two arguments, the first mandatory and the second optional. The first argument is a function whose return value must be a Promise. The second argument is optional and takes a hash function. The main purpose of this argument is to distinguish between data caching in the case of complex inputs.

  • unstable_setGlobalCacheLimit

Used to set the global react-cache cache limit

The principle of

Since there is very little code for react-cache, let’s look directly at the source implementation

export function unstable_createResource<I.K: string | number.V> (fetch: I => Thenable
       
        , maybeHashInput? : I => K,
       ) :Resource<I.V> {
  const hashInput: I= >K = maybeHashInput ! = =undefined ? maybeHashInput : id= > id;
		// The default hash function is sufficient for simple input
  const resource = {
    read(input: I): V {
      const key = hashInput(input);
      // Generates a key corresponding to a particular input
      const result: Result<V> = accessResult(resource, fetch, input, key);
      switch (result.status) {
        case Pending: {
          const suspender = result.value;
          throw suspender;
        }
        case Resolved: {
          const value = result.value;
          return value;
        }
        case Rejected: {
          const error = result.value;
          throw error;
        }
        default:
          // Should be unreachable
          return (undefined: any);
      }
    },

    preload(input: I): void {
      // react-cache currently doesn't rely on context, but it may in the
      // future, so we read anyway to prevent access outside of render.
      readContext(CacheContext);
      constkey = hashInput(input); accessResult(resource, fetch, input, key); }};return resource;
}
Copy the code

When you execute unstable_createResource it returns an object with.read and.preload methods..preload is simple, but let’s focus on.read

  • When called in the React component.read()
  • throughmaybeHashInput()The generated key checks the cache
  • Return if there is synchronization
  • If not, the data pull function passed in when the object is created is executed to generate onePromiseAt the same timethrow
  • Nearest ancestorSuspenseThe component catches thisPromise, suspend, render fallback UI
  • whenPromise resolvedLater,react-cacheThere’s an internal wiretap.PromiseWill,resolvedTo the cache
  • At the same timeSuspenseComponents are also foundPromise resolved, re-render the child components
  • The child component executes again.read()Method, check the cache by key, find that it has been cached, sync back, and render, and the whole process ends

other

The react-cache internal cache mechanism uses the LRU strategy, which I won’t talk about here, but the most important feeling is that we are writing in a synchronous way, that is, we think the data is already there, and we are just reading, not pulling.

const Todo = (props) = > {
  const [showDetail, setShowDetail] = useState(false);

  if(! showDetail) {return (
      <li onClick={()= >{ setShowDetail(true); }} ><strong>{props.todo.title}</strong>
      </li>
    );
  }

  const todoDetail = remoteTodoDetail.read(props.todo.id);

  return (
      <li>
          <strong>{props.todo.title}</strong>
          <div>{todoDetail.detail}</div>
      </li>
  );
};
Copy the code

Instead of thinking about how to use useEffect, write components in a natural way:

  • Read the data
  • To render the UI

Rather than

  • Rendering component
  • Considering the loading condition
  • Trigger life cycle
  • Perform data pull
  • Consider the component state at pull time
  • Set to state
  • Rerender the UI

Let’s look at the code above. What would it look like if you wrote it the traditional way

const Todo = (props) = > {
  const [showDetail, setShowDetail] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [todoDetail, setTodoDetail] = useState(null);

  useEffect(() = > {
    if (showDetail) {
      setIsLoading(true);
      getTodoDetail(props.todo.id)
        .then(todoDetail= > setTodoDetail(todoDetail))
        .finally(() = > {
          setIsLoading(false);
        })

    }
  }, [showDetail, props.todo.id]);

  if (isLoading) {
    return <div>loading detail...</div>;
  }

  if(! showDetail) {return (
      <li onClick={()= >{ setShowDetail(true); }} ><strong>{props.todo.title}</strong>
      </li>
    );
  }

  if (todoDetail === null) return null;
  return (
      <li>
          <strong>{props.todo.title}</strong>
          <div>{todoDetail.detail}</div>
      </li>
  );
};
Copy the code

For those who want to actually play with it, the sample code has been pushed to the Github repository

WARN

If you try to download react-cache, you will most likely encounter TypeError: Cannot read property ‘readContext’ of undefined, Cannot read property ‘readContext’ of undefined, Cannot read property ‘readContext’ of undefined However, since the context-related code is only in the TODO phase and has no actual effect, there are two solutions

See these two issues for details

  • Cannot ready property ‘readContext’ of undefined #14575
  • React-cache alphas don’t work with 16.8+ #14780

The solution

  • They will bereact-cachenode_modulesI’m gonna copy it inside, and I’m gonna manuallyreadContextComment out the relevant code
  • Built directly using source code from the Github repositoryreact-cache, you can write the following code topackage.json“, and then execute
    • The build process will use Java, so remember to install it
"postinstall": "git clone https://github.com/facebook/react.git --depth=1 && cd react && yarn install --frozen-lockfile && npm run build react-cache && cd .. && npm i $(npm pack ./react/build/node_modules/react-cache) && rm -rf react react-cache-*.tgz"
Copy the code