React Functional Components using Hooks are a More Accurate Implementation of the React Mental Model for State and Effects, than React Classes

TL;DR React made updating the DOM declarative. Hooks made components themselves declarative.

The key of React was allowing declarative code to be mapped to an imperative DOM.

This was especially true of functional components, which would simply map data to an object describing the UI. React would take this object and surgically (imperatively) update the DOM.

However, with class components, while the render function was still declarative, the class instance itself (where the state lived) is mutable - which made it harder to reason about.

The implementation for state and side-effects were within these class components - tied to the mutating instance.

React hooks are a re-conception and re-implemenation of state and side-effects in React - an implementation instead of in class components, is in functional components. As a basic definition they are functions that let you "hook into" React state and lifecycle features. But the key is their implementation with functional components in a declarative api.

"But why is this a 'more accurate implementation of the react mental model'?"

React hooks allow components to be truly declarative even if they contain state and side-effects.

State is now retrieved declaratively without mutating the structure of the component (ie as the class instance would be).

Side-effects are now declaratively aligned with state, instead of with the component's mutation.

Just as the first key of react was a declarative mapper to the DOM, hooks are the second key: providing a declarative api in the component for state and side effects.

"Um, OK, sure.. How about some code?"

Lets look at two versions of doing the same thing. The first version uses the initial class-based implementation of state and effects, and second uses the new hook-based implementation.

The example is an (very contrived) User component. An input will search for the user and display their name, which can be edited and saved.

Using React's initial class-based implementation of state and effects

https://codesandbox.io/s/react-classes-are-the-wrong-mental-model-n9zbs



/* * A code sample to show how React class components are * not the best implementation of the react mental model. * * Limitations: * - 1. With react classes, `this` is mutable and harder * to reason about * - 2. With react classes, the lifecyle hooks are aligned * with the component instead of the data. * * To see 1: save a user's name, and then immediately * change it again. You'll see the confirmation alert has * the wrong name (the new one, not the one which was saved). * Because "this" is mutated before the save finishes, * the wrong data is surfaced to the user. * * To see 2: Notice how the code for componentDidUpdate * and componentDidMount is doing the same thing? What we * care about is changes to "username" data but instead * the model here is built around changes to the component. */ import React from " react " ; class User extends React . Component { state = { username : this . props . username }; handleUsernameChange = e => { this . setState ({ username : e . target . value }); }; handleNameChange = e => { const name = e . target . value ; this . setState ( state => ({ ... state , user : { ... state . user , name } })); }; save = () => { // Pretend save that takes two seconds setTimeout ( () => alert ( `User's name has been saved to " ${ this . state . user . name } ` ), 2000 ); }; async fetchUser () { const response = await fetch ( `https://api.github.com/users/ ${ this . state . username } ` ); if ( ! response . ok ) { return {}; } return await response . json (); } async componentDidMount () { if ( this . props . username ) { if ( this . state . username ) { const user = await this . fetchUser (); this . setState ({ user }); } } } async componentDidUpdate ( prevProps , prevState ) { if ( this . state . username !== prevState . username ) { if ( this . state . username ) { const user = await this . fetchUser (); this . setState ({ user }); } } } componentWillUnmount () { // clean up any lingering promises } render () { return ( <> Search < input value = { this . state . username || "" } placeholder = "Github Username" onChange = { this . handleUsernameChange } /> < hr /> { this . state . user && ( <> < h2 > Name </ h2 > < input value = { this . state . user . name } onChange = { this . handleNameChange } /> < button onClick = { this . save } > Save </ button > </> ) } </> ); } } export default User ;

Here is the live code running. You can see point 1 described in the code comment above: save a user's name, and then immediately change it again. You'll see the confirmation alert has the wrong name (the new one, not the one which was saved).

Now lets look at...

Using React's new hook-based implementation of state and effects

https://codesandbox.io/s/react-hooks-are-a-better-mental-model-f9kql



/* * A code sample to show how React functional components useing "hooks" are a * better implementation of the react mental model. */ import React , { useState , useEffect } from " react " ; const fetchUser = async username => { if ( ! username ) return await {}; const response = await fetch ( `https://api.github.com/users/ ${ username } ` ); if ( ! response . ok ) return {}; return await response . json (); }; const saveUser = user => { // Pretend save that takes two seconds setTimeout (() => alert ( `User's name has been saved to " ${ user . name } ` ), 2000 ); }; export default ({ username : initialUsername = "" }) => { const [ user , setUser ] = useState ({}); const [ username , setUsername ] = useState ( initialUsername ); useEffect (() => { const doFetchAndSet = async () => { const u = await fetchUser ( username ); setUser ( u ); }; doFetchAndSet (); }, [ username ]); return ( <> Search < input value = { username || "" } placeholder = "Github Username" onChange = { e => setUsername ( e . target . value ) } /> < hr /> { user . name && ( <> < h2 > Name </ h2 > < input value = { user . name } onChange = { e => setUser ({ ... user , name : e . target . value }) } /> < button onClick = { () => saveUser ( user ) } > Save </ button > </> ) } </> ); };

Again, here is this live code running. If you try to reproduce the bug from the first example, you won't be able to.

What insights am I missing? What did I neglect or exaggerate? Let me know!