React Hooks are here to stay. We had our time with them, learned how they work and how to use them. Most importantly, we learned that eslint-plugin-react-hooks
is our best friend, especially react-hooks/exhaustive-deps
rule :). From that, we can create Hooks that group common logic and reuse them on multiple projects, something like Lodash functions. With that, let's see 5 useful Hooks that saves you some time and make your code easier to read.
Simple Hook to use with input tag. The primary purpose is an abstraction over input onChange
property.
function useInputState(initialState = "") {
const [state, setState] = useState(initialState);
const setInputState = useCallback(event => setState(event.target.value), []);
return [state, setInputState];
}
We are extracting value from the event argument and use it to set the next state. We are also using useCallback
Hook with empty array (zero dependencies) to be sure that setInputState
reference will never change and thus cause unneeded render.
() => {
const [value, onChange] = useInputState("");
return <input type="text" value={value} onChange={onChange} />;
}
This Hook is for those situations when you need to toggle between two values.
function useToggleState(
initialState = false,
[on, off] = [true, false]
) {
const [state, setState] = useState(initialState);
const toggleState = useCallback(() => {
setState(s => (s === on ? off : on));
}, [on, off]);
return [state, toggleState, setState];
}
It has default values (true
/false
) for interaction with an input checkbox element. You can also use it to toggle between dark and light themes just by setting on
value to 'dark' and off
value to 'light'. The main part of useToggleState
is toggleState
function, which is nothing special, just a good old conditional (ternary) operator.
() => {
const [theme, toggleTheme] = useToggleState("light", ["dark", "light"]);
return (
<div className={theme}>
<button onClick={toggleTheme}>Toggle theme</button>
</div>
);
}
If you need to skip the first render of functional component (the equivalent of componentDidMount
method in class component), useDidUpdateEffect
Hook is right for the job.
function useDidUpdateEffect(fn, inputs) {
const fncRef = useRef();
fncRef.current = fn;
const didMountRef = useRef(false);
useEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true;
} else {
return fncRef.current();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, inputs);
}
We set didMountRef
default value to false
to indicate that the component is not mounted. After that, we use useEffect
to change didMountRef
or call the provided function. First time useEffect
runs, it will set didMountRef
to true
indicating componentDidMount
. Every other call to useEffect
will call the provided function simulating componentDidUpdate
. I wrote simulating because the functional component doesn't know the difference between mount and update. It knows only to render itself, and that is the reason we store the value of didMountRef
in useRef
Hook to keep data between renders.
This one is like useEffect
, with a twist. It runs a callback function only if all dependencies are different than null
or undefined
.
function useNotNilEffect(fn, dependencies = []) {
const fnRef = useRef();
fnRef.current = fn;
useEffect(() => {
const notNil = dependencies.every(
item => item !== null && item !== undefined
);
if (notNil) {
return fnRef.current();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
}
We don't want to depend on callback function to run effect because we can potentially cause the infinite loop. That is the reason we save function reference to useRef
and on every render reassign the value of fnRef
to the latest function reference. Inside useEffect
we are checking if our dependencies will pass nil test and thus will it run callback or ignore it.One note is that we can't rely on auto fix dependencies (react-hooks/exhaustive-deps
rule) because ESLint can only statically determine our dependencies.
() => {
const [state, toggleState] = useToggleState();
useEffect(() => {
//run on mount and state change
}, [state]);
useDidUpdateEffect(() => {
//will skip mount but will run on state change
}, [state]);
return <button onClick={toggleState}>Toggle</button>;
}
Almost every React project will have tables. Those tables will probably have sort capabilities. We'll want to group common sort logic to make our job easier.
function useTableSort(initialKey, initialDirection) {
const [sortKey, setSortKey] = useState(initialKey);
const [
sortDirection,
toggleSortDirection,
setSortDirection
] = useToggleState(initialDirection, [
SORT_DIRECTION.ASC,
SORT_DIRECTION.DESC
]);
const setSort = useCallback(
key => {
if (key !== sortKey) {
setSortKey(key);
setSortDirection(initialDirection);
} else {
toggleSortDirection();
}
},
[sortKey, setSortKey, toggleSortDirection, setSortDirection]
);
return { key: sortKey, direction: sortDirection, set: setSort };
}
Sort logic will have key
(table column), and direction
(ASC/DESC) by which we sort table rows. useTableSort
returns key
, direction
, and sort
. sort
method depends on a key
argument. If the key
is the same as the last one, it will toggle direction
otherwise will change key
and set the direction
to initialDirection.
({ data = [] }) => {
const { key, direction, set } = useTableSort("name", SORT_DIRECTION.ASC);
const list = useMemo(() => {
/*sort data*/
}, [key, direction, data]);
return (
<table>
<thead>
<tr>
<th onClick={() => {set("name")}}>Name</th>
<th onClick={() => {set("email")}}>Email</th>
</tr>
</thead>
{/* list */}
</table>
);
}
Code reusability is always important, and React Hooks are not the exception to that rule. 5 Hooks is step in that direction.