Skip to content

ribbon-studios/react-utils

NPM Version NPM Downloads Coveralls

CI Build Maintainability Semantic Release Code Style: Prettier

React Utils 🔧

Collection of react utilities curated by Ribbon Studios Team~

Hooks

useCachedState

import { useCachedState } from '@ribbon-studios/react-utils';

export type MySimpleInputProps = {
  value?: string;
};

export function MySimpleInput({ value: externalValue }: MySimpleInputProps) {
  // This is a utility for keeping external properties in-sync with the internal state
  const [value, setValue] = useCachedState(() => externalValue, [externalValue]);

  return <input value={value} onChange={(event) => setValue(event.target.value)} />;
}

useSubtleCrypto

import { useSubtleCrypto } from '@ribbon-studios/react-utils';

export type ProfileProps = {
  email?: string;
};

export function Profile({ email }: ProfileProps) {
  const hashedEmail = useSubtleCrypto('SHA-256', email);

  return <img src={`https://gravatar.com/avatar/${hashedEmail}.jpg`} />;
}

useLocalStorage

import { useLocalStorage } from '@ribbon-studios/react-utils';

export function Profile() {
  const [value, setValue] = useLocalStorage('hello');

  return value;
}

useSessionStorage

import { useSessionStorage } from '@ribbon-studios/react-utils';

export function Profile() {
  const [value, setValue] = useSessionStorage('hello');

  return value;
}

createThemeHook

Creates a hook that automatically updates the data-theme attribute on the body to the active theme.

// use-theme.ts
import { createThemeHook } from '@ribbon-studios/react-utils';

export const useTheme = createThemeHook({
  // Ensure you use `as const` or the types won't be correct!
  themes: ['light', 'dark'] as const,

  // This is the theme that will be used when light mode is preferred
  light: 'light',

  // This is the theme that will be used when dark mode is preferred
  dark: 'dark',
});

type Modes = typeof useTheme.$modes;

// Navigation.tsx
const THEME_LABELS: Record<Modes, string> = {
  auto: 'Auto',
  light: 'Light',
  dark: 'Dark',
};

const NEXT_THEME: Record<Modes, Modes> = {
  auto: 'light',
  light: 'dark',
  dark: 'auto',
};

export function Navigation() {
  const [mode, setMode] = useTheme();
  const nextTheme = useCallback(() => {
    setMode((mode) => NEXT_THEME[mode]);
  }, [setMode]);

  return (
    <div>
      <button onClick={nextTheme}>{THEME_LABELS[mode]}</button>
    </div>
  );
}

React Router

useLoaderData

import { useLoaderData } from '@ribbon-studios/react-utils/react-router';

export async function loader() {
  return {
    hello: 'world',
  };
}

export function Profile() {
  // No more type casting!
  const value = useLoaderData<typeof loader>();

  return value.hello;
}

<Await/>

import { useLoaderData, Await } from '@ribbon-studios/react-utils/react-router';

export async function loader() {
  return Promise.resolve({
    greetings: Promise.resolve(['hello world', 'hallo welt']),
  });
}

export function Profile() {
  const data = useLoaderData<typeof loader>();

  return (
    <Await resolve={data.greetings}>
      {/* Retains the type! */}
      {(greetings) => (
        <>
          {greetings.map((greeting, i) => (
            <div key={i}>{greeting}</div>
          ))}
        </>
      )}
    </Await>
  );
}

Testing Utilities

wrap

This utility is more for testing purposes to easily create wrappers for other components.

import { wrap } from '@ribbon-studios/react-utils';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const Router = wrap(MemoryRouter);
const ReactQuery = wrap(QueryClientProvider, () => ({
  client: new QueryClient(),
}));

it('should ...', async () => {
  const Component = await Router(ReactQuery(import('../MyComponent.tsx')));

  // Properties are forwarded to your component as you'd expect
  render(<Component value="Hello world!" />);

  // ...
});

wrap.concat

Helper function for wrappers that combines them together, useful if you need the whole kitchen sink!

import { wrap } from '@ribbon-studios/react-utils';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const Router = wrap(MemoryRouter);
const ReactQuery = wrap(QueryClientProvider, () => ({
  client: new QueryClient(),
}));

const KitchenSink = wrap.concat(Router, ReactQuery);

it('should ...', async () => {
  const Component = await KitchenSink(import('../MyComponent.tsx')));

  // Properties are forwarded to your component as you'd expect
  render(<Component value="Hello world!" />);

  // ...
});

Built-Ins

We have a variety of wrappers for libraries built-in to simplify testing!

import { HelmetProvider } from '@ribbon-studios/react-utils/react-helmet';
import { QueryClientProvider } from '@ribbon-studios/react-utils/react-query';
import { MemoryRouter } from '@ribbon-studios/react-utils/react-router';

const KitchenSink = wrap.concat(HelmetProvider, QueryClientProvider, MemoryRouter);

it('should ...', async () => {
  const Component = await KitchenSink(import('../MyComponent.tsx')));

  render(<Component value="Hello world!" />);

  // ...
});

Want to Contribute?

About

Collection of react utilities curated by the Ribbon Studios Team~ ❤️

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors