React hooks without hooks - a collection of hook-like Null Components
react-unhook
attempts to emulate some of the functionality and segmentation aspect of react hooks,
packaging it into a standalone "Null Components" (components that render null
) without the
use of React hooks under-the-hood.
(Note: This is not about avoiding hooks. Just an alternative to some of it).
React Hooks are a new addition to React 16.8 and it changes the way we have been approaching React components, formalising new ways of encapsulating logic in our components.
Taking inspiration of that, we can make use of the existing lifecycle methods to achive some behaviours of React hooks via Null Components. This allows us to achieve similar code style and logic encapsulation of React hooks, aside from low-level / library optimization of hooks.
With that said, there are some limitations to this Null Component pattern.
Stateful hooks (eg: "useReducer") cannot be emulated easily as we are not able to expose
functions of the component without resorting to anti-patterns (eg: using React.createRef
to
access component methods).
However, this Null Component pattern works well for "listeners", "workers" or "value-change triggers" (triggering of a function after a change in value). For example, listening to geolocation changes, interval calls, data fetching on parameter changes etc.
// Imagine that you have a signup form that on certain value change,
// we want to fetch things or asynchronously set values
// Using "Null Components" we can declaratively define those effects.
function SignupForm(props) {
return (
<Fragment>
<Input name="input-one" />
<Input name="input-two" />
<Input name="input-three" />
<FetchWhenInputOneIsFilled name="action-one" />
<ValidateWhenTwoIsDirty name="action-two" />
<UpdateInputThreeWhenTwoIsValid name="action-three" />
</Fragment>
);
}
npm install react-unhook --save
import { UseCallback, UseEffect } from 'react-unhook';
These examples are adopted from React's official docs on hooks. i.e. https://reactjs.org/docs/hooks-effect.html
The unhook examples makes use of withState
HOC (1, 2) to
keep the code style closer to the hooks
examples. You can also manage your state
using a normal class.
Examples are also available at http://yeojz.github.io/react-unhook
Using Hooks:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
Using Unhook:
function Example(props) {
// assumes you're using withState HOC.
// eg: withState('count', 'setCount', 0)(Example);
const { count, setCount } = props;
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<UseEffect
fn={() => {
document.title = `You clicked ${count} times`;
}}
/>
</div>
);
}
Using Hooks:
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
Using Unhook:
function FriendStatus(props) {
// withState('isOnline', 'setIsOnline', null)(FriendStatus);
const { isOnline, setIsOnline } = props;
return (
<Fragment>
{isOnline === null ? 'Loading' : isOnline ? 'Online' : 'Offline'}
<UseEffect
fn={() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(
props.friend.id,
handleStatusChange
);
};
}}
/>
</Fragment>
);
}
Using Hook:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
Using Unhook:
<UseEffect
fn={() => {
document.title = `You clicked ${count} times`;
}}
inputs={[count]}
/>
Note: The comparator function, by default, follows React Hook's areHookInputsEqual
method,
which uses Object.is
to compare the values in the array.
All unhook components make use of UseCallback
and UseEffect
at their core.
Many of the components are inspired by hooks from react-use,
but re-implmented using react-unhook's <UseEffect />
instead of actual React Hooks.
Component which emulates useEffect
hook.
interface Props {
fn: () => void | Noop;
inputs?: Array<any>;
comparator?: EqualityFn;
}
The difference between UseEffect
and UseCallback
is that the function passed
into UseEffect
may return a "clean-up" function which will be executed when unmounting
the component. In most cases, you can just utilise UseEffect
.
interface Props {
fn: () => void;
inputs?: Array<any>;
comparator?: EqualityFn;
}
Only runs the callback when inputs change and not during mounting.
interface Props {
fn: () => void | VoidFn;
inputs: any[]; // unlike UseEffect, this is required.
comparator?: EqualityFn;
}
Alias method using UseEffect
with prop.inputs
preset to []
interface Props {
fn: () => void;
}
Calls a function when the component is mounted
interface Props {
fn: () => void;
}
Calls a function when the component will unmount.
interface Props {
fn: () => void;
}
Calls the function at every specified interval (in milliseconds), eg: Polling.
interface Props {
fn: () => void;
time: number;
}
Calls the function after the specified wait time (in milliseconds)
interface Props {
fn: () => void;
time: number;
}
Tracks user's geographic location.
interface Props {
fn: (
error: GeolocationPositionError | null,
data: GeolocationPosition | null
) => void;
watch?: boolean;
options?: PositionOptions;
}
Fires a callback when mouse leaves target element.
interface Props {
fn: () => void;
target: () => HTMLElement | Document | Window;
capture?: boolean;
}
react-unhook
is MIT licensed