Wrappers for React's hooks that make them more portable
Read the introductory post on dev.to
$ npm i portable-hooks
import React from 'react';
import { useEffect } from 'portable-hooks';
function App({ text }) {
useEffect(() => {
document.title = text;
}, [text]);
return <h1>{text}</h1>;
}
Yeah, but what if you wanted to move that effect function outside the component, so you can use in elsewhere? React's existing useEffect
hook leverages the component function closure to use the current props & state. This effectively traps effect functions inside the component. If you wanted to extract the effect that sets document.title
, you'd have to do this:
import React, { useEffect } from 'react';
function setDocumentTitle(title) {
document.title = title;
}
function App({ text }) {
useEffect(() => setDocumentTitle(text), [text]);
return <h1>{text}</h1>;
}
Notice that, if you're correctly managing dependencies, you have to write text
in two places:
- As an argument to
setDocumentTitle
, and - In the dependencies array (
useEffect
's 2nd argument)
Why are we doing this? Functions arguments are dependencies, by their very nature.
React is asking us to write out these arguments twice every time we use one of these dependency-based hooks, if we want to avoid bugs. Wouldn't it be more concise to only write them in one place:
import React from 'react';
import { useEffect } from 'portable-hooks';
function setDocumentTitle(title) {
document.title = title;
}
function App({ text }) {
useEffect(setDocumentTitle, [text]);
return <h1>{text}</h1>;
}
The portable-hooks
package provides wrapped versions of React's own hooks, which call your functions with the dependencies as their arguments. I don't know about you, but that seems pretty elegant to me. Now, your function signature and your dependencies are the very same thing, and you're less likely to run into bugs.
Wouldn't it be great to customise components by passing in effects as props:
import axios from 'axios';
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { useEffect } from 'portable-hooks';
function App({ dataURL, fetchData }) {
const [data, setData] = useState(null);
useEffect(fetchData, [dataURL, setData]);
return <div>{data ? <div>{/* use `data` for something here */}</div> : 'Loading...'}</div>;
}
async function fetchDataUsingAxios(url, setData) {
const result = await axios(url);
setData(result.data);
}
ReactDOM.render(<App dataURL="https://..." fetchData={fetchDataUsingAxios} />, document.body);
Now you have a component that expects its fetchData
prop to be a function that matches a certain signature, but you can implement that function in any way you want.
Look, lying about dependencies is a bad idea, and portable-hooks
very much encourages you (by design) to not lie about dependencies, buuuuut in rare cases it is actually useful. Don't worry though, I got you covered.
Each hook in portable-hooks
differs from React's version by caring about one extra optional argument. If you set it, React's hook will use this as its dependency list, and the original inputs will still be passed into your function.
Here's a (very contrived) example which will spam the console from the moment the component mounts to the moment it is unmounted, regardless of the number of times it is updated:
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { useEffect } from 'portable-hooks';
function logMountDuration(x) {
let seconds = 0;
const id = setInterval(() => {
seconds++;
console.log(`"${x}" was mounted ${seconds} seconds ago`);
}, 1000);
return () => clearInterval(id);
}
function App({ text }) {
const [count, setCount] = useState(0);
useEffect(logMountDuration, [text], []);
return (
<div>
<h1>{text}</h1>
<button onClick={() => setCount(count + 1)}>
{`I've been pressed `}
{count}
{` times`}
</button>
</div>
);
}
ReactDOM.render(<App text="Example" />, document.body);
// > "Example" was mounted 1 seconds ago
// > "Example" was mounted 2 seconds ago
// > "Example" was mounted 3 seconds ago
// ...
portable-hooks
exports the following hooks (which all care about dependencies):
useCallback
useEffect
useImperativeHandle
useLayoutEffect
useMemo
As explained earlier, they're all wrappers around React's own hooks, and expose the same API (with an additional optional argument for those situations where you wanna lie about dependencies), so you can use them interchangeably.
This means that all of your existing anonymous-argument-less code is already compatible, and you can start a refactor by updating your imports:
import React, { useEffect } from 'react';
// ...becomes...
import React from 'react';
import { useEffect } from 'portable-hooks';
- Colin Gourlay (colin@colin-gourlay.com)