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
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.
- Tools
- Installation
- Usage
- provide and wrap
- withContext
- createContextHook
- createMobXContext
- useAsyncState
- useAsyncEffect
- useLocalStorage and useSessionStorage
- ErrorBoundary
- isCapacitor
- getDisplayName
- API
- provide
- wrap
- withContext
- createContextHook
- createMobXContext
- useAsyncState
- useAsyncEffect
- useLocalStorage
- useSessionStorage
- ErrorBoundary
- isCapacitor
- getDisplayName
`` bash`
npm install @hilma/tools
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:
`tsx
import React from 'react';
const App = () => {
return (
{/ .... /}
);
}
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:__
`tsx
import React from 'react';
import { provide } from '@hilma/tools';
const App = () => {
return (
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:
`tsx
import React from 'react';
import { provide } from '@hilma/tools';
const App = () => {
return (
export default provide(
[AuthProvider, { basename: "/" }],
ThemeProvider,
[StylesProvider, { style: "dark" }]
)(App);
`
withContext is used to consume multiple contexts via props in some component.
__Example__:
`tsx
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 (
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.
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:
`tsx`
const UsernameContext = createContext
And then building our own custom useMyContext hook that can ignore this null, like so:
`tsx
// 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:
`tsx
import { createContextHook } from '@hilma/tools';
UsernameContext.displayName = "Username";
const useMyContext = createContextHook(UsernameContext);
`
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).
`tsx
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: ThemeStore
1) A context for the
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:
`tsx
import { ThemeContext, ThemeProvider, useTheme } from './ThemeContext';
const App = () => (
);
export default App;
const FuncComp = () => {
const theme = useTheme();
return (
class ClassComp {
static contextType = ThemeContext;
render() {
return (
$3
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.`tsx
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
;
}
`$3
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:`tsx
import { useAsyncEffect } from '@hilma/tools';const Comp = () => {
useAsyncEffect(async () => {
// stuff
return async () => {
// more stuff
}
}, []);
return
;
}
`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:`tsx
import { useAsyncEffect } from '@hilma/tools';const Comp = () => {
// ...
useAsyncEffect(async (isMounted) => {
await someActionThatTakesAWhile();
if (isMounted.current) setSomeState();
}, [])
return
;
}
`$3
These functions wrap
useState and create a simple API for working with JSON data stored in either localStorage or sessionStorage.`tsx
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 (
onClick={() => {
// this will update both the state (causing a re-render) and localStorage
setTheme("light");
}}
>
Change Theme
{canSendHeart && (
onClick={() => {
// this will update both the state (causing a re-render) and
sessionStorage
setCanSendHeart(false);
}}
>
Send Heart
)}
);
}
`$3
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.`tsx
const Throws: React.FC = () => {
throw new Error("ERROR");
}const App: React.FC = () => {
return (
fallback={
An Error Has Occured!}
callback={(error, info) => { ... }}
>
);
}
`$3
Returns a boolean value. If
true, the code is running in a Capacitor environment.`tsx
import { isCapacitor } from '@hilma/tools';console.log(isCapacitor());
`$3
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).`tsx
import { getDisplayName } from '@hilma/tools';const Comp = () => {
return
;
}console.log(getDisplayName(Comp)); // 'Comp'
const OtherComp = () => {
return
;
}OtherComp.displayName = 'MyComponent';
console.log(getDisplayName(OtherComp)); // 'MyComponent';
`API
$3
`typescript
export function provide(
...parents: Providers
): (
child: React.ComponentType
) => React.ComponentType;
`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]$3
`typescript
export function wrap(
...parents: Provider
): {
(element: Element) => Element;
(
component: React.ComponentType, props: TProps
) => Element;
}
`The
Providers type here is the same as for provide. See above.$3
`typescript
export function withContext(
mapContextToProps: MapContextToProps
): (
child: React.ComponentType
) => React.ComponentType>;
`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.$3
`typescript
export function createContextHook(
context: React.Context
): () => T
`$3
`typescript
export function createMobXContext(
storeInstance: T
): [React.Context, React.FC<{ children?: React.ReactNode }>, () => T]
`$3
`typescript
export function useAsyncState(
initialState: T | (() => T)
): [T, (value: T | ((prev: T) => T)) => Promise, () => Promise]
`$3
`typescript
export function useAsyncEffect(
effect: (isMounted: { current: boolean }) => (void | (() => void) | Promise void)>),
deps: React.DependencyList
): void;
`$3
`typescript
export function useLocalStorage(
key: string,
fallback: T
): [T, (value: T | ((prev: T) => T)) => void];
`$3
`typescript
export function useSessionStorage(
key: string,
fallback: T
): [T, (value: T | ((prev: T) => T)) => void];
`$3
`typescript
export interface ErrorBoundaryProps {
children?: React.ReactNode;
fallback?: React.ReactNode;
onError?: (error: unknown, info: React.ErrorInfo) => void;
};export class ErrorBoundary extends React.Component { ... };
`$3
`typescript
export function isCapacitor(): boolean
`$3
`typescript
export function getDisplayName(
component: React.ComponentType
): string;
``