Skip to main content

Introduction

The @zayne-labs/toolkit-react/utils module provides essential utilities for working with React components, props, refs, and events.

Installation

Utilities are available through a separate import path:
import { composeRefs, mergeProps, composeEventHandlers } from '@zayne-labs/toolkit-react/utils';

Ref Utilities

composeRefs

Combines multiple refs into a single ref callback.
import { composeRefs } from '@zayne-labs/toolkit-react/utils';
import { useRef } from 'react';

function Component({ forwardedRef }) {
  const localRef = useRef(null);
  const anotherRef = useRef(null);
  
  return (
    <div ref={composeRefs(localRef, forwardedRef, anotherRef)}>
      Content
    </div>
  );
}
import { forwardRef } from 'react';
import { composeRefs } from '@zayne-labs/toolkit-react/utils';

const Input = forwardRef<HTMLInputElement, Props>((props, ref) => {
  const internalRef = useRef<HTMLInputElement>(null);
  
  useEffect(() => {
    // Use internalRef for internal logic
    internalRef.current?.focus();
  }, []);
  
  return (
    <input
      ref={composeRefs(ref, internalRef)}
      {...props}
    />
  );
});
Type Signature:
type PossibleRef<TRef extends HTMLElement> = React.Ref<TRef> | undefined;

function composeRefs<TRef extends HTMLElement>(
  ...refs: Array<PossibleRef<TRef>>
): RefCallback<TRef>;
composeRefs handles both callback refs and RefObject refs automatically.

setRef

Sets a ref value, handling both callback and object refs.
import { setRef } from '@zayne-labs/toolkit-react/utils';

function Component() {
  const ref = useRef(null);
  
  useEffect(() => {
    const element = document.getElementById('my-element');
    setRef(ref, element);
    
    return () => setRef(ref, null);
  }, []);
  
  return <div id="my-element" />;
}
Type Signature:
function setRef<TRef extends HTMLElement>(
  ref: PossibleRef<TRef>,
  node: TRef | null
): ReturnType<RefCallback<TRef>>;

Props Utilities

mergeProps

Merges multiple props objects with special handling for className, style, and event handlers.
import { mergeProps } from '@zayne-labs/toolkit-react/utils';

function Component({ userProps }) {
  const defaultProps = {
    className: 'base-class',
    style: { color: 'blue' },
    onClick: () => console.log('default click')
  };
  
  const enhancedProps = {
    className: 'enhanced-class',
    style: { fontSize: '16px' },
    onClick: () => console.log('enhanced click')
  };
  
  // Merged props:
  // - className: 'base-class enhanced-class'
  // - style: { color: 'blue', fontSize: '16px' }
  // - onClick: calls enhanced click first, then default (unless prevented)
  const merged = mergeProps(defaultProps, enhancedProps, userProps);
  
  return <div {...merged}>Content</div>;
}
const props1 = { id: 'first', className: 'btn' };
const props2 = { className: 'btn-primary', disabled: true };

const merged = mergeProps(props1, props2);
// Result:
// {
//   id: 'first',
//   className: 'btn btn-primary',
//   disabled: true
// }
Merge Behavior:
  • Regular props: Rightmost value wins (like Object.assign)
  • className: All classes are concatenated
  • style: Styles are merged, rightmost values override
  • Event handlers: Called in sequence (right to left), can be prevented with preventDefault()
  • ref: NOT merged (use composeRefs instead)
Type Signature:
function mergeProps<TProps extends Record<never, never>>(
  ...propsObjectArray: Array<TProps | undefined>
): UnionToIntersection<TProps>;
The ref prop is not merged by mergeProps. Use composeRefs to combine refs.

Event Utilities

composeEventHandlers

Composes multiple event handlers into a single handler.
import { composeEventHandlers } from '@zayne-labs/toolkit-react/utils';

function Component({ onClick, onUserClick }) {
  const handleInternalClick = (e) => {
    console.log('Internal click');
  };
  
  const handleValidation = (e) => {
    if (!isValid) {
      e.preventDefault(); // Stops subsequent handlers
      return;
    }
    console.log('Validation passed');
  };
  
  return (
    <button
      onClick={composeEventHandlers(
        onClick,
        handleValidation,
        handleInternalClick,
        onUserClick
      )}
    >
      Click me
    </button>
  );
}
Execution Order: Handlers are called from right to left (last to first). If any handler calls preventDefault() on a SyntheticEvent, subsequent handlers are skipped.
const handler1 = () => console.log('Handler 1');
const handler2 = () => console.log('Handler 2');
const handler3 = () => console.log('Handler 3');

const composed = composeEventHandlers(handler1, handler2, handler3);

// When called:
// Handler 3
// Handler 2
// Handler 1
Type Signature:
function composeEventHandlers(
  ...eventHandlerArray: Array<AnyFunction | undefined>
): (event: unknown) => unknown;

function composeTwoEventHandlers(
  formerHandler: AnyFunction | undefined,
  latterHandler: AnyFunction | undefined
): (event: unknown) => unknown;

Type Utilities

StateSetter

Type for state setter functions (like React’s setState).
import type { StateSetter } from '@zayne-labs/toolkit-react/utils';

function useCustomState<T>(initialValue: T): [T, StateSetter<T>] {
  const [state, setState] = useState(initialValue);
  
  const customSetState: StateSetter<T> = (valueOrUpdater) => {
    console.log('Setting state');
    setState(valueOrUpdater);
  };
  
  return [state, customSetState];
}

PossibleRef

Type for refs that might be undefined.
import type { PossibleRef } from '@zayne-labs/toolkit-react/utils';

function Component({ externalRef }: { externalRef?: PossibleRef<HTMLDivElement> }) {
  const internalRef = useRef<HTMLDivElement>(null);
  
  return <div ref={composeRefs(internalRef, externalRef)} />;
}

Slot Utilities

getSlot

Extracts specific child components by type.
import { getSlot } from '@zayne-labs/toolkit-react/utils';

function Tabs({ children }) {
  const tabList = getSlot(children, TabList);
  const panels = getSlot(children, TabPanel);
  
  return (
    <div>
      {tabList}
      <div className="panels">{panels}</div>
    </div>
  );
}

getSlotMap

Creates a map of slots by component type.
import { getSlotMap } from '@zayne-labs/toolkit-react/utils';

function Form({ children }) {
  const slots = getSlotMap(children, {
    header: FormHeader,
    body: FormBody,
    footer: FormFooter
  });
  
  return (
    <form>
      {slots.header}
      <main>{slots.body}</main>
      {slots.footer}
    </form>
  );
}

Best Practices

Use composeRefs for Multiple Refs

Always use composeRefs when you need to attach multiple refs to the same element

Merge Props Carefully

Be aware of the merge order - rightmost props override left ones

Event Handler Order

Remember that composed event handlers execute from right to left

Type Safety

Use TypeScript types provided for better type inference

Common Patterns

Forwarding Refs with Internal Logic

import { forwardRef } from 'react';
import { composeRefs } from '@zayne-labs/toolkit-react/utils';

const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
  const internalRef = useRef<HTMLButtonElement>(null);
  
  useEffect(() => {
    // Internal logic using internalRef
    internalRef.current?.focus();
  }, []);
  
  return (
    <button
      {...props}
      ref={composeRefs(ref, internalRef)}
    />
  );
});

Composable Component Props

import { mergeProps } from '@zayne-labs/toolkit-react/utils';

function useButtonProps(props: ButtonProps) {
  const baseProps = {
    className: 'btn',
    role: 'button',
    tabIndex: 0
  };
  
  const variantProps = {
    className: `btn-${props.variant}`,
  };
  
  const interactionProps = {
    onClick: handleClick,
    onKeyDown: handleKeyDown
  };
  
  return mergeProps(baseProps, variantProps, interactionProps, props);
}

Sequential Event Handling

import { composeEventHandlers } from '@zayne-labs/toolkit-react/utils';

function FormField({ onChange, onValidate }) {
  const handleInternalChange = (e) => {
    // Internal state updates
    setDirty(true);
  };
  
  const handleValidation = (e) => {
    if (!validate(e.target.value)) {
      e.preventDefault();
      setError('Invalid input');
    }
  };
  
  return (
    <input
      onChange={composeEventHandlers(
        onChange,           // User handler (called last)
        handleInternalChange,  // Internal logic
        handleValidation    // Validation (called first)
      )}
    />
  );
}

Build docs developers (and LLMs) love