Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
June 5, 2019 09:21 pm GMT

React Suspense with the Fetch API

Dan Abramov, in response to a React developer asking why Suspense was not responding to the fetch API:

From the legend Dan Abramov himself, we receive such gems as There is [no data fetching solution compatible with React Suspense] that exists yet, and [React Cache] will be the first one, and Suspense is limited to code splitting.

If I have one thing to tell Daniel Abra Cadabra Abramov, besides how impressed I am with his work, its this:

Lets reveal the magic behind the curtain that is React Suspense. For educational purposes, Ill cover how I created this package.

Shut Up and Give Me the Package!

If youre just here for solutions, I dont blame you. You can find fetch-suspense on NPM and the most extensive documentation of your life on the GitHub repository.

import useFetch from 'fetch-suspense';const MyComponent = () => {  // "Look! In the example! It's a fetch() request! It's a hook!"  //   "No! It's kind of like both at the same time."  const serverResponse = useFetch('/path/to/api', { method: 'POST' });  // The return value is the body of the server's response.  return <div>{serverResponse}</div>;};

How Does Suspense Work?

A lot of the new React features are built into the React library, as opposed to being external packages, due to the performance benefits of being tightly coupled to the engine that powers React, known as React Fiber.

Due to React Fibers direct integration with features such as Suspense and hooks, you cannot create a verbatim copy of Suspense in React 16.5. However, you can probably make a less performant polyfill. Ill use some polyfill examples so that you can conceptualize what is happening with Suspense.

class Suspense extends React.Component {  constructor(props) {    super(props);    this.state = {      error: null    };  }  componentDidCatch(e) {    this.setState({ error: e });  }  render() {    if (this.state.error) {      return this.props.fallback;    }    return this.props.children;  }}/*<Suspense fallback={<Loading />}>  <ErrorThrower /></Suspense>*/

Here is ye olde class component: a fossil remnant of yonder days of React development. The componentDidCatch method is a method that fires whenever a child component throws an error. This allows you to replace uncaught JavaScript errors with a nice UI for your users or otherwise implement important logic during application errors.

What the above does is mounts Suspense. Since there is no error in the local state, the children of Suspense are mounted too. In this case, the <ErrorThrower /> component is mounted, and it throws an error.

That error bubbles up to the Suspense instance, where the componentDidCatch method receives it. It handles that error by saving it to its state, causing it to re-render.

Now that it has rendered with an error in its local state, it no longer renders its children prop, nor the <ErrorThrower /> devil-child as a result. Instead, it renders its fallback prop, which we have set to a nice <Loading /> modal.

This is how Suspense works now, except instead of throwing errors, JavaScript Promises are thrown. When Suspense catches a Promise, it re-renders, displaying the fallback prop instead of the children that previous threw a Promise. When the Promise resolves, it re-renders again; this time no longer displaying the fallback prop, and instead attempting to re-render the original children, under the presumption that the children are now ready to be rendered without throwing Promises around like theyre meaningless.

An implementation may look something like this:

class Suspense extends React.Component {  constructor(props) {    super(props);    this.state = {      promise: null    };  }  componentDidCatch(e) {    // Drake meme where he says no to errors here.    if (e instanceof Error) {      throw e;    }    // Drake meme where he says yes to promises here.    if (e instanceof Promise) {      this.setState({        promise: e      }, () => {        // When the promise finishes, go back to rendering the original children.        e.then(() => {          this.setState({ promise: null });        });      });    }    // This line isn't compatible with the Drake meme format.    else {      throw e;    }  }  render() {    if (this.state.promise) {      return this.props.fallback;    }    return this.props.children;  }}/*<Suspense fallback={<Loading />}>  <PromiseThrower /></Suspense>*/

Its important to note here that the original children attempted to render before the fallback occurred. It never succeeded.

How Does This Apply to Fetch Hooks?

What you should have gathered by now is that the fetch hook will need to throw Promises. So it does. That promise is conveniently the fetch request. When Suspense receives that thrown fetch request, it falls back to rendering its fallback prop. When that fetch request completes, it attempts to render the component again.

Theres just one little tricky dicky problem with thatthe component that threw the fetch request had only attempted to render, but did not succeed. In fact, it is not a part of the fallback at all! It has no instance. It never mounted. It has no state (not even a React hook state); it has no component lifecycle or effects. So when it attempts to render again, how does it know the response of this fetch request? Suspense isnt passing it, and itnot being instantiatedcannot have data attached to it.

Golly, How Do You Solve That Conundrum?

We solve it with memoization!

Like that fancy new React.memo feature?

Yes! (in concept)

No! (more literally)

It does not use React.memo, which memoizes React components based on their props. Instead, I use an array of infinite depth to memoize the parameters passed to fetch.

If a request comes in to fetch data that has been requested before (the second attempt to instantiate after the first attempt failed with a Promise), then it simply returns the data that eventually resolved from the first requests Promise. If this is a fresh request, then we fetch it, cache it in the memoization array, and throw the fetch Promise. By comparing the current request to all entries in the memoization array, we know if we have dispatched this request before.

const deepEqual = require('deep-equal');interface FetchCache {  fetch?: Promise<void>;  error?: any;  init: RequestInit | undefined;  input: RequestInfo;  response?: any;}const fetchCaches: FetchCache[] = [];const useFetch = (input: RequestInfo, init?: RequestInit | undefined) => {  for (const fetchCache of fetchCaches) {    // The request hasn't changed since the last call.    if (      deepEqual(input, fetchCache.input) &&      deepEqual(init, fetchCache.init)    ) {      // If we logged an error during this fetch request, THROW the error.      if (Object.prototype.hasOwnProperty.call(fetchCache, 'error')) {        throw fetchCache.error;      }      // If we received a response to this fetch request, RETURN it.      if (Object.prototype.hasOwnProperty.call(fetchCache, 'response')) {        return fetchCache.response;      }      // If we do not have a response or error, THROW the promise.      throw fetchCache.fetch;    }  }  // The request is new or has changed.  const fetchCache: FetchCache = {    fetch:      // Make the fetch request.      fetch(input, init)        // Parse the response.        .then(response => {          // Support JSON.          if (Object.prototype.hasOwnProperty.call(response.headers, 'Content-Type')) {            return response.json();          }          // Not JSON.          return response.text();        })        // Cache the response for when this component        //   attempts to render again later.        .then(response => {          fetchCache.response = response;        })        // Cache the error for when this component        //   attempts to render again later.        .catch(e => {          fetchCache.error = e;        }),    init,    input  };  // Add this metadata to the memoization array.  fetchCaches.push(fetchCache);  // Throw the Promise! Suspense to the rescue!  throw fetchCache.fetch;};

That Sounds Like a Memory Leak

It can be a feature or a bug!

But if you think its a bug in your project, you can invalidate the cache by providing a lifespan in milliseconds to the fetch request. Passing a third parameter (a number) to the useFetch hook will tell it to remove the metadata from the memoization array after that many milliseconds. We implement it as easily as so:

// NEW: lifespan parameterconst useFetch = (  input: RequestInfo,  init?: RequestInit | undefined,  lifespan: number = 0) => {  // ...  const fetchCache: FetchCache = {    fetch:      // Make the fetch request.      fetch(input, init)        .then( /* ... */ )        .then( /* ... */ )        .catch( /* ... */ )        // Invalidate the cache.        .then(() => {          // If the user defined a lifespan,          if (lifespan > 0) {            // Wait for the duration of the lifespan,            setTimeout(              () => {                // Find this fetch request and kill it                //   from the memoization array.                const index = fetchCaches.indexOf(fetchCache);                if(index !== -1) {                  fetchCaches.splice(index, 1);                }              },              lifespan            );          }        }),    // ...  };  // ...};// ...

When the fetch has completed, and weve updated the metadata, tick-tock. Its important that the lifespan timer occurs after the catch of the Promise, because we want it to set even if an error occurred.

Conclusion

When Dan Abramov tells you that you cant do something, you do it.

If you liked this article, feel free to give it a heart or a unicorn. Its quick, its easy, and its free! If you have any questions or relevant great advice, please leave them in the comments below.

To read more of my columns, you may follow me on LinkedIn, Medium, and Twitter, or check out my portfolio on CharlesStover.com.


Original Link: https://dev.to/charlesstover/react-suspense-with-the-fetch-api-374j

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To