This post is the first in a series that aims to follow the thoughts and actions that rise as we adopt the new React Hooks API.
The birth of a pattern
When Facebook first introduced Flux around 2014 I knew it was the answers to all my cravings. Watching the trends shift from MVC to MVVM to MV* I always felt there are some fundamental flaws in those pattern when applied to modern front-end architecture.
Luckily, most people felt the same and Flux adoption was massive and Redux became the standard implementation of it. Its not that Redux is the best solution (I actually bet on a different library at first) but though it has its weaknesses and awkwardness, it provides a clear flow with some core principles that are crucial to large scale apps.
Redux — the good parts
We’ve been using Redux and redux-saga to build our front-end for a long time now. When dealing with large or medium (or any) system, you always want to follow the same principles:
- Separating concerns — different layers for domain, service and presentation logic
- Develop in isolation — each layer and component can be developed without loading the entire app, using only tests and mocks.
- Code reusability — structuring our app into small reusable chunks and then using the same code in the web, mobile, and even CLI app.
Patterns used by Redux and sagas provided us with those principles and it has been working pretty good for a while, so why should we migrate away from it?
Clean code is not lean code
While this architecture helps us run a tight operation, it comes with a burden that leads to frustration among developers.
- Endless boilerplate code — even for the simplest feature. Actions, reducers, sagas, pass-through props and many other pieces of code which act as crutches to the actual function code.
- Hard to visualize — while redux makes complex features easier to grasp, it actually has the opposite effect on simple features (which are the more common features). Tracking down the flow through actions and sagas can quickly turn into vertigo.
Nevertheless, among all alternatives, I wasn’t convinced of a better way (though many might disagree). Other solutions either didn’t enable us to follow the principles described above or provided very little benefit over current implementation.
Then came Hooks.
Hooks might seem weird at first and very counter-intuitive. But once you understand how they fit in the architecture, you will see why they change everything.
Hooks allow you to follow Flux architecture while hiding Flux implementation.
I will not go over what hooks are or how they work. You can read about it in Dan Abramov’s post “Making sense of hooks” or in the official docs. Instead, I will show how hooks are compared to a classic Redux app by building a simple Songs application.
The purpose of this and the following posts is not to teach hooks, but rather to show how they fit in an application that follows our core principles.
Everything stated here is based my own personal observations and predictions. Hooks are still in alpha and are actively discussed in their RFC, so please don’t take this (like any other opinion) as divine truth.
“Hooked On Songs” — the requirements
- Display a list of songs
- Allow the user to download a song
- Display the download status of a song
Which components will we need?
We will need 4 components, not including the base App component. Here is a sketch of our components as they are mounted in the app:
What functionality will we need?
- Fetch the list of songs
- Pass the songs to the components
- Download a song
- Track download status and update the component
Step 1: Building the app with Redux
We’ll be using redux with redux-saga (you may be using redux-thunk or observables, but the principles are the same). Here is the react-redux part of the code, emitting the sagas that make the actual network calls.
That’s a 100 lines of code there, and we only showed the presentation layer. We still need to add action creators, reducers, sagas and the service that fetch or download the songs. Yet, if you know redux, you got the idea.
This code is long, but it is clean since it lets us:
- Design the components in isolation — we can build and style each of the components just by passing the data through props.
- Separate presentation from domain logic — network access logic is encapsulated away from the presentation
…but it comes at a price:
- Lots of boilerplate
- Nesting hell — we need to pass the download action and status through 3 different components just to get to the Download button which actually uses it.
The question of container vs pure components sparks a long debate. It is now common to say that there shouldn’t be a holy separation between pure components and containers and so, some of the nesting can be removed. Nevertheless, When developing large apps, nesting will always be present in one way or another so we will leave it here for the sake of the example.
Let’s stop here, you got the idea.
Step 2: Building the app with Hooks
The first thing you should notice is that this code is only 87 lines, yet it includes not only presentation logic but also domain logic. The code is not only shorter but cleaner, with no boilerplate. That makes it easier to read and understand.
Let’s compare the way we implement the 4 functions described earlier in the requirements with hooks vs redux:
1. Fetch the list of songs
Bind an action using
mapDispatchToProps with redux
connect and pass it to the component through props. The component will then fire the action when it is ready during
componentDidMount lifecycle method.
The action will be caught by a saga (not implemented here) which will then execute the API call. A reducer will take the output of the saga and update the state.
Call our custom hook
useSongListwhich uses a core hook called
useEffect for fetching the data and another core hook —
useStatefor passing state to the component.
useEffect is fired after the component is painted, so it can be used as a replacement for
2. Pass songs to the component
connect we bind the store’s state to the component with the help of a HOC container. The container locates the data in the store and pass it to the component as props. Whenever the store changes, for example as a result of a reducer update, the props will change the component will re-render.
Our hook passes the state directly to the component without the use of props. If the state will change, the hook will cause the component to re-render as if the props changed.
useStat creates an encapsulated state, that means that the results are only available to the component that fired the hook. If another component will also fire the hook, the api call will be executed again.
This is a bit cheating, since the redux example provided us with global state so you may say that the extra code is the result of extra functionality. But you will see soon that is is easy to add global state to this hook, without complicating the app.
3. Download a song
Same as binding the fetch songs action, we bind a download action using
connect. We then had to pass it down through the nested components until it reached the
DownloadItem component that actually used.
useDownloadSonghook which returns a vanilla method to be executed when needed. By using hooks we drastically pushed this logic all the way down to the download button, eliminating any nesting.
4. Track download state
That’s pretty tricky since we are using global state. We need to keep the id of the currently download song(s) then pass it down as props. The item list will use this data and set the
isDownloading property of each item to true or false.
useDownloadSong hook uses the same state mechanism as
useSongList so whenever the download state changes, it will re-render the Download button component. This allows us to track the download state in a Flux-y way but doesn’t require any actions and reducers.
Since it uses encapsulated state, we can track the state of each button without re-rendering the entire page! So it is not only cleaner, but it is more powerful and efficient.
Aftermath: “That comparison is false — hooks are not flux”
Looking at the hooks code, it seems we can achieve the same results with redux or by dropping redux all in all. The second code looks like it shorter just because it gave up on flux, or is it?
The title of this text is Flux without fuss because in my view, hooks let you write flux with code that looks more natural.
Just as async\await handle asynchronous methods with a syntax that looks like it is synchronous, hooks allow you to implement Flux with syntax that hides away the boilerplate.
Lets examine some claims that may rise:
1. You are calling the download method directly — thats not Flux!
Flux doesn’t mean you have to execute side effects by passing through actions and sagas, only that data flow is unidirectional. Technically we are calling the callback from the hook, but the reply is coming through outside state updates.
2. useState is not global so thats the same as setState in classes
That was my first misconception about Hooks — “if I wanted state in a function component then I would’t use a function component”. But the state coming from the hook is not the same as a class state: first, even though it is local, it still comes from outside, like props. That means you can reuse the state logic in multiple components or can simply extract away the state management to a different layer, leaving the component cleaner.
Second, it is easy to add global state to a hook — I will not go over it here but you can check it here and here. In a nutshell: just extract the store to a provider then use
useContext hook to turn the hook into a consumer. That means that you can start with local state, then switch to global state, without changing the component.
3. The components are not isolated since they execute network calls
That’s correct, if we render the SongsPage component inside Styleguidist or Storybook, it will immediately try to access the network. So they are not truly isolated.
I will cover that in the next post, but for now, the pattern should be the same as the answer to the previous claim — you pass the Network or API class using Context and the use
useContext to grab it from the hook. That way, you can inject a mock api service when running your component in isolation.
In the next posts I will continue to share our migration process and take a deeper dive into the architecture to see how it helps us with more complex patterns. Stay tuned!