Skip to content

Instantly share code, notes, and snippets.

@lebbe
Last active January 13, 2023 11:59
Show Gist options
  • Save lebbe/d680dfd2109a537db319f32073e81edf to your computer and use it in GitHub Desktop.
Save lebbe/d680dfd2109a537db319f32073e81edf to your computer and use it in GitHub Desktop.
A hook for simple bookmarked-based feature toggling

A simple React hook for toggling something on and off with a bookmark link. If the feature is only meant for internal testing etc, this hook makes it "safe" to release a feature into production without letting end users see it. (Of course, nerdy developers with development tools will be able to find it.)

You can use a simple javascript:url as a bookmarklet that triggers a custom event. The name of the event shoud be identical with the featureName in the code below:

Drag this link to the bookmarks bar to toggle Fancy Feature in our product:

<a href="javascript:window.dispatchEvent(new Event('fancy_feature'));">Toggle Fancy Feature</a>
import useFeatureToggle from '../hooks/useFeatureToggle';
export function Component() {
const showFancyFeature = useFeatureToggle('fancy_feature');
// More state handling ....
return (
<div>
// More templating...
{showFancyFeature && <FancyFeature />}
</div>
);
}
import { useEffect, useState } from 'react';
export default function useFeatureToggle(featureName: string) {
const [showFeature, setShowFeature] = useState(
localStorage[featureName] === 'on'
);
useEffect(() => {
function handler() {
if (!showFeature) {
localStorage.setItem(featureName, 'on');
} else {
localStorage.removeItem(featureName);
}
setShowFeature(!showFeature);
}
window.addEventListener(featureName, handler);
return () => {
window.removeEventListener(featureName, handler);
};
}, [showFeature, setShowFeature, featureName]);
return showFeature;
}
import { renderHook } from '@testing-library/react-hooks';
import { act } from 'react-dom/test-utils';
import useFeatureToggle from './useFeatureToggle';
describe('useFeatureToggle', function () {
test('should start out false initially', function () {
const { result } = renderHook(() => useFeatureToggle('TEST'));
expect(result.current).toEqual(false);
});
test('Should be true then false, and work independently of another feature toggle', async function () {
const hook1 = renderHook(() => useFeatureToggle('TEST'));
const hook2 = renderHook(() => useFeatureToggle('TEST2'));
expect(hook1.result.current).toEqual(false);
expect(hook2.result.current).toEqual(false);
await act(async function () {
window.dispatchEvent(new Event('TEST'));
});
expect(hook1.result.current).toEqual(true);
expect(hook2.result.current).toEqual(false);
await act(async function () {
window.dispatchEvent(new Event('TEST2'));
});
expect(hook1.result.current).toEqual(true);
expect(hook2.result.current).toEqual(true);
await act(async function () {
window.dispatchEvent(new Event('TEST'));
window.dispatchEvent(new Event('TEST2'));
});
expect(hook1.result.current).toEqual(false);
expect(hook2.result.current).toEqual(false);
});
test('Should persist state via localStorage', async function () {
const hook1 = renderHook(() => useFeatureToggle('TEST'));
expect(hook1.result.current).toEqual(false);
await act(async function () {
window.dispatchEvent(new Event('TEST'));
});
expect(hook1.result.current).toEqual(true);
const hook2 = renderHook(() => useFeatureToggle('TEST'));
expect(hook2.result.current).toEqual(true);
});
});
@lebbe
Copy link
Author

lebbe commented Jan 13, 2023

More generally this hook could be used as a pub/sub architecture, where independent widgets (React and others) communicate with each other via sending and listening for custom events on window. Then of course, you could send more complex data structures via the event object, and set that data object instead of flipping a simple boolean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment