DEV Community

Carles Ballester
Carles Ballester

Posted on

React asynchronous render with built-in callbacks (a proposal)

The most consistent way to deal with side effects is to define them in event handlers. This is how you keep the logic consistent and you can define state changes correctly without going into weird re-renders.

The main problem now in react is due to use useEffect for getting asynchronous resources. useEffect is known for triggering infinite loops in react components and make renders inefficient. This is because we're dealing with a side effect during render and the current implementation is messing with react internal render logic.

useEffect was build for handling subscriptions. When you subscribe to a external store, you expect events to be dispatch with certain order and not to have an infinite loop. However, react didn't provide a way to solve the fetch of external resources when component mounts and so developers start using this hook as a solution to go.

So the issue we want to solve here is how to get a resource in a component asynchronously without useEffect.

Let's start with an easier situation. How do we get resources for the current component during an event handler. Let's take a click event.

In this case you would define the fetch in the click handler so you could deal with all state changes (loading, error, success) during all the asynchronous events that a fetch goes through.

function App () {
    const [status, setStatus] = useState('idle')
    return (
    <div>
        {status}
        <Button
            onClick={() => {
                fetch()
                    .then(() => setStatus('ready'))
                    .catch(() => setStatus('error'))
            }}
        />
    </div>
}
Enter fullscreen mode Exit fullscreen mode

Notice how simple it gets in this case just because we have an event handler that start the sequence. Here we're declaratively saying how states in the component changes after a click happens.

However, we don't have an event handler to solve this when the component mounts.

So, say we have a component that shows info about a movie.

function Movie ({id}) {
    const [status, setStatus] = useState('idle')
    const [movie, setMove] = useState(null)
    useEffect(() => {
        // we're using effect this as event handler for mounting
        fetchMovie(id)
            .then((movie) => {
                setStatus('ready')
                setMovie(movie)
            })
            .catch(() => setStatus('error'))
        // this can become fragile when the dependencies are messy
    }, [id])
    return (
    <div>
        {/* we have to deal with inconsistent states (unless we suspend) */}
        {movie.title}
    </div>
}
Enter fullscreen mode Exit fullscreen mode

Notice that, all those issues won't be there if we implement the same functionality but after a button is clicked.

If we dig a bit on the API. The actual problem is that useEffect has to be executed every time the component renders, even if there's no DOM update. That's why the dependency array helps react to tell if the callback inside needs to be executed.

In my opinion this is an unstable equilibrium. Meaning, solves the problem but it'll break easily. Adding the dependencies array is a patch. As a developer, I'm afraid of adding expensive calculations in components because I know it's a potential performance issue in the future.

How do we solve the resource loading then? Well, in the end, we're abstracting a DOM that represents our application. Browser's DOM is not perfect but it have many handy functionalities that we use instinctively like event bubbling and event handlers that we take for granted for all DOM elements. However, we don't use any of them when we develop react components.

The event handler we're missing here is onMount. That's it. "On mount, get the resource and when the resource is ready, use it for this component". That's the user story here so what if provide onMount event handlers for all Components.

Here's a possible implementation:


const MovieTitle = (props)=> {
    const movie = props.onMount()
    return <div>{movie.title}</div>
}

const Movie = ({id}) => {
    return (
        <MovieTitle
            fallback={<DisplayMovieLoading />}
            onMount={()=> {
                // the component will be ready after the promise is resolved
                return fetchMovie(id)
                    .then(setMovie)
            }}
        />
    )
}
Enter fullscreen mode Exit fullscreen mode

Notice, there're no more side effects in any component. Now you may ask, how you deal with id changes? Same way as we do know, using a key prop with the id would run onMount again.

What do you think about this approach? Would make your apps simpler?

Top comments (0)