npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@redis-ui/components

v42.8.0

Published

This guide covers best practices, conventions, and patterns for creating new components in the `@redislabsdev/redis-ui-components` library.

Readme

Creating New Components Guide

This guide covers best practices, conventions, and patterns for creating new components in the @redislabsdev/redis-ui-components library.

Table of Contents

Radix UI Foundation

This component library is built on top of Radix UI, a low-level UI component library that provides unstyled, accessible primitives for building high-quality design systems and web applications.

Note: Not all components need Radix. Simple presentational components (like Button, Badge, Card) can be built from scratch using styled-components.

Note: When using Radix, or any other external library for that matter, make sure you don't expose Radix types in public API. Always wrap them in our own types.

Component Structure

Basic Component Anatomy

Every component should follow this folder structure:

src/ComponentName/
├── ComponentName.tsx          # Main component implementation
├── ComponentName.types.ts     # Types and interfaces
├── ComponentName.style.ts     # Styles
├── ComponentName.test.tsx     # Unit tests
├── index.ts                   # Public exports
└── components/                # Sub-components (if needed)
    └── SubComponent/
        ├── SubComponent.tsx
        ├── SubComponent.types.ts
        └── SubComponent.style.ts

The tree is recurisve, so you can have sub-components inside sub-components, etc.

Example: Simple Component

// Card.tsx
import * as S from './Card.style';
import { CardProps } from './Card.types';

const Card = (props: CardProps) => <S.Card {...props} />;

export default Card;

File Organization

1. Component File (ComponentName.tsx)

✅ DO:

  • Use React.forwardRef when the component should allow access to the DOM element underneath
    • Use forwardRefWithGenerics, if component has generics params (also works as standard forwardRef)
  • Provide default props for optional parameters
  • Import styled components as * as S for namespacing
  • Keep the main component focused and delegate to sub-components
  • Extract complex logic into separate hooks, separate different logic to separate hooks

❌ DON'T:

  • Mix business logic with presentation
  • Hardcode theme values

2. Types File (ComponentName.types.ts)

Conventions:

  • Always use separate file for types
  • Extend HTML element attributes when wrapping native elements
  • Use generics for flexible, reusable components
  • If there are sub-components, create separate types file for each of them and compose the styles at the root level
  • Use fully qualified names for exported types
  • It is preferable to split component props into Own and Rest props, which can simplify property composition and their use in special cases.
    • Use Own*Props primarily to define actively processed props and props for direct inheritance
    • Use Rest*Props for other props that you plan to pass as-is to inner components (e.g. HTMLAttributes<>)
// Button.types.ts
import { ButtonHTMLAttributes } from 'react';

export const buttonSizes = ['large', 'medium', 'small'] as const;
export const buttonVariants = ['primary', 'destructive', 'secondary-fill'] as const;

export type ButtonSizes = (typeof buttonSizes)[number];
export type ButtonVariants = (typeof buttonVariants)[number];

export type OwnButtonProps = {
  size?: ButtonSizes;
  variant?: ButtonVariants;
  disabled?: boolean;
};

export type RestButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;

export type ButtonProps = OwnButtonProps & RestButtonProps;

3. Style File (ComponentName.style.ts)

✅ DO:

  • Access theme values via useTheme().components.componentName
  • Use token maps for consistent styling (styleFromTokens)
  • Use fully qualified names for exported styled components (simplifies component debugging with React DevTools)
// Button.style.ts
import styled, { css } from 'styled-components/macro';
import { getFocusStyle, useTheme } from '@redislabsdev/redis-ui-styles';
import { ButtonProps } from './Button.types';

export const baseButtonStyle = css`
  border: 0;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;

  ${getFocusStyle()};

  &:disabled {
    cursor: not-allowed;
  }
`;

export const Button = styled.button<Required<Pick<ButtonProps, 'size' | 'variant'>>>`
  ${baseButtonStyle};
  gap: ${() => useTheme().components.button.gap};
  padding: ${({ size }) => useTheme().components.button.sizes[size].padding};
  background-color: ${({ variant }) => useTheme().components.button.variants[variant].bgColor};
`;

❌ DON'T:

  • Hardcode colors, spacing, or other design tokens
  • Use inline styles

4. Index File (index.ts)

Conventions:

  • Export the default component
  • Export all types using export *
  • Export utility hooks/functions if applicable
  • Do not export styled components

Component Composition

Note: It’s best to discuss whether composition is appropriate early on, ideally during the design phase. By the time a PR is under review, changing this decision will involve substantial changes. Feel free to reach out to FE Infra team for advice.

Benefits:

  • Allows both simple and advanced usage patterns.
    • Note that composed components should also provide simple props for non-advanced usage.
  • Provides flexibility for custom layouts
  • Maintains clean API surface
  • Supports core Design System and allows custom application styling

Object.assign Pattern

For components with sub-components and composition hierarchy, use Object.assign:

// Tabs.tsx
import { TabsProps } from './Tabs.types';
import ContentPane from './components/ContentPane/ContentPane';
import TabBar from './components/TabBar/TabBar';
import Compose from './components/Compose/Compose';

const Tabs = Object.assign(
  ({ tabs, variant, ...restProps }: TabsProps) => (
    <Tabs.Compose {...restProps}>
      <Tabs.TabBar tabs={tabs} variant={variant} />
      <Tabs.ContentPane tabs={tabs} />
    </Tabs.Compose>
  ),
  { Compose, TabBar, ContentPane }
);

export default Tabs;

Benefits:

  • The assign() function works better with TypeScript and code completion than direct assignment.

Context Pattern

For sharing state and props between component and descendants.

Create component contexts in separate file: ComponentName.context.ts(x)

For most cases you can use standard component context generator with first two returned values: context hook and provider

import { createNullableContext } from '/Helpers';

export const [useMyComponentContext, MyComponentContextProvider, MyComponentContext] =
  createNullableContext<MyComponentContextType>({
    forComponentName: 'MyComponent'
  });

There is also primitive value state context generator (see usages for details.). It generates 3 things (each of them allows update state):

  • Context hook, returning the state
  • Context Provider, holding the state
  • TransProvider, which acts as the Provider if there is no ascending Provider, or becomes transparent if there is one.
import { createPrimitiveContextState } from '/Helpers';

export const [useFieldStatus, FieldStatusProvider, FieldStatusTransProvider] =
  createPrimitiveContextState<FieldStatus>();

If none of the generators meets the requirements, a custom context can be created based on the regular React Context. In this case, export the Provider and a hook that retrieves, validates, processes the context values and returns the result.

// Component.context.ts
import { createContext, useContext } from 'react';

export const ComponentContext = createContext<ComponentContextParams>({
  variant: 'default',
  size: 'medium'
});

export const useComponentParams = (
  customParams: Partial<ComponentContextParams> = {}
): ComponentContextParams => {
  const context = useContext(ComponentContext);
  return customParams ? assignWith({}, context, customParams, skipUndefined) : context;
};

✅ DO:

  • Use context for deeply nested component communication
  • Memoize context values to prevent unnecessary re-renders
  • Provide sensible defaults

❌ DON'T:

  • Overuse context for simple prop drilling
  • Forget to memoize context values

Compose pattern

Start component life from pure composition tree.

<Tabs.Compose value={value} onChange={onChange}>
  <Tabs.TabBar.Compose>
    {tabs.map(({ value, label, ...rest }) => (
      <Tabs.TabBar.Tab key={value} value={value} {...rest}>
        {label}
      </Tabs.TabBar.Tab>
    ))}
    <Tabs.TabBar.Marker/>
  </Tabs.TabBar.Compose>
  <Tabs.ContentPane.Compose>
    {tabs.map(({ value, content }) => (
      <Tabs.ContentPane.Content key={value} value={value}>
        {content}
      </Tabs.ContentPane.Content>
    ))}
  </Tabs.ContentPane.Compose>
</Tabs.Compose>

Each non-leaf node in the tree should be Compose node.

Compose component receives configuration props, defines states, contexts and core logic and optionally adds container, styled with DS.

Leaf components remain pure renderers, reading context and their own props.

Place components in directory tree, where each node component has its own directory and all sub-components are placed in a components directory within the component directory

When composition is ready, wrap each Compose component and sub-components of the level with node component, which exposes simple configuration API

Compose and nested components should be assigned to the node component (and used in composition in it).

const Tabs = Object.assign(
  ({ tabs, ...restProps }: TabsProps) => (
    <Tabs.Compose {...restProps}>
      <Tabs.TabBar tabs={tabs} />
      <Tabs.ContentPane tabs={tabs} />
    </Tabs.Compose>
  ),
  { Compose, TabBar, ContentPane }
);

✅ DO:

  • Define context state and logic (policy/behavior/rules) only in Compose
  • Keep nested and especially leaf components focused
  • Keep node components simple (they should only contain simple composition using Compose and nested components)
  • When using Radix library, use their node components in Compose and their leaf components in leaf
  • Extract and separate complex logic into separate hooks

❌ DON'T:

  • Add logic, contexts, states to node components
  • Make leaf components overcomplicated
  • Overload composition hierarchy

Benefits:

  • Configuration logic doesn’t leak
  • Nested components stay focused (“lego bricks”)
  • Flexibility
    • Each NodeComponent works as configuration entry point
    • Each node can be extracted/rearranged at any level safely
  • Simple by default, powerful when needed
    • node components limit configuration options to what’s most necessary
    • if you need more tuning — go to composition
  • DS-based by default, restylable via composition
  • Consumers can build custom composition nodes that still respect invariants

Form Fields

If you're developing another form component that can be used with the FormField component, be sure to connect it with the FormField contexts (those that are applicable)

  • SharedId
  • FieldStatus
  • FieldRequired
  • FieldDisabled
  • FieldReadonly
  • FieldAdditionText

Testing

Test File Structure

// Component.test.tsx
import { render, screen } from '@testing-library/react';
import Component from './Component';

describe('Component', () => {
  describe('Basic rendering', () => {
    it('should render with default props', () => {
      render(<Component>Test content</Component>);
      expect(screen.getByText('Test content')).toBeInTheDocument();
    });
  });

  describe('Variants', () => {
    it('should render primary variant', () => {
      render(<Component variant="primary">Primary</Component>);
      expect(screen.getByText('Primary')).toBeInTheDocument();
    });
  });

  describe('Composition mode', () => {
    it('should render with composition components', () => {
      render(
        <Component.Compose>
          <Component.Header>Header</Component.Header>
          <Component.Body>Body</Component.Body>
        </Component.Compose>
      );
      expect(screen.getByText('Header')).toBeInTheDocument();
    });
  });
});

Testing Library Queries

Prefer queries in this order:

  1. getByRole (most accessible)
  2. getByLabelText
  3. getByPlaceholderText
  4. getByText
  5. getByTestId (last resort)

You can find more info in Testing Library docs.

Tips & Tricks

1. Transient Props in Styled Components

Use $ prefix for transient props:

// ✅ Good - won't pass $variant to DOM
const Button = styled.button<{ $variant: string }>`
  color: ${({ $variant }) => $variant === 'primary' ? 'blue' : 'gray'};
`;

// ❌ Bad - will pass variant to DOM and can cause warnings
const Button = styled.button<{ variant: string }>`
  color: ${({ variant }) => variant === 'primary' ? 'blue' : 'gray'};
`;

2. Theme Type Safety

Create theme types for your component in packages/styles:

// packages/styles/src/themes/types/theme/components/myComponent.types.ts
export type MyComponentTheme = {
  padding: string;
  gap: string;
  variants: {
    primary: { bgColor: string; textColor: string };
    secondary: { bgColor: string; textColor: string };
  };
};

Then add to all theme files (light, dark, light2, dark2).

3. Composition Helpers

Use helper types from Helpers:

import { ComposeElementProps, ChildFree } from '../Helpers';

// ComposeElementProps: Standard props for composition components
// ChildFree: Omits children from props
type MyProps = ChildFree<ComposeElementProps>;

4. Storybook Integration

Create stories in docs/stories/ComponentName/:

// ComponentName.stories.tsx
import { Meta, StoryObj } from '@storybook/react';
import { ComponentName } from '@redislabsdev/redis-ui-components';

const meta: Meta<typeof ComponentName> = {
  component: ComponentName,
  title: 'Components/ComponentName'
};

export default meta;
type Story = StoryObj<typeof ComponentName>;

export const Playground: Story = {
  args: {
    variant: 'primary',
    size: 'medium'
  }
};

Note: For composition components, create a separate ComponentNameCompose.mdx file.