Skip to content

Instantly share code, notes, and snippets.

@stevoland
Last active January 10, 2025 14:06
Show Gist options
  • Save stevoland/a6cb19ed10c33e420b08b8e5c69b2336 to your computer and use it in GitHub Desktop.
Save stevoland/a6cb19ed10c33e420b08b8e5c69b2336 to your computer and use it in GitHub Desktop.
babel-plugin-react-hook-form-no-memo

react-hook-form (as of v7.53) may behave incorrectly when user code is compiled with react-compiler.

This babel plugin can be applied before the compiler to opt-out all functions which reference useForm by inserting the "use no memo" directive.

Only supports using the named export:

// worky
import { useForm } from 'react-hook-form
const Component = () => {
  useForm()
  return null
}

// no worky
import rhf from 'react-hook-form
const Component = () => {
  rhf.useForm()
  return null
}

Requires @babel/helper-plugin-utils

module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'./react-hook-form-no-memo',
[
'babel-plugin-react-compiler',
{
target: '18',
},
],
],
};
const { declare } = require('@babel/helper-plugin-utils')
const BabelPluginReacthookFormNoMemo = declare(({ types: t }) => ({
visitor: {
Program(programPath, pass) {
if (pass.file.opts.filename?.includes('node_modules')) {
return
}
const { bindings } = programPath.scope
for (const key in bindings) {
const binding = bindings[key]
if (
!t.isImportSpecifier(binding.path.node) ||
!t.isIdentifier(binding.path.node.imported) ||
!t.isImportDeclaration(binding.path.parentPath?.node) ||
binding.path.parentPath?.node.source.value !== 'react-hook-form' ||
binding.path.node.imported.name !== 'useForm'
) {
continue
}
binding.referencePaths.forEach((refPath) => {
const function_ = refPath.getFunctionParent()
if (!function_) {
return
}
if (!t.isBlockStatement(function_.node.body)) {
function_
.get('body')
.replaceWith(
t.blockStatement([t.returnStatement(function_.node.body)]),
)
}
if (!t.isBlockStatement(function_.node.body)) {
return
}
const directives = function_.node.body.directives ?? []
const hasManualOptOut = directives.some(
(directive) => directive.value.value === 'use no memo',
)
if (hasManualOptOut) {
return
}
function_.node.body.directives = [
...directives,
t.directive(t.directiveLiteral('use no memo')),
]
})
}
},
},
}))
module.exports = BabelPluginReacthookFormNoMemo
import { transformSync } from '@babel/core'
import ReactCompiler from 'babel-plugin-react-compiler'
import ReactHookFormNoMemo from './react-hook-form-no-memo'
const transform = (code) => {
const output = transformSync(code, {
babelrc: false,
configFile: false,
filename: 'test.js',
plugins: [
ReactHookFormNoMemo,
[
ReactCompiler,
{
target: '18',
},
],
],
})
return output.code
}
test('named import', () => {
const code = `import { useForm } from 'react-hook-form'
const Component = () => {
const form = useForm()
return null
};
`
expect(transform(code)).toBe(`import { useForm } from 'react-hook-form';
const Component = () => {
"use no memo";
const form = useForm();
return null;
};`)
})
test('local alias', () => {
const code = `import { useForm as useFormLocal } from 'react-hook-form'
const Component = () => {
const form = useFormLocal()
return null
};
`
expect(transform(code))
.toBe(`import { useForm as useFormLocal } from 'react-hook-form';
const Component = () => {
"use no memo";
const form = useFormLocal();
return null;
};`)
})
test('function without block statement', () => {
const code = `import { useForm } from 'react-hook-form'
const useThing = () => useForm()
`
expect(transform(code)).toBe(`import { useForm } from 'react-hook-form';
const useThing = () => {
"use no memo";
return useForm();
};`)
})
test('function with manual opt-out', () => {
const code = `import { useForm } from 'react-hook-form'
const useThing = () => {
"use no memo"
return useForm()
}
`
expect(transform(code)).toBe(`import { useForm } from 'react-hook-form';
const useThing = () => {
"use no memo";
return useForm();
};`)
})
test('function compiled', () => {
const code = `import { useForm } from 'something-else'
const Component = () => {
useForm()
return null
}
`
expect(transform(code)).not.toMatch(/use no memo/)
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment