Clean and Maintainable (Draft)

An exploration in clean and maintainable code.


I was once tasked with writing a tinder-style swiper and it left a mark on me. Starting off, it was pretty simple: there was a post someone wrote where they created a "Disney Tinder" game. I found the code and forked the repo here: https://github.com/johnnyboyy/disney-tinder/tree/master. The code worked well enough, but it took a long time to understand exactly what was going on. Libraries aside, this was the real confusing part:

// ...
 
const bind = useGesture(
  ({
    args: [index],
    down,
    delta: [xDelta],
    distance,
    direction: [xDir],
    velocity
  }) => {
    const trigger = velocity > 0.2;
 
    const dir = xDir < 0 ? -1 : 1;
 
    if (!down && trigger) nope.add(index);
 
    set(i => {
      if (index !== i) return;
      const isNope = nope.has(index);
 
      const x = isNope ? (200 + window.innerWidth) * dir : down ? xDelta : 0;
 
      const rot = xDelta / 100 + (isNope ? dir * 10 * velocity : 0);
 
      const scale = down ? 1.1 : 1;
      return {
        x,
        rot,
        scale,
        delay: undefined,
        config: { friction: 50, tension: down ? 800 : isNope ? 200 : 500 }
      };
    });
 
    if (!down && nope.size === data.length)
      setTimeout(() => nope.clear() || set(i => to(i)), 600);
  }
);
 
// ...

Luckily, most of this code was from an example created by the react-with-gesture (now @use-gesture/react). The code itself is still too complicated, but at least there's some comments to help understand what's going on. From the Code Sandbox:

// ...
 
const bind = useDrag(({ args: [index], active, movement: [mx], direction: [xDir], velocity: [vx] }) => {
  const trigger = vx > 0.2 // If you flick hard enough it should trigger the card to fly out
  if (!active && trigger) gone.add(index) // If button/finger's up and trigger velocity is reached, we flag the card ready to fly out
  api.start(i => {
    if (index !== i) return // We're only interested in changing spring-data for the current spring
    const isGone = gone.has(index)
    const x = isGone ? (200 + window.innerWidth) * xDir : active ? mx : 0 // When a card is gone it flys out left or right, otherwise goes back to zero
    const rot = mx / 100 + (isGone ? xDir * 10 * vx : 0) // How much the card tilts, flicking it harder makes it rotate faster
    const scale = active ? 1.1 : 1 // Active cards lift up a bit
    return {
      x,
      rot,
      scale,
      delay: undefined,
      config: { friction: 50, tension: active ? 800 : isGone ? 200 : 500 },
    }
  })
  if (!active && gone.size === cards.length)
    setTimeout(() => {
      gone.clear()
      api.start(i => to(i))
    }, 600)
})
 
// ...

I prefer to have the code explain itself since the comments can easily become outdated. I believe it was Douglas Crockford that said, "comments are not checked by the compiler." I felt a strong pull to get this code expressed properly. I'm hoping also that I can express what I've done and why well enough as I'm still new to writing in general...

I started off trying to rename some of these variables and create some "helper variables" that could describe better what was going on technically. An example from the final code being:

({ down: isHeld, movement: [xMovement], direction: [xDirection], velocity: [xVelocity] }) => {
  const swipedHardEnough = xVelocity > 0.2;
  const direction = xDirection < 0 ? "left" : "right";
  const swiped = !isHeld && swipedHardEnough;
 
  if (isHeld) {
    onSwiping(direction, xMovement);
  } else if (swiped) {
    onSwiped(direction);
  } else {
    onSwiping(undefined, xMovement);
  }
}

At the time, the "rotation" bits were still included and it became clear to me they needed to be two separate components. I can't say exactly why or what the feeling is, but I hope the code can speak better to it than my words:

Rotatable

Each card is skewed a little based on it's position in the Deck, and the top most card is rotated into the straight up-and-down position. In the original code, the rotation and swiping are handled at the same time and in the same place, but it was calling out to be separated.

function Rotatable({
	style,
	rotation,
	children,
	...rest
}: { rotation: SpringValue<number> } & PropsWithChildren<ComponentProps<typeof animated.div>>) {
	return (
		<animated.div
			{...rest}
			style={{
				...style,
				rotate: to([rotation], (newRotation) => {
					return `${newRotation}deg`;
				}),
			}}
		>
			{children}
		</animated.div>
	);
}

Swipeable

This was the code that handled the swiping action. I was eventually able to split this out and give the variables and props some better names for clarity.

function Swipeable({
	xPosition,
	onSwiping,
	onSwiped,
	style,
	children,
	...rest
}: {
	xPosition: SpringValue<number>;
	onSwiping: (direction: "left" | "right" | undefined, xMovement: number) => void;
	onSwiped: (direction: "left" | "right") => void;
} & PropsWithChildren<ComponentProps<typeof animated.div>>) {
	const gestureProps = useDrag(
		({ down: isHeld, movement: [xMovement], direction: [xDirection], velocity: [xVelocity] }) => {
			const swipedHardEnough = xVelocity > 0.2;
			const direction = xDirection < 0 ? "left" : "right";
			const swiped = !isHeld && swipedHardEnough;
 
			if (isHeld) {
				onSwiping(direction, xMovement);
			} else if (swiped) {
				onSwiped(direction);
			} else {
				onSwiping(undefined, xMovement);
			}
		},
	);
 
	return (
		<animated.div
			{...rest}
			style={{
				...style,
				transform: to([xPosition], (newX) => {
					return `translate3d(${newX}px, 0px ,0)`;
				}),
			}}
		>
			<div className="flex touch-none select-none items-center justify-center" {...gestureProps()}>
				<div className="max-w-[250px]">{children}</div>
			</div>
		</animated.div>
	);
}