Collection of react utilities curated by Ribbon Studios Team~
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)} />;
}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`} />;
}import { useLocalStorage } from '@ribbon-studios/react-utils';
export function Profile() {
const [value, setValue] = useLocalStorage('hello');
return value;
}import { useSessionStorage } from '@ribbon-studios/react-utils';
export function Profile() {
const [value, setValue] = useSessionStorage('hello');
return value;
}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>
);
}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;
}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>
);
}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!" />);
// ...
});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!" />);
// ...
});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!" />);
// ...
});