Skip to content
A piece of code using React Nook A piece of code using React Nook

React Nook

An alternate reality where React hooks can be called conditionally.

An enhancement, not a replacement

You can use nooks surgically where conditionality matters, without having to rewrite any logic around it. Reuse your existing hooks and call them conditionally!

Freedom of choice

You’re no longer bound by the Rules of Hooks. No more splitting into smaller components just to mount an effect sometimes.

Out of the box

All nook magic happens at runtime, no additional builds tools are necessary.

You can use your favorite package manager:

Terminal window
npm install react-nook

Then import:

import { ... } from 'react-nook';

Want to jump straight into action? There are various bite-sized demos that showcase specific scenarios where conditional hooks are helpful, and how to use them. If you want a more detailed walkthrough of the features and mechanisms, then keep reading!

React = Composable UI, and with the introduction of hooks, that composability soared to the next level! The promise of reusable behavior across components is not all sunshine and rainbows though. To achieve their goal, the React team had to impose certain limitations on hook use, known as the “Rules of Hooks”. This mostly boils down to:

  • Only call Hooks at the top level (outside of control-flow)
  • Only call Hooks from React functions (components or other hooks)

The latter is reasonable and I don’t think anyone is arguing about it, whereas the former can be a pain when working in bigger projects. When writing custom hooks, we rarely plan around someone wanting to skip the hook’s execution based on a condition.

/**
* @returns seconds passed since mount
*/
export function useSeconds() {
const [value, setValue] = useState(0);
useEffect(() => {
const handle = setInterval(() => setValue((prev) => prev + 1), 1000);
return () => clearInterval(handle);
});
return value;
}
function Widget() {
const seconds = useSeconds();
}

Now say we want to count up only when the widget is “active”. It’s easy enough for this simple use case, but for more complex hooks, a refactor like this can be quite involved.

export function useSeconds(active: boolean) {
const [value, setValue] = useState(0);
useEffect(() => {
if (!active) return;
const handle = setInterval(() => setValue((prev) => prev + 1), 1000);
return () => clearInterval(handle);
}, [active]);
return value;
}
function Widget(props) {
const seconds = useSeconds(props.active);
}

What if we want to reset the count every time it becomes active again? Well then we’re out of luck, we have to split the components and mount based on this condition. What if we could just use hooks conditionally?

Before I explain what ✳︎nooks are, I want to emphasize that they’re not a replacement for ⚛︎hooks, they make existing hooks better. Don’t worry about having to rewrite your existing logic.

The library also does not replace the React runtime in any way, nor does it require a build-step to work. React Nook uses a niche property of tagged template literals, and is described in more details here.

✳︎Nooks are places in your codebase where “Rules of Hooks” can finally lay back and relax…

Let’s go over the relationship between ⚛︎hooks and ✳︎nooks

  • ⚛︎hooks CANNOT use other ⚛︎hooks conditionally (the whole reason we’re here)
  • ⚛︎hooks can use ✳︎nooks conditionally: useNook(...)
  • ✳︎nooks can use other ✳︎nooks conditionally: mountFoo``(...)
  • ✳︎nooks can use ⚛︎hooks conditionally: nook(useFoo)

The easiest way to create a ✳︎nook is to wrap an existing ⚛︎hook.

// A regular React hook
function useToggle(initial: boolean) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((prev) => !prev), []);
return [value, toggle] as const;
}
// A nook with the same behavior
const mountToggle = nook(useToggle);

If you’re just now beginning to write these ⚛︎hooks, you may want to write them as ✳︎nooks to begin with!

const mountToggle = nook((initial: boolean) => {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((prev) => !prev), []);
return [value, toggle] as const;
});

The convention is to name nooks starting with mount instead of use, just to visually differentiate ✳︎nooks from ⚛︎hooks.

The ✳︎nooks API can seem strange as first, but you get used to it. To use mountFoo we append an empty template to it, then call the result:

mountFoo``(first_arg, second_arg, ...);

Compared to the ⚛︎hooks version:

useFoo(first_arg, second_arg, ...);

This small change allows the library to track based on which expression was called, instead of in which order. You can learn more about the technical details below

To use nooks inside regular components or hooks, reach for useNook.

function Component(props) {
// ...
const [value, toggle] = useNook(() => {
// Aahh... all cozy and nice
if (props.active) {
return mountToggle``();
}
// If not active, return a fallback and a noop
return ['Inactive', () => {}];
});
// ...
}

No additional wrapper is necessary in this case:

const mountTimer = nook(() => {
const [value, setValue] = useState(0);
// ...
return value;
});
const mountTimerWithToggle = nook((initialActive: boolean) => {
const [active, setActive] = useState(initialActive);
// Resets each time we toggle active, because the timer gets unmounted
const time = active ? mountTimer``() : 0;
const toggle = useCallback(() => setActive((prev) => !prev), []);
return [time, toggle];
});

There’s a variety of bite-sized demos to check out!

(explanation-in-progress)