Skip to content

Instantly share code, notes, and snippets.

@EvanBacon
Created June 18, 2019 22:46
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save EvanBacon/8739dc52a4dbb72e869f19b1e5cdda6c to your computer and use it in GitHub Desktop.
Save EvanBacon/8739dc52a4dbb72e869f19b1e5cdda6c to your computer and use it in GitHub Desktop.
A look at some different approaches to using hooks for pseudo classes.

Many ways to useHooks

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.

Use Case (useCase 😘)

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).

Spread 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),
    },
  ];
};

Direct Manipulation

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;
};

Direct Manipulation & Callback

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]);
};

Direct Manipulation & Spread Props

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),
    },
  ];
};

Let's just call this the JFI method

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 };
};

Summary

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>
  );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment