million: With a parent component wrapped into a block() function, a nested input element loses focus after each typed character.

What version of million are you using?

2.6.4

Are you using an SSR adapter? If so, which one?

None

What package manager are you using?

bun

What operating system are you using?

Mac

What browser are you using?

Brave (Chromium based)

Describe the Bug

Hi! I’ve been stoked to try Million in some of my own projects for a while now (because projects at my work use older React versions and I couldn’t use Million there).

I’ve set up a project with bun, NextJS and Million, everything seemed fine, but now there’s this weird behavior that I can’t figure out how to solve.

//package.json

{
  "name": "mynewproject",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-slot": "^1.0.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.0.0",
    "debounce": "1.2.1",
    "lucide-react": "^0.289.0",
    "million": "2.6.4",
    "next": "13.5.6",
    "react": "^18",
    "react-dom": "^18",
    "tailwind-merge": "^1.14.0",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "typescript": "^5",
    "@types/debounce": "1.2.1",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10",
    "postcss": "^8",
    "tailwindcss": "^3",
    "eslint": "^8",
    "eslint-config-next": "13.5.6"
  }
}

//next.config.mjs

import million from "million/compiler";

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true
}

const millionConfig = {
  auto: true,
}

export default million.next(nextConfig, millionConfig);

//parent component:

"use client"

import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import Image from 'next/image'
import Link from 'next/link'
import React, { useState } from 'react'

import { block } from "million/react";

const Home = () => {
  const [ email, setEmail ] = useState('');
  const [ joined, setJoined ] = useState(false);

  const handleEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.preventDefault();
    setEmail(event.currentTarget.value);
  }

  const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault();
    setJoined(true);
  };

  return (
    <main className="flex flex-col items-center justify-center p-24 w-full h-full py-12 md:py-24 lg:py-32 xl:py-48 bg-transparent absolute z-50">
    
    {!joined ? (<div className="container px-4 md:px-6">
      <div className="grid gap-6 items-center">
        <div className="flex flex-col justify-center space-y-4 text-center">
          <div className="space-y-2">
            <h1 className="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-500">
              Welcome to our app.
            </h1>
            <p className="max-w-[600px] text-zinc-200 md:text-xl dark:text-zinc-100 mx-auto">
              Join us on this journey.
            </p>
          </div>
          <div className="w-full max-w-sm space-y-2 mx-auto">
            <form className="flex space-x-2">
              <Input
                className="max-w-lg flex-1 bg-gray-800 text-white border-gray-900"
                placeholder="Enter your email"
                type="email"
                onChange={(e) => {
                  handleEmail(e);
                  }
                }
                value={email}
              />
              <Button onClick={(e) => handleSubmit(e)}
              className="bg-white text-black" type="submit">
                Join Now
              </Button>
            </form>
            <p className="text-xs text-zinc-200 dark:text-zinc-100">
              Get ready for some fun.
              <Link className="underline underline-offset-2 text-white" href="#">
                Terms & Conditions
              </Link>
            </p>
          </div>
        </div>
      </div>
    </div>) : null}

    {joined ? (<div className="container px-4 md:px-6">
      <div className="grid gap-6 items-center">
        <div className="flex flex-col justify-center space-y-4 text-center">
          <div className="space-y-2">
            <h1 className="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-500">
              Thank you!
            </h1>
            <p className="max-w-[600px] text-zinc-200 md:text-xl dark:text-zinc-100 mx-auto">
              Your email is now on our waitlist!
            </p>
          </div>
        </div>
      </div>
    </div>) : null}

    <div className="absolute bottom-0 left-0 flex h-48 w-full items-center justify-center">
        <a
        className="pointer-events-auto flex place-items-center gap-2 p-8"
        href="https://github.com/davesagraf"
        target="_blank"
        rel="noopener noreferrer"
      >
        {' '}
        <Image
          src="/github-mark.svg"
          alt="github"
          className="dark:invert"
          width={50}
          height={12}
          priority
        />
      </a>
    </div>

    <Image
    className="pointer-events-none"
      src="/sundust.svg"
      alt="sundust"
      quality={100}
      fill
      sizes="100vw"
      style={{
        objectFit: 'cover',
        opacity: 0.6
      }}
    />
  </main>
  )
}

const HomeBlock = block(Home);

const HomePage = () => {

  return (
    <HomeBlock />
  );
};

export default HomePage;

//child component:

import * as React from "react"

import { cn } from "@/lib/utils"

// import { block } from "million/react";

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

//million-ignore
const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Input.displayName = "Input"

// const InputBlock = block(Input);

export { Input }

I’ve already tried different MillionJS compiler options, both CommonJS & ES module imports and exports, tried separating React useState hooks in the parent component to another function and passing them as props, tried parent component with block() function and child component without, and vice versa.

I also tried adding debugger & comparing all steps with block() function and without, but DevTools don’t show a difference.

I read all the MillionJS docs, followed rules of blocks and compiler settings, read about usage of UI libraries vs DOM elements, followed this post https://dev.to/tobysolutions/how-to-use-millionjs-in-a-next-app-1eim , read MillionJS docs here on github and all the previous issues, but I can’t find a solution to this problem.

I know that this component doesn’t need Million much and I could continue building my project with bun & NextJS for a long time and it would be fine.

However, my initial goal was to integrate Million not only when everything is lagging, but as early as possible (I started this project yesterday), so that from early on I can see all the benefits of using it and how it’s affecting components I’m building, so that I could make my components better for both React and Million from the start.

Hope this issue is not too big.

Thanks.

What’s the expected result?

Regular HTML input element behavior, as in any standard React component with input element.

Link to Minimal Reproducible Example

https://app.replay.io/recording/my-app--92a45a2e-bfba-4079-b65d-beb7261cd01e

Participation

  • I am willing to submit a pull request for this issue.

About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Comments: 27 (12 by maintainers)

Most upvoted comments

Hmm, thanks @vimode, taking a look now. Thank you.

Hmm, @davesagraf, @wmitsuda; thanks for this! Let me have a review of what the issue is.

I had a similar issue, the only way I could fix it was by moving the form to a separate component and make it an uncontrolled component.

Hmm, can I see a demo of this?

Sure thing.

Here is the quick demo of the bug with controlled component, this code is very similar to the expense tracker demo from the sink. Except, that the input element expense is now controlled by the parent component. Note: Only the first input expense has been changed to demo the bug. The amount and radio buttons are still uncontrolled. So the first input will lose focus on each keystroke.

The updated code has the addition of newExpense state, the handleOnChange function and their usage as props in InputForm https://gist.github.com/vimode/151c957e3abe1edd38f27c688e5400d1

The expense tracker at https://sink.million.dev does not have this issue as the form is uncontrolled element, managed by the form input itself. https://github.com/aidenybai/million/blob/main/packages/kitchen-sink/src/examples/expense-tracker.tsx

I had a similar issue, the only way I could fix it was by moving the form to a separate component and make it an uncontrolled component.

I am trying to use this in production but need help with the same input focus issue when we search. Are there any updates to this issue? Or any workarounds? If not, then I should undo the implementation.

You could temporarily million ignore that search component wrapper to prevent that from happening for now, 3.0 has a lot of these fixes in store already.

Any updates @oliverloops?

https://million.dev/docs/automatic (go to the “Ignoring Components” section.

My company is also facing this issue in automatic and manual mode when wrapping components containing an input in a block. For many of them, it does not make sense for us to have uncontrolled components; I believe this is a legitimate issue that is not solved by the uncontrolled workaround. Happy to sponsor or put a bounty on this issue, more than happy to look at solving it as well–any direction is useful as we’re familiar with using Million but not the Million codebase.

Edit: I see #833 solves this.

cc @aidenybai

Thank you very much! We will look into it and give you feedback. Thank you very much.

My company is also facing this issue in automatic and manual mode when wrapping components containing an input in a block. For many of them, it does not make sense for us to have uncontrolled components; I believe this is a legitimate issue that is not solved by the uncontrolled workaround. Happy to sponsor or put a bounty on this issue, more than happy to look at solving it as well–any direction is useful as we’re familiar with using Million but not the Million codebase.

Edit: I see #833 solves this.

cc @aidenybai

Can we turn off automatic PR closing here, so that bot doesn’t close it until the issue is actually solved?

Sure thing! I’ll try to check it out.

plz, don’t close it!

I’ll have some similar issue before. let me help you on this if I can help @tobySolutions

Awesome! Go ahead please Oliver

I’ll have some similar issue before. let me help you on this if I can help @tobySolutions