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()
后 - through
maybeHashInput()
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 one
Promise
At the same timethrow
- Nearest ancestor
Suspense
The component catches thisPromise
, suspend, render fallback UI - when
Promise
resolved
Later,react-cache
There’s an internal wiretap.Promise
Will,resolved
To the cache - At the same time
Suspense
Components 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 be
react-cache
从node_modules
I’m gonna copy it inside, and I’m gonna manuallyreadContext
Comment out the relevant code - Built directly using source code from the Github repository
react-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