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.
{
"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"
}
}
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)
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.
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, thehandleOnChange
function and their usage as props inInputForm
https://gist.github.com/vimode/151c957e3abe1edd38f27c688e5400d1The 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.
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.
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
Sure thing! I’ll try to check it out.
plz, don’t close it!
Awesome! Go ahead please Oliver
I’ll have some similar issue before. let me help you on this if I can help @tobySolutions