-
Notifications
You must be signed in to change notification settings - Fork 27.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NEXT-1198] Next.js 13.0.1+ breaks radio buttons in both app directory and pages #49499
Comments
In this video you can see how it flickers on page reload. Note this is the version using pages/ with appDir enabled in Next 13.0.1. next-radio-bug.mov |
Can confirm this both happend on next@canary with its default react.js release and with react@next + react-dom@next From what I can see happening in the dom it kind of seems like it looses the checked state when the next-route-announcer gets placed in the dom and the title gets set. (not sure if this is related tho) The value in the state doesn't get changed so logic speaking the radio button should be checked according to the available data in the component. |
Setting manually the e.g: 'use client'
import { useState, useId, useEffect } from 'react';
function Test() {
const toBeSelectedId = useId();
const [selectedTopping, setSelectedTopping] = useState('Medium');
useEffect(() => {
document.querySelector(`[data-id="${toBeSelectedId}"]`).checked = true;
// or even with
document.querySelector(`[type=radio][name=topping][value=Medium]`).checked = true;
}, [toBeSelectedId]);
return (
<div className="flex flex-col items-center">
<input
type="radio"
name="topping"
value="Regular"
id="regular"
checked={selectedTopping === 'Regular'}
onChange={e => setSelectedTopping(e.target.value)}
/>
<label htmlFor="regular">Regular</label>
<input
type="radio"
name="topping"
value="Medium"
id="medium"
checked={selectedTopping === 'Medium'}
onChange={e => setSelectedTopping(e.target.value)}
// --- HERE
data-id={toBeSelectedId}
/>
<label htmlFor="medium">Medium</label>
<input
type="radio"
name="topping"
value="Large"
id="large"
checked={selectedTopping === 'Large'}
onChange={e => setSelectedTopping(e.target.value)}
/>
<label htmlFor="large">Large</label>
</div>
)
}
export default function Page() {
return <Test />;
} |
@lsagetlethias thank you your fix worked for me. I was tearing my hair out last night trying to understand if it was me doing something wrong |
YW. I suppose it can also be done with Still an ugly fix tho'. |
Same problem here with Instead I'm facing another (more cryptic) bug.
To replicate I just changed few line of code in "use client";
import { useState } from "react";
function Test() {
const [selectedTopping, setSelectedTopping] = useState("Medium");
const onChange = (e) => {
console.log("onChange");
setTimeout(() => {
console.log("onChange setTimeout");
setSelectedTopping(e.target.value);
}, 2000);
};
return (
<div className="flex flex-col items-center">
<input
type="radio"
name="topping"
value="Regular"
id="regular"
checked={selectedTopping === "Regular"}
onChange={onChange}
/>
<label htmlFor="regular">Regular</label>
<input
type="radio"
name="topping"
value="Medium"
id="medium"
checked={selectedTopping === "Medium"}
onChange={onChange}
/>
<label htmlFor="medium">Medium</label>
<input
type="radio"
name="topping"
value="Large"
id="large"
checked={selectedTopping === "Large"}
onChange={onChange}
/>
<label htmlFor="large">Large</label>
</div>
);
}
export default function Page() {
return <Test />;
} Registrazione.schermo.2023-06-13.alle.16.03.35.mov |
@christianjuth I'm facing the problem starting from Found that with |
There seems to be a related issue in react |
Another hack, remove the "name" attribute, then it will work (the initial value will be set). Since the example is a controlled input, it will work like the regular radio button (only 1 selected). It's annoying, you can't use formData to collect all form elements value. |
@sophiebits @timneutkens can you pls downgrade react-builtin |
@Luk-z can you check if this has been fixed in the latest next version? I tried updating my original repo that reproduced the issue, and I think it might be working now. But I wanna make sure I'm not overlooking something. |
@christianjuth tried with |
|
I'm not really sure how to move forward. I've already adopted the app directory in a large project, which despite being "stable" still feels beta imo. I would love to get app directory bug fixes in next If I upgrade I need to worry not only about every radio button in my project, but also any library I'm using that might be using radio buttons. Updating feels like it will introduce bugs, but I also don't want to stick with For example, I've noticing some weird routing errors where navigating back and forth between two routes get's stuck and refuses to transition to the other route without a manual full page reload. I know the links aren't broken because the transition works the first few times then breaks. |
Hi @christianjuth I experienced the same issue about 2 weeks ago, the bug is confirmed. Hopefully you have a radio button component that you're using in multiple places, because that will make my proposition easier: Use radix-ui's radio button primitives. The primitives have a parent To handle state changes, pass your onChange handler to onValueChange, which returns a string representation of the value of the currently selected radio input. If you're using something like React-hook-form, you can build a basic handler using the setValue function (takes name of key to update, new value, and some optional arguments). One other thing worth mentioning is that the bug you're experiencing only applies in Dev mode. The bug doesn't apply to the built application, even if you stick to vanilla radio inputs. I prefer radix though cuz then I know immediately how the build inputs will behave. Best wishes, and let me know if you need further direction. Hopefully this gets you moving again. |
@ozzyfromspace appreciate the help, but like the other hacks in this thread this doesn't solve the underlying issue. I know there are multiple ways I can hack together a workaround, but that still doesn't solve the underlying problem. I've also found when managing large scale projects it really easy to introduce lots of npm packages but difficult to remove them later when the project becomes bloated. I'm a fan of Radix, but I cant afford to introduce another UI library just to fix one bug. To be honest, this isn't a huge issue for me at the moment. I haven't noticed it since opening up this thread. My main problem is I've come to trust the React team when they say something is stable. Maybe I've just gotten lucky, but I've never seen a bug like this in a production React build. I'm realizing the Next.js team moves faster then the React time, and I think I'm going to need to be more careful when upgrading Next versions in the future. I don't want to sound ungrateful, because it's amazing what Next team has accomplished in a short time. I just can't afford to have projects breaking like this when upgrading Next versions. I'm sorry if I'm blowing the severity of this issue out of proportion. I just never though I'd have to worry about html primitives themselves breaking in a Next.js update. |
@christianjuth that's fair, I completely see where you're coming from. Best wishes with your project, and feel free to tag me if you have any bugs in general. Sometimes it's not a bug per-se, but a non-intuitive way that the new App router handles something or other. Cheers. |
It does work with useRef. Indeed a weird bug/solution. const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const input = inputRef.current;
if (input && checked) {
input.checked = true;
}
}, []);
return <input ref={inputRef}... |
For Info, the bug not present in prod mode, only in dev mode... possible refresh causes dev stat |
so then it could be fixed smth like this: // NOTE: https://github.com/vercel/next.js/issues/49499
if (process.env.NODE_ENV === 'development') {
useEffect(() => {
for (const el of document.getElementsByName('my-radio')) {
if (el.hasAttribute('checked')) {
(el as HTMLInputElement).checked = true
}
}
}, []);
} |
Tested and working: // NOTE: https://github.com/vercel/next.js/issues/49499
if (process.env.NODE_ENV === 'development') {
useEffect(() => {
document.querySelectorAll('input[type=radio]').forEach((elem) => {
if (elem.hasAttribute('checked')) {
(elem as HTMLInputElement).checked = true
}
});
}, []);
} This line required configuration so I went with the more standard syntax above. for (const el of document.getElementsByName('my-radio')) { FWIW, I am only seeing this bug when I am running a proxy server to get https working locally. I am using mkcert for the local cert and local-ssl-proxy to run the proxy on localhost:3001, while the normal server is running on localhost:3000. So https://localhost:3001 has the bug but http://localhost:3000 does not. Pretty weird, but that's what I'm seeing. |
Fixes whatever part of #26876 and vercel/next.js#49499 that #27394 didn't fix, probably. From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs. Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again.
Fixes whatever part of #26876 and vercel/next.js#49499 that #27394 didn't fix, probably. From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs. Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again.
Fixes whatever part of #26876 and vercel/next.js#49499 that #27394 didn't fix, probably. From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs. Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again.
Fixes whatever part of #26876 and vercel/next.js#49499 that #27394 didn't fix, probably. From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs, so the DOM is out of sync with React state. Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again.
Fixes whatever part of #26876 and vercel/next.js#49499 that #27394 didn't fix, probably. From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs, so the DOM is out of sync with React state. Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again. DiffTrain build for [db69f95](db69f95)
Fixes whatever part of facebook#26876 and vercel/next.js#49499 that facebook#27394 didn't fix, probably. From manual tests I believe this behavior brings us back to parity with latest stable release (18.2.0). It's awkward that we keep the user's state even for controlled inputs, so the DOM is out of sync with React state. Previously the .defaultChecked assignment done in updateInput() was changing the actual checkedness because the dirty flag wasn't getting set, meaning that hydrating could change which radio button is checked, even in the absence of user interaction! Now we go back to always detaching again.
Fixed in v13.5.6-canary.1 via 0a80017 |
@apostolos is correct, after I checked the reproduction with that version, the bug is gone. Please upgrade! |
This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you. |
Verify canary release
Provide environment information
Operating System: Platform: linux Arch: arm64 Version: #78-Ubuntu SMP Tue Apr 18 09:00:08 UTC 2023 Binaries: Node: 19.8.1 npm: 9.5.1 Yarn: 1.22.19 pnpm: N/A Relevant packages: next: 13.3.1 eslint-config-next: N/A react: 18.2.0 react-dom: 18.2.0
Which area(s) of Next.js are affected? (leave empty if unsure)
App directory (appDir: true)
Link to the code that reproduces this issue
https://github.com/christianjuth/next-pages-radio-bug
To Reproduce
Note: this bug appears in 13.0.1+ including 13.4.2-canary.3
This bug appears in both a pages/ and app/. It only appears in the pages/ route when app dir is enabled. In other words, enabling app dir seems to have an effect on the pages dir.
Recreate the bug with pages/
npx create-next-app@latest
(opt out of TS, eslint, and Tailwind for simplicity)yarn dev
. If you visit http://localhost:3000/pages you will notice the controlled radio button is initially selected but then flickers uncollectedRecreate the bug with app/
npx create-next-app@latest
(opt out of TS, eslint, and Tailwind for simplicity)yarn dev
. If you visit http://localhost:3000 you will notice the controlled radio button is initially selected but then flickers uncollectedDebugging further
Inspecting the dom shows the following
However, the input marked
checked=""
is not checked and if we query the dom we can seedocument.getElementById("medium").checked = false
Describe the Bug
There seems to be an issue where React is initially rendering the radio input as selected and then it almost instantly looses it's selected state. My guess is this has something to do with React's strict mode. However, this issue doesn't arise until I enable the appDir.
Expected Behavior
I would expect the controlled radio input selection to visually match the state in React.
Which browser are you using? (if relevant)
1.51.110 Chromium: 113.0.5672.77 (Official Build) (64-bit)
How are you deploying your application? (if relevant)
No response
NEXT-1198
The text was updated successfully, but these errors were encountered: