Write your React (or Preact) hooks as JavaScript classes.
"Say what????"
I know. Sounds nuts, but hear me out. Hooks can be a little surprising with their sharp pointy edges. Maybe you've been bitten by weird synchronization issues with props and stale state. Or maybe the scoping has bitten you. Or maybe you miss the more readable class lifecycle api. Maybe you don't like scrolling to the bottom of your effects to know when they run. And sweet merciful mother all that memoization madness!
If any of this rings true with you, then give baitshop a try! For all their edges and gotchas hooks are fantastic for composing behavior in your components. But just because you use hooks to compose behavior doesn't mean you need to use them directly.
Here's what using baitshop looks like when creating a basic fetch hook in JavaScript:
import { Hook, createHook } from "baitshop"
class Fetcher extends Hook {
getInitialState() {
return {
stage: "loading",
response: null,
error: null,
}
}
onChange() {
// refetch if any prop changes (in this case, if the url changes)
this.doFetch();
}
doFetch() {
const { url } = this.props;
this.setState({ stage: "loading", response: null, error: null });
fetch(url)
.then((response, error) => {
// prevent setting state if the url has changed while we were fetching
if (url != this.props.url) return;
if (error) this.setState({ stage: "error", response: null, error });
else this.setState({ stage: "ready", response, error: null });
});
}
getActions() {
// allow users to refetch manually--no memoization needed!
return {
refetch: () => this.doFetch()
};
}
}
const useFetcher = createHook(Fetcher);
function CuteDoggo({ id }) {
const { stage, response, error, refetch } = useFetcher({ url: `/pics/of/dogs/${id}` });
...
}
Naturally there's a great deal more you can do with baitshop. And if you prefer using TypeScript, then you're in luck as baitshop is written and fully typed with TypeScript!
Follow these instructions to get baitshop setup in your project. And then scroll on down to the documentation to get a run down on baitshops API and some nifty examples to set you off.
Baitshop is written with both React and Preact hooks -- so it can natively support one or the other. Each version is built seperately and neither libraries are bundled with Baitshop so the choice is entirely yours which version you want to use. Both React and Preact are marked as peer dependencies, but you'll only need to have the one you use installed.
First you'll need to install baitshop with npm:
npm install baitshop
Or if you prefer yarn:
yarn install baitshop
That's it. Nice work! ✋You've successfully installed baitshop.
React is the default version supported by Baitshop, so you can import it like so:
import { Hook, createHook } from "baitshop";
Or you can be more specific and import directly from the React build:
import { Hook, createHook } from "baitshop/react";
To import and use Baitshop with Preact, you'll need to import the Preact build:
import { Hook, createHook } from "baitshop/preact";
The core export is the Hook
class which all your baitshop hooks should extend.
In addition, there are two hook creation functions: createHook()
and createSharedHook()
. You will use one or both of these to create hooks which will use your custom Hooks.
The Hook is the core class you should extend and it forms the scaffolding from which you can create any kind of hook you want. You can think of it like a mini class-based component that doesn't render anything and can be composed in parallel to other Hooks. Like in React components: Hooks have props, state, and various lifecycle methods.
Hooks use TypeScript generics to allow you to type out your props (P), state (S), and actions (A). All the generics are optional though and default to an empty object.
Props are the external inputs to your Hook and are like React props in that they're an object, and changes to them can be "listened" for and can trigger internal changes.
State in the Hook class works very much like state in React.Component. It's an object, and setting state merges your updates into the existing state rather than overwrite it completely.
Generally this.bait
is for consumers of your Hook, and is not really needed inside the Hook instance. Your Hook's state
and actions
are merged together into a single object which is returned from your hook function. More broadly: your bait is the public api for your hook.
The update method is much like React's forceUpdate in that it will trigger a rerender of the component, but it is different in that it does not trigger any methods directly in the Hook but instead triggers the external/downstream components to update. This method is called automatically anytime you set the state of the Hook using setState
.
This method works just like React's setState except that it does not accept a functional setter. But it does merge the update into the existing state using the spread operator.
This method is only called once during the initialization phase of the Hook lifecycle and should return an object that contains the Hooks initial state. If this method is not defined, state
will default to an empty object.
This method lets you define the external, functional api for your Hook. It is called only once during the initialization phase of the Hook lifecycle, and should return an object who's keys map to functions (either defined on the Hook itself or arrow functions). Baitshop does not auto-bind these actions for you, so it's up to you to ensure your action's have the appropriate this
binding.
If you need to hook into the component onMount event, then this method is the place to do it. If your Hook will do things reliant on the changing value of props, you should use the onChange
method instead. Otherwise if you call a function in your onMount
, and then call that same function in your onChange
then it will result in that function being called twice during the initialization phase which is probably not what you want.
The onUnmount
method is your cleanup method which will be called once when the component the Hook is in unmounts. Note that prior to calling onUnmount
, both the update
and setState
methods become noops that will do nothing when called so you don't have to worry about asynchronously calling setState on an unmounted component.
This method is called unconditionally every update time the parent component renders. This means if you're composing with other React hooks or even other baitshop Hooks: you'll need to call them inside this method to avoid breaking the rule of hooks and causing bugs in your app.
react-hooks/rules-of-hooks
eslint rule: consider using the baitshop version eslint-plugin-baitshop-hooks instead. React's rules-of-hooks
rule does not account for hooks written as classes and will error should you use a hook in the onRender method. The baitshop version of the rule lints for all the same rules of hooks and is mostly a copy of react's with a few necessary tweaks.
This method is called during any render in which the didPropsChange
method call returns true. If you haven't changed the default didPropsChange
then this method will be called during the initialization phase of the hook and it's prevProps will be an empty object.
This method is used to determine if the props have changed since the last time the Hook was rendered. By default this method performs a shallow comparison between the old and new props, but can be overriden to support other strategies. This method is called in every render pass, including the initialization phase of the hook.
This is the method baitshop uses to determine if an a call to setState(update)
is actually changing the state or not. By default this method performs a shallow comparison between the update and the existing state using the keys being updated, but can be overriden to support other strategies. This method is called anytime setState
is called.
The createHook function is how you take a Hook class and create a function that can be used as a hook in your components.
The createSharedHook function works just like how it says. It returns an array with the first element as a React context provider for you to mount anywhere in your component tree, and the second as a hook for your use in any descendant node under the context provider. Each instance of the useSharedHook will then return the bait
object of the shared Hook class instance. Any props you want to use in the shared Hook must be passed into the SharedHookProvider
, as props passed into the useSharedHook
call will be ignored.
-
set
this.props
-
call
getInitialState()
-
set
this.state
-
call
getActions()
-
set
this.bait
-
your contructor body runs
-
call
onMount()
-
call
didPropsChange(prevProps)
- call
onChange(prevProps)
- call
-
call
onRender()
-
set
this.props
-
call
didPropsChange(prevProps)
- call
onChange(prevProps)
- call
-
call
onRender()
-
change
update()
to a noop -
call
onUnmount()
-
-
set
this.state
-
set
this.bait
-
call
update()
-
Here are some "recipes" to help you customize your hook to get the behavior you want.
class Custom extends Hook<P, S, A> {
didPropsChange(): boolean {
return false;
}
}
class Custom extends Hook<P, S, A> {
onRender(): void { ... }
}
class Custom extends Hook<P, S, A> {
getInitialState(): S {
return {
yourState: "here"
somethingElse: this.props.bar;
}
}
}
class Custom extends Hook<P, S, A> {
// you don't have to do anything!
}
class Custom extends Hook<P, S, A> {
getActions(): A {
return {
doAThing: this.customAction,
anotherAction: this.anotherAction,
};
}
customAction(...args) { ... }
anotherAction() { ... }
}
class Custom extends Hook<P, S, A> {
onMount(): void { ... }
onUnmount(): void { ... }
}
class Custom extends Hook<P, S, A> {
onChange(prevProps: P): void { ... }
}
class Custom extends Hook<P, S, A> {
didPropsChange(prevProps: P): boolean {
return this.props.user.id !== prevProps.user.id;
}
onChange(prevProps: P): void { ... }
}
class Custom extends Hook<P, S, A> {
onRender(): void {
const a = useSomeHookYouDontWantToRewrite();
const b = useAnotherBaitshopHookEven();
// remember that all the rules of hooks still apply here
}
}
Contributers are welcome. If you see something that could be fixed or improved then please open an issue for it.
Oh, I'm so glad you asked. Because a baitshop is a hooks store...
BSD 3-Clause License
Copyright (c) 2020 Aaron Goin, All rights reserved.