Skip to content

Instantly share code, notes, and snippets.

@dasblitz
Last active July 4, 2021 19:25
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dasblitz/6f8841bc71f1d82f3ce7214a21ad64db to your computer and use it in GitHub Desktop.
Save dasblitz/6f8841bc71f1d82f3ce7214a21ad64db to your computer and use it in GitHub Desktop.
React createFetcher explained
// 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