2019-07-14
Turning imperative code into declarative using React
With React’s lifecycle hooks many pieces of imperative code can be turned into a safer, declarative version, where most complexity is contained inside a React component.
Declarative? What’s that?
React is most commonly used to build interfaces on the web using declarative HTML-like syntax. Declarative here means telling the library what we want the end result to be rather than what we want the computer to strictly do.
Declarative code means that the library has to do additional calculations to determine what to edit in the DOM. On the other hand we always get a more deterministic visual representation in return for the small performance hit in calculating the DOM diff. If done correctly, the UI will match our state. In imperative UI code this guarantee is significantly harder to achieve, because we need to always remember to update every place where our state is used one by one and manually.
Sometimes imperative is needed
For instance when using libraries, imperative code cannot always be avoided. Let’s take a look at how Spotify’s Web Playback SDK works for pausing, resuming and listening for a status of a track.
As we can see the Spotify Playback SDK is highly asynchronous, event based and imperative by nature. What if we wanted to implement a small player UI where you can play and pause the track and importantly see the current state of track? Can we implement all this in a safe and declarative manner or at least hide all the complexity of dealing with asynchronity?
Lifecycles to the rescue
We can take advantage of React’s lifecycle hooks and their strong support for immutable state updates to create our own small diffing algorithm.
Effect hooks included with React hooks are excellent for turning imperative code to declarative.
useEffect(() => { ... }, [])
is equivalent to
componentDidMount() { ... }
.
useEffect(() => () => { ... }, [])
is equivalent to
componentWillUnmount() { ... }
.
useEffect(() => { ... }, [myProp])
is equivalent to
componentDidUpdate(prevProps) {
if (prevProps.myProp !== myProp) {
...
}
}
By using these three different hooks, we can deal with all diffing operations that happen in React with imperative code! Here’s a lightweight example of a player that fully synchronizes with Spotify player’s playing state and one-way synchronizes with the volume.
Creating a new null component just for handling state between us and Spotify encloses all complexity within that component. Additionally due to the pure nature of the component we can control the underlying state simply by passing the state we want (declarativeness!). This helps get rid of data races caused by the asynchronous nature of the Spotify player, because all state changes in the underlying Spotify player are bubbled up using the listener we registered on the player.