Hooks Are Not A Replacement For Flux

Sean
4 min readJun 18, 2021

React hooks has provided devs with a new way to create components that no longer relies on using Javascript’s dodgy implementation of classes.

Now JS devs can write plain Javascript functions as React components, which is awesome (watch Source Decoded’s video on prototypes for some background info on Javascript classes).

Believe me that this is not an article panning hooks, hooks are super useful and I use them every day at my work, but I think there’s a miscomprehension among many devs about what hooks are for and how they should be used. And there’s definitely some confusion about how their status in regards to Redux.

Don’t talk to me about Separation of Concerns!

I get it, you’ve heard this a million times. It’s like a catch-phrase for some devs, like reusability or some other common programming trope; but why is this idea actually useful?

Say I’m making a React app and I have a button that has text and it changes depending on the state:

import React, { useState } from 'react';const Form = () => {
const [start, setStart] = useState(true);
return (
<Button
onClick={() => setStart(!start)}
>{start ? 'Start' : 'Stop'}</Button>
);
};

Now I want to prevent the user from double-clicking so I decide to add a throttle to the onClick method:

import React, { useState } from 'react';const Form = () => {
const [start, setStart] = useState(true);
// This function says only run my function once
// within 1000 mm...
const onClick = throttle(() => {
setStart(!start);
}, 1000);
return (
<Button
onClick={onClick}
>{start ? 'Start' : 'Stop'}</Button>
);
};

Notice that throttle returns a function, because of that this component is now broken.

const onClick = throttle(() => {
setStart(!start);
}, 1000);

React will re-render this method every time start changes which means my throttle function will instantiate onClick every render and cause the throttle effect to stop working.

Now I can fix this by using useMemo or useCallback from React:

const onClick = useMemo(() => throttle(() => {
setStart(!start);
}, 1000), [start]);

But is it just me or are things starting to look a little bit silly?

Imagine a real-life complex component, full of these confusing-looking hooks all micro-managing little bits of state and trying not to trip over themselves worrying about whether they’re getting re-rendered or not.

The thing is this component’s UI hasn’t changed at all it’s the business logic in the component that’s getting complicated. And the more complicated the business logic gets the more we actually limit the potential of the UI.

Imagine a few weeks down the line your Product Owner asks you to measure whether the user’s prone to double clicking or they simply want to click the button faster. So you want to measure the user’s behaviour but keep the throttle on the setStart, we can’t do that without refactoring the onClick in some way, which means you could be adding bugs to the existing feature.

The Flux way

Lets look at a more decoupled way of implementing this functionality:

import React from 'react';
import { useSelect, useDispatch } from 'react-redux';
import { buttonClick } from './actions';
const Form = ({ start, onClick }) => { return (
<Button
onClick={onClick}
>{start ? 'Start' : 'Stop'}</Button>
);
};export default connect(
state => ({
start: state.start
}),
{
onClick: buttonClick
}
)(Form);

This component is now completely decoupled from the state or anything else. It just expects a boolean called start and an onClick function.

I can use the state reducer to throttle the buttonClick payload:

// The throttled function is now detached from the render
// so it won't re-instantiate...
const setStart = throttle(start => start, 1000);
const reducer = (state = {
start: true
}, action) => {
switch (action.type) {
case BUTTON_CLICK:
return {
...state,
start: setStart(!state.start),
};
default:
return state;
}
};

And I can still measure the amount of times the button is clicked by the user because the business logic isn’t distorting the user’s real-time interaction with the app:

const setStart = throttle(start => start, 1000);const reducer = (state = {
start: true,
clickedCount: 0,
}, action) => {
switch (action.type) {
case BUTTON_CLICK:
return {
...state,
clickedCount: state.clickedCount + 1,
start: setStart(!state.start),
};
default:
return state;
}
};

That feature just got added with two extra lines and no refactoring.

The word fad comes to my mind a bit too easily when I hear the word hooks, but that’s being a bit unfair. Hooks are great because now we can make function components in React instead of using classes. There are still plenty of times where you want to know when the component has mounted, when it’s un-mounted and other lifecycle events.

But for me business logic belongs elsewhere. Why is it necessary to have to think about whether your function or variable is being continually re-instantiated while you’re trying to write code that’s unrelated to the render process? It’s like treading water while doing your times tables.

The developers world is continually transforming and updating and I think there’s a tendency to think that we have to chuck out all the old paradigms whenever something new comes along.

I get it, it’s boring being a stickler for tradition, but actually, the internet’s been around for a little bit of time now and there’s a lot of work gone into building it. Maybe let’s keep the bits that are good.

--

--