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.

Revisions

  1. stevoland revised this gist Oct 10, 2024. 2 changed files with 17 additions and 8 deletions.
    16 changes: 11 additions & 5 deletions react-hook-form-no-memo.js
    Original file line number Diff line number Diff line change
    @@ -34,18 +34,24 @@ const BabelPluginReacthookFormNoMemo = declare(({ types: t }) => ({
    t.blockStatement([t.returnStatement(function_.node.body)]),
    )
    }
    const directives = function_.node.body?.directives
    const hasManualOptOut = directives?.some(

    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_
    .get('body')
    .unshiftContainer('body', t.stringLiteral('use no memo'))
    function_.node.body.directives = [
    ...directives,
    t.directive(t.directiveLiteral('use no memo')),
    ]
    })
    }
    },
    9 changes: 6 additions & 3 deletions react-hook-form-no-memo.test.js
    Original file line number Diff line number Diff line change
    @@ -32,7 +32,8 @@ test('named import', () => {
    `
    expect(transform(code)).toBe(`import { useForm } from 'react-hook-form';
    const Component = () => {
    "use no memo"
    "use no memo";
    const form = useForm();
    return null;
    };`)
    @@ -50,7 +51,8 @@ test('local alias', () => {
    expect(transform(code))
    .toBe(`import { useForm as useFormLocal } from 'react-hook-form';
    const Component = () => {
    "use no memo"
    "use no memo";
    const form = useFormLocal();
    return null;
    };`)
    @@ -63,7 +65,8 @@ test('function without block statement', () => {
    `
    expect(transform(code)).toBe(`import { useForm } from 'react-hook-form';
    const useThing = () => {
    "use no memo"
    "use no memo";
    return useForm();
    };`)
    })
  2. stevoland renamed this gist Oct 10, 2024. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. stevoland created this gist Oct 10, 2024.
    23 changes: 23 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,23 @@
    [react-hook-form](https://github.com/react-hook-form/react-hook-form) (as of v7.53) may behave incorrectly when user code is compiled with [react-compiler](https://react.dev/learn/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:

    ```ts
    // 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`
    16 changes: 16 additions & 0 deletions babel.config.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,16 @@
    module.exports = {
    presets: [
    '@babel/preset-env',
    '@babel/preset-react',
    '@babel/preset-typescript',
    ],
    plugins: [
    './react-hook-form-no-memo',
    [
    'babel-plugin-react-compiler',
    {
    target: '18',
    },
    ],
    ],
    };
    55 changes: 55 additions & 0 deletions react-hook-form-no-memo.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,55 @@
    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)]),
    )
    }
    const directives = function_.node.body?.directives
    const hasManualOptOut = directives?.some(
    (directive) => directive.value.value === 'use no memo',
    )

    if (hasManualOptOut) {
    return
    }

    function_
    .get('body')
    .unshiftContainer('body', t.stringLiteral('use no memo'))
    })
    }
    },
    },
    }))

    module.exports = BabelPluginReacthookFormNoMemo
    97 changes: 97 additions & 0 deletions react-hook-form-no-memo.test.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,97 @@
    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/)
    })