refactor(react): create shared component factory for reusable architecture

- Extract toConnectedComponent into shared utility with full TypeScript generics
- Create src/utils/component-factory.tsx with generic types:
  - StateHookFn, PropsHookFn, RenderFn for type safety
  - Generic toConnectedComponent factory function
  - ConnectedComponent type helper for inference
- Update PlayButton and MuteButton to use shared factory
- Remove duplicate toConnectedComponent implementations
- Add proper JSDoc documentation for the component factory pattern
- Fix TypeScript indexing issues with Record<string, any> typing
- Maintains full type safety while eliminating code duplication

This follows Adobe React Spectrum and Base UI architectural patterns for
maximum reusability and extensibility across components.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2025-09-08 17:15:40 -07:00
committed by Christian Pillsbury
co-authored by Claude
parent 26998a1217
commit 38215df70b
3 changed files with 62 additions and 40 deletions
@@ -1,5 +1,6 @@
import { useMediaDispatch, useMediaSelector } from '@vjs-10/react-media-store';
import * as React from 'react';
import { toConnectedComponent } from '../utils/component-factory';
export const useMuteButtonState = (_props: any) => {
/** @TODO Fix type issues with hooks (CJP) */
@@ -31,7 +32,7 @@ export const useMuteButtonProps = (
props: React.PropsWithChildren<{ [k: string]: any }>,
state: ReturnType<typeof useMuteButtonState>,
) => {
const baseProps = {
const baseProps: Record<string, any> = {
/** data attributes/props - non-boolean */
['data-volume-level']: state.volumeLevel,
/** @TODO Need another state provider in core for i18n (CJP) */
@@ -79,25 +80,6 @@ export const renderMuteButton = (
export type renderMuteButton = typeof renderMuteButton;
export const toConnectedComponent = (
useStateHook: useMuteButtonState,
usePropsHook: useMuteButtonProps,
defaultRender: renderMuteButton,
displayName: string,
) => {
const ConnectedComponent = ({
render = defaultRender,
...props
}: MuteButtonProps & { render: renderMuteButton }) => {
const connectedState = useStateHook(props);
const connectedProps = usePropsHook(props, connectedState);
return render(connectedProps, connectedState);
};
ConnectedComponent.displayName = displayName;
return ConnectedComponent;
};
export const MuteButton = toConnectedComponent(
useMuteButtonState,
useMuteButtonProps,
@@ -1,5 +1,6 @@
import { useMediaDispatch, useMediaSelector } from '@vjs-10/react-media-store';
import * as React from 'react';
import { toConnectedComponent } from '../utils/component-factory';
export const usePlayButtonState = (_props: any) => {
/** @TODO Fix type issues with hooks (CJP) */
@@ -29,7 +30,7 @@ export const usePlayButtonProps = (
props: React.PropsWithChildren<{ [k: string]: any }>,
state: ReturnType<typeof usePlayButtonState>,
) => {
const baseProps = {
const baseProps: Record<string, any> = {
/** @TODO Need another state provider in core for i18n (CJP) */
/** aria attributes/props */
role: 'button',
@@ -75,25 +76,6 @@ export const renderPlayButton = (
export type renderPlayButton = typeof renderPlayButton;
export const toConnectedComponent = (
useStateHook: usePlayButtonState,
usePropsHook: usePlayButtonProps,
defaultRender: renderPlayButton,
displayName: string,
) => {
const ConnectedComponent = ({
render = defaultRender,
...props
}: PlayButtonProps & { render?: renderPlayButton }) => {
const connectedState = useStateHook(props);
const connectedProps = usePropsHook(props, connectedState);
return render(connectedProps, connectedState);
};
ConnectedComponent.displayName = displayName;
return ConnectedComponent;
};
export const PlayButton = toConnectedComponent(
usePlayButtonState,
usePlayButtonProps,
@@ -0,0 +1,58 @@
import * as React from 'react';
/**
* Generic types for the component factory pattern
*/
export type StateHookFn<TProps = any, TState = any> = (props: TProps) => TState;
export type PropsHookFn<TProps = any, TState = any, TResultProps = any> = (
props: TProps,
state: TState,
) => TResultProps;
export type RenderFn<TProps = any, TState = any> = (
props: TProps,
state: TState,
) => React.ReactElement;
/**
* Generic factory function to create connected components following the hooks pattern
* inspired by Adobe React Spectrum and Base UI architectures.
*
* @param useStateHook - Hook that provides component state
* @param usePropsHook - Hook that enhances props with state-derived values
* @param defaultRender - Default render function for the component
* @param displayName - Display name for React DevTools
* @returns Connected component with customizable render prop
*/
export const toConnectedComponent = <
TProps extends Record<string, any>,
TState,
TResultProps extends Record<string, any>,
TRenderFn extends RenderFn<TResultProps, TState>
>(
useStateHook: StateHookFn<TProps, TState>,
usePropsHook: PropsHookFn<TProps, TState, TResultProps>,
defaultRender: TRenderFn,
displayName: string,
) => {
const ConnectedComponent = ({
render = defaultRender,
...props
}: TProps & { render?: TRenderFn }) => {
const connectedState = useStateHook(props as TProps);
const connectedProps = usePropsHook(props as TProps, connectedState);
return render(connectedProps, connectedState);
};
ConnectedComponent.displayName = displayName;
return ConnectedComponent;
};
/**
* Type helper to infer the component type from the factory
*/
export type ConnectedComponent<
TProps extends Record<string, any>,
TRenderFn extends RenderFn<any, any>
> = React.FC<TProps & { render?: TRenderFn }>;