Skip to content

Collection of react utilities curated by the Rainbow Cafe~ ❤️

License

Notifications You must be signed in to change notification settings

ribbon-studios/react-utils

Repository files navigation

NPM Version NPM Downloads Coveralls

CI Build Maintainability Semantic Release Code Style: Prettier

React Utils 🔧

Collection of react utilities curated by the Rainbow Cafe~

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`} />;
}

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;
}

defer

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

export async function loader() {
  // Properly maps the types so our 'useLoaderData' type wrapper can get them!
  return defer({
    hello: 'world',
    hallo: Promise.resolve('welt'),
  });
}

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

  return value.hello;
}

<Await/>

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

export async function loader() {
  return defer({
    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

hooked

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

const useMyHook = (value: string) => useMemo(() => value, [value]);

const HookedComponent = hooked(useMyHook);

it('should ...', async () => {
  // Properties are forwarded to your component as you'd expect
  render(<HookedComponent hook="Hello world!" />);

  expect(screen.getByText('Hello world!')).toBeTruthy();
});

Multiple Arguments

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

const useMyHook = (value: string, otherValue: string) => useMemo(() => `${value} ${otherValue}`, [value, otherValue]);

const HookedComponent = hooked(useMyHook);

it('should ...', async () => {
  // Properties are forwarded to your component as you'd expect
  render(<HookedComponent hook={['Hello world!', 'Hallo welt!']} />);

  expect(screen.getByText('Hello world! Hallo welt!')).toBeTruthy();
});

Type Map

Type Output Example
string string 'hello world'
number number.toString() '1'
boolean boolean.toString() 'true'
undefined '<undefined>' '<undefined>'
object JSON.stringify(object) '{ "hello": "world" }'
array JSON.stringify(array) '["hello", "world"]'

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-async';
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 Rainbow Cafe~ ❤️

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published