When I try to learn something, I search around for the optimal approach. In the case of hooks I found two reasonable approaches and one approach that only makes sense in some use-cases. Below I've documented all of them.
I want to change the style of a text element when the user is clicking down on it. Because this is React Native for web, there are no CSS pseudo classes, so I need to manage all of the state myself. Because classes like active
, focus
, hover
, and visited
could be commonly used the API must be very self-contained.
... and I want people to like me, so I'm using hooks (the old implementation used render props).
This first approach returns the boolean value and a set of props (onMouseDown
, onMouseUp
) which are then spread onto the object you want to observe.
import React from 'react';
import useActive from './useActive';
function App() {
const [isActive, bind] = useActive();
return (
<Text {...bind} style={{ color: isActive ? 'blue' : 'black' }}>
Is Pressing
</Text>
);
}
The implementation is very clean and can be very easy to for a user to understand.
import { useState } from 'react';
export default () => {
const [isActive, setActive] = useState(false);
return [
isActive,
{
onMouseDown: () => setActive(true),
onMouseUp: () => setActive(false),
},
];
};
In this approach you would pass a reference to the hook and it would return the boolean.
import { useRef } from 'react';
import useActive from './useActive';
function App() {
const ref = useRef(null);
const isActive = useActive(ref);
return (
<Text ref={ref} style={{ color: isActive ? 'blue' : 'black' }}>
Is Pressing
</Text>
);
}
The implementation is a lot more complex and has potential to conflict with any existing props.
import { useState, useEffect } from 'react';
import getNode from './getNode';
export default ref => {
const [isActive, setActive] = useState(false);
useEffect(() => {
const node = getNode(ref);
if (!(node && typeof node.addEventListener === 'function')) return;
const onStart = () => setActive(true);
const onEnd = () => setActive(false);
node.addEventListener('mousedown', onStart);
node.addEventListener('mouseup', onEnd);
return () => {
if (!node) return;
node.removeEventListener('mousedown', onStart);
node.removeEventListener('mouseup', onEnd);
};
// Only update when the ref has changed.
}, [ref && ref.current]);
return isActive;
};
Another method I saw around the hub was the use of a callback.
import { useRef } from 'react';
import useActive from './useActive';
function App() {
const ref = useRef(null);
useActive(ref, isActive => {
// Do something
});
return <Text ref={ref}>Is Pressing</Text>;
}
There is no custom state being created.
import { useEffect } from 'react';
import getNode from './getNode';
export default (ref, callback) => {
useEffect(() => {
const node = getNode(ref);
if (!(node && typeof node.addEventListener === 'function')) return;
const onStart = () => callback(true);
const onEnd = () => callback(false);
node.addEventListener('mousedown', onStart);
node.addEventListener('mouseup', onEnd);
return () => {
if (!node) return;
node.removeEventListener('mousedown', onStart);
node.removeEventListener('mouseup', onEnd);
};
// Only update when the ref has changed.
}, [ref && ref.current]);
};
import React from 'react';
import useActive from './useActive';
function App() {
// Case #4
const [bind] = useActive(isActive => {
// Do something
});
return <Text {...bind}>Is Pressing</Text>;
}
There is no custom state being created.
export default callback => {
return [
{
onMouseDown: () => callback(true),
onMouseUp: () => callback(false),
},
];
};
In this appraoch we do everything.
import { useRef } from 'react';
import useActive from './useActive';
function App() {
const ref = useRef(null);
const [isActive, bind] = useActive(ref, isActive => {
// Do something here
});
// or here
return (
<Text {...bind} ref={ref} style={{ color: isActive ? 'blue' : 'black' }}>
Is Pressing
</Text>
);
}
I apologize for this...
import { useState, useEffect } from 'react';
import getNode from './getNode';
export default (ref, callback) => {
let inputRef = ref;
let inputCallback = callback;
// Support the first prop being a function
if (typeof ref === 'function') {
inputRef = null;
inputCallback = ref;
}
const [isActive, setActive] = useState(false);
const [bind, setBindings] = useState({});
useEffect(() => {
const node = getNode(inputRef);
const resolve = value => {
if (inputCallback) {
inputCallback(value);
// Possibly return if a callback is defined.
// return;
}
setActive(value);
};
if (!(node && typeof node.addEventListener === 'function')) {
// If there is no valid ref, then use the binding method.
setBindings({
onMouseDown: resolve.bind(this, true),
onMouseUp: resolve.bind(this, false),
});
return;
}
// Using the ref, erase the bindings.
setBindings({});
const onStart = resolve.bind(this, true);
const onEnd = resolve.bind(this, false);
node.addEventListener('mousedown', onStart);
node.addEventListener('mouseup', onEnd);
return () => {
if (!node) return;
node.removeEventListener('mousedown', onStart);
node.removeEventListener('mouseup', onEnd);
};
}, [inputRef && inputRef.current]);
// Return an object to account for when `isActive` isn't being used.
return { isActive, bind };
};
So umm, yeah...
import { useRef } from 'react';
import useActive from './useActive';
function App() {
const ref = useRef(null);
// Case #1
const { isActive, bind } = useActive();
// Case #2
const { isActive } = useActive(ref);
// Case #3
useActive(ref, isActive => {
// Do something
});
// Case #4
const { bind } = useActive(isActive => {
// Do something
});
// Case #4 (alt)
const { bind } = useActive(null, isActive => {
// Do something
});
// Case #fuckit
const { isActive, bind } = useActive(ref, isActive => {
// Do something
});
return (
<Text {...bind} ref={ref} style={{ color: isActive ? 'blue' : 'black' }}>
Is Pressing
</Text>
);
}