use-imported-hook
Description
This package allows you to dynamically import any hook in a React component!
// MyComponent.jsx (importer)
import useImportedHook from 'use-imported-hook/hook'
export default function MyComponent() {
const [load, setLoad] = useState(false)
useImportedHook(
load && import('./useLazyHook.jsx'),
)
return (
<button onClick={() => setLoad(true)}>
Click me
</button>
)
}
// useHook.jsx (importer)
import useImportedHook from 'use-imported-hook/hook'
export default function useHook({load, ...props}) {
return useImportedHook(
load && import('./useLazyHook.jsx'),
props
)
}
// useLazyHook.jsx (importee)
import { useEffect, useCallback, useState } from 'react'
/* @__IMPORTABLE_HOOK__ */
export default function useLazyHook({a, b, c}) {
const [d, setD] = useState(false)
useEffect(() => {
// ...
}, [a, b])
return useCallback(() => {
// ...
}, [c])
}
In the examples above, useLazyHook
will only be loaded if load
is true. This allows you to defer the loading of most of your components' logic (everything that isn't needed for the initial render).
Setup
-
install the package
npm i --save use-imported-hook
-
add the Babel plugin to your config
// .babelrc.js module.exports = { // ... plugins: [ 'module:use-imported-hook' ] }
useImportedHook
Syntax for useImportedHook<T, U>(
importPromise: false | Promise<{default: (args: T) => U}>,
parameters: T?,
defaultReturn: U?
): U
Argument | Required | Example |
---|---|---|
importPromise |
true |
bool && import('./path.jsx') |
parameters |
false |
{ a, b } |
defaultReturn |
false |
"" |
Parameters
-
importPromise
(required)can either be falsy in which case the hook won't be loaded, or it can be a the promise returned by
import()
. By using it in combination withimport()
, webpack is able to package the hook in a separate chunk and to load it on demand.-
❗ The path passed toimport()
must be a relative path for babel to resolve it properly❌ useImportedHook(bool && import('/src/hooks/useHook.jsx'))
❌ useImportedHook(bool && import('@alias/useHook.jsx'))
✅ useImportedHook(bool && import('./useHook.jsx'))
-
❗ The path passed toimport()
must be a string literal for babel to run a static code analysis❌ useImportedHook(bool && import(`./${hook}.jsx`))
✅ useImportedHook(bool && import('./useHook.jsx'))
-
-
parameters
(optional)is optional and will default to
{}
. Do note that it is a single argument, so if you need to pass more than one thing to your hook, you can use{a, b, c}
or[a, b, c]
. -
defaultReturn
(optional)The return value of
useImportedHook
as long as the hook hasn't loaded yet.
Return value
-
While
importPromise
is either falsy or pending,useImportedHook
returnsdefaultReturn
. -
Once
importPromise
is truthy and resolves,useImportedHook
returns whatever the imported hook returns.
Syntax for the imported hook
-
The function containing all the built-in hooks must be the default export
❌ function withHooks() { ❌ useEffect(() => {/*...*/}) ❌ } ❌ export default function() { ❌ withHooks() ❌ }
✅ function withHooks() { ✅ useEffect(() => {/*...*/}) ✅ } ✅ export default withHooks
-
The function containing all the built-in hooks must be labeled with a leading comment containing the exact string
@__IMPORTABLE_HOOK__
/* @__IMPORTABLE_HOOK__ */ export default function() { useEffect(() => {/*...*/}) }
-
All of your built-in hooks must be in a single function
❌ function moreStuff() { ❌ useEffect(() => { ❌ // ... ❌ }) ❌ } /* @__IMPORTABLE_HOOK__ */ export default function useLazyHook() { ❌ moreStuff() return useCallback(() => { /* ... */ }) }
/* @__IMPORTABLE_HOOK__ */ export default function useLazyHook() { ✅ useEffect(() => { ✅ // ... ✅ }) return useCallback(() => { /* ... */ }) }
-
An imported hook can't contain a call to
useImportedHook
/* @__IMPORTABLE_HOOK__ */ export default function useLazyHook() { ❌ useImportedHook(bool && import('./useOtherHook.jsx')) return useCallback(() => { /* ... */ }) }
-
Not all initial values for
useState
anduseRef
can be extracted statically- Allowed initial values
-
✅ true
andfalse
-
✅ 0
,1
,2
... (all integers) -
✅ 0.5
,.1
... (all floats) -
✅ ""
,"hello world"
(all strings) -
✅ {}
(empty object) -
✅ []
(empty array) -
✅ {a: 1}
(non-empty objects if values are themselves allowed values) -
✅ [0, 1]
(non-empty arrays if items are themselves allowed values) -
✅ !0
,-1
(unary expressions) -
✅ null
,undefined
,NaN
,Infinity
-
- Forbidden initial values
-
❌ myVar
(variable identifiers) -
❌ `hello ${"world"}`
(template literals) -
❌ () => {}
(functions and arrow functions)
-
- Allowed initial values
Limitation: forbidden built-in hooks
Currently, only a subset of all built-in Hooks in React are supported inside the imported hook:
-
✅ useEffect
-
✅ useCallback
-
✅ useMemo
-
✅ useLayoutEffect
-
✅ useImperativeHandle
-
✅ useDebugValue
-
✅ useState
-
✅ useRef
-
❌ useReducer
-
❌ useContext
If your imported hook needs to use unsupported built-in hooks, the best approach is to declare the unsupported hooks before useImportedHook
and pass them as arguments:
// useHook.jsx (importer)
import { useContext } from 'react'
import useImportedHook from 'use-imported-hook/hook'
export default function useHook({shouldLoad, ...props}) {
const a = useContext(MyContext)
useImportedHook(
shouldLoad && import('./useLazyHook.jsx'),
{...props, a}
)
}
// useLazyHook.jsx (importee)
import { useEffect } from 'react'
/* @__IMPORTABLE_HOOK__ */
export default function useLazyHook({a, b}) {
useEffect(() => {
console.log(a) // value of `MyContext` provider
}, [a, b])
}
useImportedHook
per component
Limitation: multiple
Currently, we don't support importing several hooks from within a single component (or hook).
export default function MyComponent() {
❌ useImportedHook(a && import('./hook1.jsx'))
❌ useImportedHook(b && import('./hook2.jsx'))
return <></>
}
Managing Webpack chunks
If you have several components that will load their hooks at the same time, you can give a clue to webpack to package them together in the same chunk:
export default function ChatComponent({userLoggedIn}) {
useImportedHook(userLoggedIn && import(
/* webpackMode: "lazy-once" */
/* webpackChunkName: "user-logged-in" */
'./useChatHook.jsx'),
)
return <></>
}
export default function AccountSettings({userLoggedIn}) {
useImportedHook(userLoggedIn && import(
/* webpackMode: "lazy-once" */
/* webpackChunkName: "user-logged-in" */
'./useSettingsHook.jsx'),
)
return <></>
}
In the above example, webpack will put both useChatHook.jsx
and useSettingsHook.jsx
in the same .js chunk file.