aidenybai / million

Optimize React performance and make your React 70% faster in minutes, not months.
https://million.dev
MIT License
16.37k stars 577 forks source link

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

Closed davesagraf closed 8 months ago

davesagraf commented 1 year ago

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

wmitsuda commented 1 year ago

I’ve just tried millionjs with automatic mode and I’m seeing the same behavior.

tobySolutions commented 1 year ago

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

oliverloops commented 1 year ago

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

tobySolutions commented 1 year ago

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

Awesome! Go ahead please Oliver

davesagraf commented 1 year ago

I’ve just tried millionjs with automatic mode and I’m seeing the same behavior.

I don't know if it has anything to do with project being run on bun in particular, but in my case this behavior has been the same with any combination of true/false set for reactStrictMode in nextConfig and auto in millionConfig here:

import million from "million/compiler";

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

const millionConfig = {
  auto: true,
}

export default million.next(nextConfig, millionConfig); 
vimode commented 1 year ago

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.

tobySolutions commented 1 year ago

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?

tobySolutions commented 1 year ago

Any updates @oliverloops?

vimode commented 1 year ago

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

tobySolutions commented 1 year ago

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

github-actions[bot] commented 11 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs within the next 7 days.

wmitsuda commented 11 months ago

plz, don't close it!

github-actions[bot] commented 11 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs within the next 7 days.

davesagraf commented 11 months ago

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

tobySolutions commented 11 months ago

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.

0x15f commented 10 months ago

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

tobySolutions commented 10 months ago

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.

github-actions[bot] commented 10 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs within the next 7 days.

Moe03 commented 10 months ago

Same issue happening for me on iOS it loses focus on every letter types and weird flashes on PC

selvamk-js commented 10 months ago

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.

tobySolutions commented 10 months ago

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.

github-actions[bot] commented 9 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs within the next 7 days.

davesagraf commented 9 months ago

This issue should not be closed yet.

github-actions[bot] commented 8 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs within the next 7 days.

davesagraf commented 7 months ago

hey everyone, has this issue been solved somehow for anyone?

0x15f commented 7 months ago

@davesagraf I will try later today, I did not explicitly try this example but a similar example that existed before was still reproducible a few days ago with a text input.

tobySolutions commented 7 months ago

@davesagraf I will try later today, I did not explicitly try this example but a similar example that existed before was still reproducible a few days ago with a text input.

Hey there @0x15f can you confirm that this is still the case. Please help provide a reproduction so I can inform the team of this. @davesagraf do you experience the same issue as well?