agentsclimarketplace

Hooks

Skill Shankulkarni/RN-Lib-Claude/plugins/rn-lib-claude/skills/hooks

Ship production-grade RN libraries faster — scaffold, audit, and publish with Claude Code.

Install
npx -y skills add Shankulkarni/RN-Lib-Claude --skill hooks

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when building custom React hooks or utility functions for a React Native library — worklet-safe patterns, TypeScript generics, memory leak prevention, state minimization, and stable callbacks.

SKILL.md

6.4 KB, as published. Nobody here has run it

Hooks + Utilities

Hook Template

import { useCallback, useEffect, useRef, useState } from 'react'

type UseCounterOptions = {
  initialValue?: number
  min?: number
  max?: number
  onChange?: (value: number) => void
}

type UseCounterResult = {
  value: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

function useCounter({
  initialValue = 0,
  min = -Infinity,
  max = Infinity,
  onChange,
}: UseCounterOptions = {}): UseCounterResult {
  const [value, setValue] = useState(initialValue)
  const onChangeRef = useRef(onChange)
  onChangeRef.current = onChange

  const increment = useCallback(() => {
    setValue(prev => {
      const next = Math.min(prev + 1, max)
      onChangeRef.current?.(next)
      return next
    })
  }, [max])

  const decrement = useCallback(() => {
    setValue(prev => {
      const next = Math.max(prev - 1, min)
      onChangeRef.current?.(next)
      return next
    })
  }, [min])

  const reset = useCallback(() => setValue(initialValue), [initialValue])

  return { value, increment, decrement, reset }
}

export { useCounter }
export type { UseCounterOptions, UseCounterResult }

State Patterns

Minimize state — derive during render

// ❌ Redundant state — derived from other state
const [items, setItems] = useState<Item[]>([])
const [count, setCount] = useState(0)       // ← derived from items.length
const [isEmpty, setIsEmpty] = useState(true) // ← derived from items.length === 0

// ✅ Derive during render — single source of truth
const [items, setItems] = useState<Item[]>([])
const count = items.length           // derived
const isEmpty = items.length === 0   // derived

Dispatch updaters for state depending on previous value

// ❌ Stale closure — value may be outdated
const increment = () => setValue(value + 1)

// ✅ Updater function — always has latest value
const increment = useCallback(() => setValue(prev => prev + 1), [])

undefined initial state with nullish coalescing

// ❌ null initial — can't distinguish "unset by user" from "not yet loaded"
const [theme, setTheme] = useState<'light' | 'dark' | null>(null)

// ✅ undefined initial — nullish coalescing gives reactive fallback
const [theme, setTheme] = useState<'light' | 'dark' | undefined>(undefined)
const resolvedTheme = theme ?? systemTheme  // fallback to system, override when set

Memory Leak Prevention — Always Clean Up

// ❌ Leaked subscription
useEffect(() => {
  const sub = EventEmitter.addListener('change', handler)
  // missing cleanup → leak on unmount
}, [])

// ✅ Always return cleanup
useEffect(() => {
  const sub = EventEmitter.addListener('change', handler)
  return () => sub.remove()
}, [])

// ✅ Timer cleanup
useEffect(() => {
  const id = setInterval(tick, 1000)
  return () => clearInterval(id)
}, [])

// ✅ Abort fetch on unmount
useEffect(() => {
  const controller = new AbortController()
  fetch(url, { signal: controller.signal }).then(setData).catch(() => {})
  return () => controller.abort()
}, [url])

High-Frequency Values — Use useRef not useState

// ❌ Scroll position in useState — causes re-render on every scroll event
const [scrollY, setScrollY] = useState(0)
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
  setScrollY(e.nativeEvent.contentOffset.y) // fires 60x/sec → 60 re-renders/sec
}

// ✅ useRef — no re-render, track for logic use
const scrollY = useRef(0)
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
  scrollY.current = e.nativeEvent.contentOffset.y
}, [])

// ✅ useSharedValue — if driving animation (see animations skill)
const scrollY = useSharedValue(0)

Stable Callback Ref Pattern

Prevents stale closures without adding callback to deps array:

function useEventCallback<T extends (...args: never[]) => unknown>(fn: T): T {
  const ref = useRef(fn)
  ref.current = fn
  return useCallback((...args: Parameters<T>) => ref.current(...args), []) as T
}

// Usage
function useMyHook({ onComplete }: { onComplete?: (result: string) => void }) {
  const handleComplete = useEventCallback(onComplete ?? (() => {}))
  // handleComplete is stable — safe in deps array, always calls latest onComplete
}

Generics

// Generic list hook
function useSelection<T>(items: T[], keyExtractor: (item: T) => string) {
  const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set())

  const toggle = useCallback((item: T) => {
    const key = keyExtractor(item)
    setSelectedKeys(prev => {
      const next = new Set(prev)
      next.has(key) ? next.delete(key) : next.add(key)
      return next
    })
  }, [keyExtractor])

  const isSelected = useCallback(
    (item: T) => selectedKeys.has(keyExtractor(item)),
    [selectedKeys, keyExtractor],
  )

  const selectedItems = items.filter(item => selectedKeys.has(keyExtractor(item)))

  return { toggle, isSelected, selectedItems, selectedKeys }
}

Worklet-Safe Utilities

Functions callable from Reanimated worklets — must be pure, no closures over JS:

function clamp(value: number, min: number, max: number): number {
  'worklet'
  return Math.min(Math.max(value, min), max)
}

function lerp(start: number, end: number, t: number): number {
  'worklet'
  return start + (end - start) * t
}

function snapToGrid(value: number, step: number): number {
  'worklet'
  return Math.round(value / step) * step
}

Rules: no console.log, no closures over objects, no async, no React hooks, no RN APIs.


TypeScript Patterns

// Discriminated union return type
type UseAsyncResult<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'error'; error: Error }
  | { status: 'success'; data: T }

// Conditional generic for optional callback
type MaybeCallback<T> = T extends undefined ? void : (value: NonNullable<T>) => void

Export Rules

// src/index.ts
export { useCounter } from './hooks/useCounter'
export type { UseCounterOptions, UseCounterResult } from './hooks/useCounter'
  • Name: use* for hooks, verb (formatX, parseX, createX) for utilities
  • Export options type + result type alongside every hook
  • Never export internal implementation helpers

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.