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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hilma/tools

v1.1.0

Published

Tools is a package that has some useful client side tools. It makes writing client side code (particularly React boilerplate) easier and simpler. You can see more code examples in [the CodeSandbox](https://codesandbox.io/p/github/ej-shafran/hilma-tools-sa

Downloads

823

Readme

Tools

Tools is a package that has some useful client side tools. It makes writing client side code (particularly React boilerplate) easier and simpler. You can see more code examples in the CodeSandbox.

This documentation is also available in the Hilma Confluence space.

Installation

npm install @hilma/tools

Usage

provide (and wrap)

provide is a function that is used to eliminate nested providers in React code.

Often we have a component with tons of wrappers or providers, where all we care about is the component logic itself, like this:

import React from 'react';

const App = () => {
    return (
        <AuthProvider>
            <ThemeProvider>
                <StylesProvider>
                    <div className="App">
                        {/* .... */}
                    </div>
                </StylesProvider>
            </ThemeProvider>
        </AuthProvider>
    );
}

export default App;

Not even taking into account that App might want to use the context provided by AuthProvider, ThemeProvider, and the like, this code is already looking much more complicated than it should be. provide aims to simplify code like this.

With provide:

import React from 'react';
import { provide } from '@hilma/tools';

const App = () => {
    return (
        <div className="App">
            {/* .... */}
        </div>
    );
}

export default provide(AuthProvider, ThemeProvider, StylesProvider)(App);

This allows us to access the context provided by AuthProvider, ThemeProvider, etc. from inside of App, and makes our code much simpler to read and understand.

If you want to pass props to a provider, you can use a tuple where the first item is the provider and the second is the props:

import React from 'react';
import { provide } from '@hilma/tools';

const App = () => {
    return (
        <div className="App">
            {/* .... */}
        </div>
    );
}

export default provide(
    [AuthProvider, { basename: "/" }],
    ThemeProvider,
    [StylesProvider, { style: "dark" }]
)(App);

withContext

withContext is used to consume multiple contexts via props in some component.

Example:

import React, { createContext } from 'react';
import { withContext } from '@hilma/tools';

const ThemeContext = createContext("black");
const ApiContext = createContext("http://localhost:8080"):

const MyComponent: React.FC<{ theme: string; api: string; }> = (props) => {
	// instead of doing
	// const theme = useContext(ThemeContext);
	// const api = useContext(ApiContext);
	// we can just get the context values from the props
	const { theme, api } = props;

	return (
		<div>
			{/* ... */}
		</div>
	);
}

const mapContextToProps = {
    theme: ThemeContext,
    api: ApiContext
}

export default withContext(mapContextToProps)(MyComponent);

Our component here takes a theme and api prop that correlate to the values provided by ThemeContext and ApiContext. We then define a mapContextToProps object that connects the contexts to the props.

createContextHook

We often can't populate our React contexts with values when we're creating them. A common solution to this problem is to define our context's type with SomeType | null, like this:

const UsernameContext = createContext<string | null>(null);

And then building our own custom useMyContext hook that can ignore this null, like so:

// either
const useMyContext = () => useContext(UsernameContext)!;

// or
const useMyContext = () => {
	const value = useContext(UsernameContext);
	if (value === null) throw new Error("You called useUsername while not inside of the UsernameProvider");
	return value;
}

createContextHook simplifies this process by automating the second approach. So the second version of useMyContext in the example above is the same as doing:

import { createContextHook } from '@hilma/tools';

UsernameContext.displayName = "Username";
const useMyContext = createContextHook(UsernameContext);

createMobXContext

createMobXContext is a function that eliminates the boilerplate needed to use mobx with React. It works with createContextHook, and is based on the fact that when using mobx, we do have a starting value (the store itself).

import { makeAutoObservable } from 'mobx';

class ThemeStore {
    color = "dark";

    constructor() {
        makeAutoObservable(this);
    }

    setColor = color => {
        this.color = color;
    }
}

const theme = new ThemeStore();

export const [ThemeContext, ThemeProvider, useTheme] = createMobXContext(theme);

Here we pass to createMobXContext our store instance and get back a tuple with three items:

  1. A context for the ThemeStore
  2. A provider that we can wrap our application with
  3. A hook that uses that context

Let's look at an example of using these items:

import { ThemeContext, ThemeProvider, useTheme } from './ThemeContext';

const App = () => (
    <ThemeProvider>
        <FuncComp />
        <ClassComp />
    </ThemeProvider>
);

export default App;

const FuncComp = () => {
    const theme = useTheme();
    
    return (
        <div>{theme.color}</div>
    );
}

class ClassComp {
    static contextType = ThemeContext;
    
    render() {
        return (
            <div>{this.context.color}</div>
        );
    }
}

useAsyncState

useAsyncState is a hook based on useState but with some extra useful asynchronous functionality. It returns a tuple with three items:

  1. state, the actual state, just like with useState
  2. setState, which sets the value of the state and returns a promise with the new state
  3. getState, which returns a promise of the state's current value no matter when. This is needed because the state variable is not always up to date with the latest state inside of a useEffect.
import { useAsyncState } from '@hilma/tools';

const Comp = () => {
    const [color, setColor, getColor] = useAsyncState('black');
    
    const updateColorWithAwait = async (event) => {
        const newColor = await setColor(event.target.value);
        console.log(newColor);
    }

    const updateColorWithCallback = (event) => {
        setColor(event.target.value, newColor => {
            console.log(newColor);
        });
    }

    const getColorAsync = async () => {
        const color = await getColor();
        console.log(color);
    }
    
    return <div></div>;
}

useAsyncEffect

useAsyncEffect is a hook based on the useEffect hook but with some extra useful asynchronous functionality. By default, useEffect doesn't accept an async function because the function returns a promise. It also doesn't accept an async cleanup function. The useAsyncEffect allows you to do both:

import { useAsyncEffect } from '@hilma/tools';

const Comp = () => {

    useAsyncEffect(async () => {
        // stuff

        return async () => {
            // more stuff
        }
    }, []);

    return <div></div>;
}

You don't have to call useAsyncEffect with an async function or with an async cleanup function.

Because the behavior of the cleanup can be asynchronous, the cleanup might be called after the next effect (if you don't have an empty dependency array) or after the component has unmounted.

To make sure you're not updating state on an unmounted component, or updating state after the next effect has run, you can use the isMounted parameter which is passed into the callback:

import { useAsyncEffect } from '@hilma/tools';

const Comp = () => {
    // ...

    useAsyncEffect(async (isMounted) => {
        await someActionThatTakesAWhile();
        if (isMounted.current) setSomeState();
    }, [])

    return <div></div>;
}

useLocalStorage and useSessionStorage

These functions wrap useState and create a simple API for working with JSON data stored in either localStorage or sessionStorage.

const MyComponent = () => {
    // the first parameter is the key to store the data in
    // the second parameter is the value to default to, in case the data
    // doesn't yet exist or is corrupted somehow
    const [theme, setTheme] = useLocalStorage<"dark" | "light">("theme", "dark");
    const [canSendHeart, setCanSendHeart] = useSessionStorage("has-seen-popup", true);

    return (
        <div className={theme}>
            <button 
                onClick={() => {
                    // this will update both the state (causing a re-render) and `localStorage`
                    setTheme("light");
                }}
            >
                Change Theme
            </button>

            {canSendHeart && (
                <button 
                    onClick={() => {
                        // this will update both the state (causing a re-render) and `sessionStorage`
                        setCanSendHeart(false);
                    }}
                >
                    Send Heart
                </button>
            )}
        </div>
    );
}

ErrorBoundary

A component which can be used like a catch block for handling uncaught errors anywhere within it.

Note: React will still print console warnings when uncaught errors are thrown inside of an application. We recommend handling scenarios that are likely to produce errors more explicitly, and using ErrorBoundary to handle severe edge cases.

const Throws: React.FC = () => {
  throw new Error("ERROR");
}

const App: React.FC = () => {
    return (
        <ErrorBoundary
          fallback={<div>An Error Has Occured!</div>}
          callback={(error, info) => { ... }}
        >
          <Throws />
        </ErrorBoundary>
    );
}

isCapacitor

Returns a boolean value. If true, the code is running in a Capacitor environment.

import { isCapacitor } from '@hilma/tools';

console.log(isCapacitor());

getDisplayName

A function that accepts a React component and returns its name. (This is mainly for testing and better error messages for developers; don't rely on this function in production).

import { getDisplayName } from '@hilma/tools';

const Comp = () => {
    return <div></div>;
}

console.log(getDisplayName(Comp)); // 'Comp'

const OtherComp = () => {
	return <div></div>;
}

OtherComp.displayName = 'MyComponent';

console.log(getDisplayName(OtherComp)); // 'MyComponent';

API

provide

export function provide<TParents extends { [key: string]: any }[]>(
	...parents: Providers<TParents>
): <TProps>(
	child: React.ComponentType<TProps>
) => React.ComponentType<TProps>;

The Providers type maps an array of prop types to an array of either ComponentType or [ComponentType, Props], depending on which props are required and whether the props include children.

  • If a provider does not take the children prop, it cannot be used within parents
  • If a provider doesn't take any required props, it can either be passed as Component or as [Component, Props]
  • If a provider takes any required props, it must be passed as [Component, Props]

wrap

export function wrap<TParents extends { [key: string]: any }[]>(
	...parents: Provider<TParents>
): {
	(element: Element) => Element;
	<TProps>(
		component: React.ComponentType<TProps>, props: TProps
	) => Element;
}

The Providers type here is the same as for provide. See above.

withContext

export function withContext<T extends {}>(
	mapContextToProps: MapContextToProps<T>
): <TProps extends T>(
	child: React.ComponentType<TProps>
) => React.ComponentType<Omit<TProps, keyof T>>;

The MapContextToProps type takes a generic type T (some basic props object) and returns a type with that type's value, mapped to context types.

createContextHook

export function createContextHook<T>(
	context: React.Context<T | null>
): () => T

createMobXContext

export function createMobXContext<T extends { [key: string]: any }>(
	storeInstance: T
): [React.Context<T>, React.FC<{ children?: React.ReactNode }>, () => T]

useAsyncState

export function useAsyncState<T>(
	initialState: T | (() => T)
): [T, (value: T | ((prev: T) => T)) => Promise<T>, () => Promise<T>]

useAsyncEffect

export function useAsyncEffect(
	effect: (isMounted: { current: boolean }) => (void | (() => void) | Promise<void | (() => void)>),
	deps: React.DependencyList
): void;

useLocalStorage

export function useLocalStorage<T>(
    key: string,
    fallback: T
): [T, (value: T | ((prev: T) => T)) => void];

useSessionStorage

export function useSessionStorage<T>(
    key: string,
    fallback: T
): [T, (value: T | ((prev: T) => T)) => void];

ErrorBoundary

export interface ErrorBoundaryProps {
   children?: React.ReactNode; 
   fallback?: React.ReactNode;
   onError?: (error: unknown, info: React.ErrorInfo) => void;
};

export class ErrorBoundary extends React.Component<ErrorBoundaryProps> { ... };

isCapacitor

export function isCapacitor(): boolean

getDisplayName

export function getDisplayName(
	component: React.ComponentType<unknown>
): string;