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:
- A context for the
ThemeStore
- A provider that we can wrap our application with
- 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:
-
state
, the actual state, just like withuseState
-
setState
, which sets the value of the state and returns a promise with the new state -
getState
, which returns a promise of the state's current value no matter when. This is needed because thestate
variable is not always up to date with the latest state inside of auseEffect
.
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 withinparents
- 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;