Last active
July 4, 2021 19:25
-
-
Save dasblitz/6f8841bc71f1d82f3ce7214a21ad64db to your computer and use it in GitHub Desktop.
React createFetcher explained
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This gist aims to explain how it's possible that async functions inside React | |
// using createFetcher(Promise).next(key) can work. | |
// A possible implementation of the new createFetcher function | |
// as shown by https://twitter.com/jamiebuilds/status/969169357094842368 | |
// @param method, should be a function returning a Promise. | |
// @returns an object with a property 'read', used to read values from the resolved 'cache'. | |
const createFetcher = function(method) { | |
// First create a Map for the resolved values. | |
const resolved = new Map() | |
return { | |
// The read function accepts a key that is used as a lookup on the resolved 'cache'. | |
read: (key) => { | |
// If no result is found it throws with a Promise. | |
if (!resolved.has(key)) { | |
throw method().then(val => resolved.set(key, val)) | |
} | |
// Otherwise return the value from the cache. | |
return resolved.get(key); | |
} | |
} | |
} | |
// Our own fetcher method, this can be any function returning a promise (like fetch) | |
const fetcherMethod = () => new Promise((resolve, reject) => { | |
setTimeout(() => { | |
resolve('hi from the future') | |
}, 3000) | |
}) | |
// Now let's create a few components with the same interface as a React component. | |
// For the sake of simplicity I'll just created them as POJO's. | |
// Create a fetcher using our fetcherMethod | |
const fetcher = createFetcher(fetcherMethod) | |
// The first component will be our async component using fetcher() | |
const asyncComponent = { | |
state: { | |
// Let's create a unique id that will be used | |
// as a key for our cache Map. | |
id: Symbol('test') | |
}, | |
render: function () { | |
// This is the really interesting part: calling fetcher.read() | |
// will throw for as long as our fetcherMethod is unresolved | |
const value = fetcher.read(this.state.id) | |
return `<p>${value}</p>` | |
} | |
} | |
// Here is another component with a normal render function | |
const syncComponent = { | |
render: function () { | |
return `<p>I'm so in sync</p>` | |
} | |
} | |
// And for the sake of completeness a component that throws an error in render. | |
// The error should be handled by componentDidCatch | |
const errorComponent = { | |
componentDidCatch: function () { | |
return `<p>Something went wrong!</p>` | |
}, | |
render: function () { | |
throw new Error('This component threw an error') | |
} | |
} | |
// Normally React will build your component tree but for the sake of simplicity | |
// we'll just create an array with our components in it and pretend | |
// it's React's component tree model | |
const componentTree = [asyncComponent, syncComponent, errorComponent] | |
// The render function of each React.Component returns html that should be rendered. | |
// We'll use this array to store those return values | |
const readyForDOMRenderTree = [] | |
// Now, every time state or props change for a component, React re-renders | |
// the component tree. There are all kinds of smart things going on | |
// to make sure it only re-renders the components that have actually changed. | |
// I'm not that smart, so I'm just going to loop over all components | |
// and call render() on each one of them: | |
reactComponentRenderer = function() { | |
componentTree.forEach((component) => { | |
try { | |
// If component.render() succeeds we're all good | |
readyForDOMRenderTree.push(component.render()) | |
} catch (e) { | |
// If component.render() fails we need to check why. | |
// Remember that our createFetcher function actually throws with a Promise | |
// if there's no resolved value yet. | |
// Let's check if our error is a Promise with a poor man's promise check: | |
if (e.then) { | |
// If it is a Promise we'll just wait for it to resolve | |
// and call component.render() again. | |
e.then(() => { | |
// By now fetcher.read() inside component.render() | |
// will return a value from the cache. | |
readyForDOMRenderTree.push(component.render()) | |
// Since our readyForDOMRenderTree changed we need to re-render | |
renderDOMTree() | |
}).catch(e => console.log('error', e)) | |
} else { | |
// If it's a regular error we just call componentDidCatch | |
component.componentDidCatch(e) | |
} | |
} | |
}) | |
// Update the DOM tree | |
renderDOMTree() | |
} | |
// Renders the DOM tree, well actually I'm just logging | |
// whatever the components render functions return :) | |
renderDOMTree = function() { | |
readyForDOMRenderTree.forEach(component => { | |
console.log(component) | |
}) | |
} | |
// With all that in place, let's start the render: | |
reactComponentRenderer() | |
// outputs: | |
// <p>I'm so in sync</p> | |
// undefined | |
// (after 3 seconds:) | |
// <p>I'm so in sync</p> | |
// <p>Something went wrong!</p> | |
// <p>hi from the future</p> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment