Published in


Long-distance React hooks 📞🎣

Photo by Mike Meyers on Unsplash

The problem is as old as it gets:

  1. there’s a tree of components
  2. two of them need to talk.

One obvious solution is to use some sort of state management library, like Redux or MobX. But let’s say the above problem statement is all there is to it — the app has no other state, it’s a sea of silent, disconnected components. It just so happens that two of them need to share one variable.

The next shot would be context. That’s what it’s for, right? Context provides a shared variable to a tree of components! But unfortunately context is not free — one component needs to be designated as the holder of the value of this context. Because in React, if something is a state (it changes), it must live in a component.

Adding context introduces another component (or at least forces a hierarchy), and that’s not what we desire here. We want two lonesome components to have a long-distance call. Period.

Hooks to the rescue

Fortunately, there are hooks in React! Hooks are a way to tie a component to React runtime (hook into, right?).

This was not possible with functional components before. They were treated by React as pure functions: take in some props, spit out DOM elements, end of story. A functional component did not have a lifecycle — it was not possible to e.g. do something only on first render, or hold state. Data in, HTML out, that’s all there was to it.

With hooks, a functional component can be impure. Some state can be added to a component, or an action performed only once (like fetching data). Hooks are amazing and take a lot of pain out of React development, while simplifying the way we use React.

Just a hook

An approach I really like when writing code is starting from the API design. When you imagine the code you’re about to use already exists, you can focus on this fantasy API. You won’t be forced into one by some implementation details or constraints.

So in this case, what we want is a hook, and the hook should provide a way to read and write to the shared variable:

const { answer, setAnswer } = useAnswer()

Nice! That’s basically all a component should need to interact with the shared variable. That was quick. As you might’ve noticed this approach of fantasy-API-first ties in nicely with Test Driven Development. First we imagine a perfect world, write some tests to express how it should work, and then — at last — create it.

So let’s do that. We start with a variable:

let answer

This is the source of truth. This is just a variable defined in a file — as simple as it gets. Now let’s get a Set for the state updaters. Each component that uses the hook will have its own updater function, and will get notified separately.

const stateUpdaters = new Set()

Why a Set?Sets have this nice property that they disallow duplicate values, which will come in handy here. Next up is the updating function for the value, passed to the component — the “write” part of this whole business:

const setValue = newValue => {
value = newValue
stateUpdaters.forEach(fn => fn(value))

Again, we’re dealing with pure JavaScript here. It’s just a function. And finally, the part where any React comes into play, namely the setState hook (which comes with React):

export default () => {
const [, setState] = useState();
return [value, setValue];

That’s all! And here you can see it all come together:

One thing though!

If you’re a bit experienced with React you might have noticed one problem with this code. Since each caller component triggers registration of an updating function, it should also deregister it when it un-mounts! I encourage you to fork this code sample and handle that scenario. Hint: it involves another hook, called useEffect 😉.

Why not?

Let’s think of some problems a developer might see with this approach:

Context was out because it introduces a third party — a source of truth, while this solution also proposes one, in the form of a variable!

Yep, but context forces a tree hierarchy, and the source of truth is a component. Here, it’s just a variable. Which simplifies the whole matter greatly and avoids polluting the component tree.

This is not idiomatic React! And this mutated variable is an abomination to the functional sprit of React!

Programming is often about finding the right tool for the job. Introducing a more complex state management solution while all that was needed is a variable shared between components is unnecessary.

Even in a big Redux-powered app, local component state has its uses (e.g. selected tab in a tab container). This approach of a shared mutable variable is just something between local component state and global app state.

This does not scale…

Exactly! If all you need is to display something in multiple places, and the component tree stands in the way, this approach might be better than adding 20 lines of logic to the global state management code.

Should it scale?

You might like this approach, but want to take it the next level. Store a lot of data instead of just a number. Use a reducer-like logic to make updating more wieldy. And then just use that hook as the state management solution.

Well, as it often happens in JavaScript-land, there’s tons of packages that do just that — simplify state management with hooks. While I personally prefer Redux for this job, these are interesting propositions that solve the problem of global state in a much more “intuitive” way.

Frankly, I would not use global mutated state for anything bigger than a single variable. And even then I’d feel a bit squeamish using it. If a state is supposed to be shared across the component tree, it’s a hint that it probably belongs to the global state.

But I hope that despite the anti-recommendation of the whole concept I’ve managed to show you an interesting use case for React hooks! That’s what they are for in the end — building blocks for composition and experimentation. And it’s also why I love React.