@hilma/tools
TypeScript icon, indicating that this package has built-in type declarations

1.1.0 • Public • Published

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;

Readme

Keywords

none

Package Sidebar

Install

npm i @hilma/tools

Weekly Downloads

350

Version

1.1.0

License

MIT

Unpacked Size

85.2 kB

Total Files

75

Last publish

Collaborators

  • hilma
  • elsasebagh