Skip to content

Conversation

Janpot
Copy link
Member

@Janpot Janpot commented Feb 18, 2025

Port core error minification infrastructure:

Copy link

netlify bot commented Feb 19, 2025

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit eca3e92
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/68baff427c36cc00083e5792
😎 Deploy Preview https://deploy-preview-1463--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@Janpot Janpot changed the title [code-infra] Initial setup for minified error messages (WIP) [code-infra] Setup error message minification Feb 20, 2025
@Janpot Janpot marked this pull request as ready for review February 20, 2025 17:43
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label May 29, 2025
Copy link

pkg-pr-new bot commented May 29, 2025

Open in StackBlitz

pnpm add https://pkg.pr.new/mui/base-ui/@base-ui-components/react@1463
pnpm add https://pkg.pr.new/mui/base-ui/@base-ui-components/utils@1463

commit: eca3e92

@mui-bot
Copy link

mui-bot commented May 29, 2025

Bundle size report

Total Size Change:${\tiny{\color{green}▼}}$-9.42KB(-0.65%) - Total Gzip Change:${\tiny{\color{green}▼}}$-778B(-0.16%)
Files: 41 total (0 added, 0 removed, 36 changed)

@base-ui-components/react/separatorparsed:${\tiny{\color{red}▲}}$+150B(+3.50%) gzip:${\tiny{\color{red}▲}}$+101B(+5.50%)
@base-ui-components/reactparsed:${\tiny{\color{green}▼}}$-4.71KB(-1.54%) gzip:${\tiny{\color{green}▼}}$-741B(-0.80%)
@base-ui-components/react/menuparsed:${\tiny{\color{green}▼}}$-800B(-0.70%) gzip:${\tiny{\color{green}▼}}$-145B(-0.38%)
@base-ui-components/react/context-menuparsed:${\tiny{\color{green}▼}}$-753B(-0.67%) gzip:${\tiny{\color{green}▼}}$-147B(-0.40%)
@base-ui-components/react/selectparsed:${\tiny{\color{green}▼}}$-513B(-0.46%) gzip:${\tiny{\color{green}▼}}$-67B(-0.18%)
@base-ui-components/react/navigation-menuparsed:${\tiny{\color{green}▼}}$-366B(-0.43%) gzip:${\tiny{\color{green}▼}}$-35B(-0.12%)
@base-ui-components/react/accordionparsed:${\tiny{\color{green}▼}}$-307B(-1.33%) gzip:${\tiny{\color{green}▼}}$-27B(-0.34%)

Show 29 more bundle changes

@base-ui-components/react/fieldsetparsed:${\tiny{\color{red}▲}}$+150B(+2.53%) gzip:${\tiny{\color{red}▲}}$+89B(+3.60%)
@base-ui-components/react/use-renderparsed:${\tiny{\color{red}▲}}$+150B(+3.91%) gzip:${\tiny{\color{red}▲}}$+99B(+5.94%)
@base-ui-components/react/formparsed:${\tiny{\color{red}▲}}$+111B(+1.93%) gzip:${\tiny{\color{red}▲}}$+77B(+3.13%)
@base-ui-components/react/meterparsed:${\tiny{\color{red}▲}}$+71B(+0.96%) gzip:${\tiny{\color{red}▲}}$+46B(+1.52%)
@base-ui-components/react/progressparsed:${\tiny{\color{red}▲}}$+62B(+0.83%) gzip:${\tiny{\color{red}▲}}$+42B(+1.38%)
@base-ui-components/react/menubarparsed:${\tiny{\color{red}▲}}$+35B(+0.18%) gzip:${\tiny{\color{red}▲}}$+28B(+0.38%)
@base-ui-components/react/inputparsed:${\tiny{\color{red}▲}}$+34B(+0.31%) gzip:${\tiny{\color{red}▲}}$+19B(+0.43%)
@base-ui-components/react/checkbox-groupparsed:${\tiny{\color{red}▲}}$+31B(+0.26%) gzip:${\tiny{\color{red}▲}}$+15B(+0.31%)
@base-ui-components/react/fieldparsed:${\tiny{\color{red}▲}}$+31B(+0.23%) gzip:${\tiny{\color{red}▲}}$+17B(+0.32%)
@base-ui-components/react/avatarparsed:${\tiny{\color{red}▲}}$+25B(+0.35%) gzip:${\tiny{\color{red}▲}}$+24B(+0.83%)
@base-ui-components/react/number-fieldparsed:${\tiny{\color{green}▼}}$-284B(-0.98%) gzip:${\tiny{\color{green}▼}}$-21B(-0.20%)
@base-ui-components/react/checkboxparsed:${\tiny{\color{green}▼}}$-254B(-1.36%) gzip:${\tiny{\color{green}▼}}$-22B(-0.31%)
Base UI checkboxparsed:${\tiny{\color{green}▼}}$-254B(-1.36%) gzip:${\tiny{\color{green}▼}}$-21B(-0.30%)
@base-ui-components/react/popoverparsed:${\tiny{\color{green}▼}}$-242B(-0.28%) gzip:${\tiny{\color{green}▼}}$-8B(-0.03%)
@base-ui-components/react/scroll-areaparsed:${\tiny{\color{green}▼}}$-241B(-1.53%) gzip:${\tiny{\color{green}▼}}$-17B(-0.30%)
@base-ui-components/react/tabsparsed:${\tiny{\color{green}▼}}$-177B(-0.74%) gzip:${\tiny{\color{green}▼}}$-1B(-0.01%)
@base-ui-components/react/preview-cardparsed:${\tiny{\color{green}▼}}$-162B(-0.27%) gzip:${\tiny{\color{green}▼}}$-49B(-0.24%)
@base-ui-components/react/toolbarparsed:${\tiny{\color{green}▼}}$-161B(-0.82%) gzip:${\tiny{\color{red}▲}}$+8B(+0.12%)
@base-ui-components/react/switchparsed:${\tiny{\color{green}▼}}$-149B(-1.02%) gzip:${\tiny{\color{green}▼}}$-15B(-0.27%)
@base-ui-components/react/tooltipparsed:${\tiny{\color{green}▼}}$-147B(-0.23%) gzip:${\tiny{\color{green}▼}}$-40B(-0.18%)
@base-ui-components/react/radioparsed:${\tiny{\color{green}▼}}$-145B(-0.97%) gzip: 0B(0.00%)
@base-ui-components/react/toastparsed:${\tiny{\color{green}▼}}$-126B(-0.48%) gzip:${\tiny{\color{green}▼}}$-13B(-0.14%)
@base-ui-components/react/alert-dialogparsed:${\tiny{\color{green}▼}}$-121B(-0.23%) gzip:${\tiny{\color{green}▼}}$-12B(-0.07%)
@base-ui-components/react/sliderparsed:${\tiny{\color{green}▼}}$-96B(-0.39%) gzip:${\tiny{\color{green}▼}}$-8B(-0.09%)
@base-ui-components/react/dialogparsed:${\tiny{\color{green}▼}}$-93B(-0.18%) gzip:${\tiny{\color{red}▲}}$+3B(+0.02%)
@base-ui-components/react/collapsibleparsed:${\tiny{\color{green}▼}}$-83B(-0.46%) gzip:${\tiny{\color{red}▲}}$+9B(+0.14%)
@base-ui-components/react/toggleparsed:${\tiny{\color{green}▼}}$-75B(-0.83%) gzip:${\tiny{\color{red}▲}}$+4B(+0.11%)
@base-ui-components/react/toggle-groupparsed:${\tiny{\color{green}▼}}$-5B(-0.04%) gzip:${\tiny{\color{red}▲}}$+16B(+0.30%)
@base-ui-components/react/radio-groupparsed:${\tiny{\color{green}▼}}$-2B(-0.01%) gzip:${\tiny{\color{red}▲}}$+14B(+0.18%)

Details of bundle changes

Generated by 🚫 dangerJS against 8e6427d

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 2, 2025
@michaldudak
Copy link
Member

Since the bundle size checker is running, we can continue with this. The size saving aren't enormous (-0.80% gzipped for the whole library), but they might get slightly more significant as we create more components that throw more errors. I tested an alternative approach, but it's much worse: #2050

One additional thing we need here is for the build to fail if error messages haven't been extracted.

@Janpot
Copy link
Member Author

Janpot commented Jun 3, 2025

but they might get slightly more significant as we create more components that throw more errors.

It would also enable more verbose error messages for a better DX. in case you were holding back atm 🙂. We can keep this on hold for a bit longer as well as far as I'm concerned to let the lib get a little more stable.

One additional thing we need here is for the build to fail if error messages haven't been extracted.

Currently CI should fail by checking changes, do you want to build this into the build step directly?

@michaldudak
Copy link
Member

in case you were holding back atm 🙂

No, that's fine :) There are size savings already, so we can move forward with this. I'd like to merge #2049 first, though, so the messages are consistent.

Currently CI should fail by checking changes

OK, that's enough.

babel.config.js Outdated
},
},
],
'babel-plugin-optimize-clsx',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed now.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Sep 5, 2025
@mui-bot
Copy link

mui-bot commented Sep 5, 2025

Bundle size report

Bundle Parsed size Gzip size
@base-ui-components/react ▼-5.38KB(-1.55%) ▼-945B(-0.86%)

Details of bundle changes

* ...
* @param {number} code
*/
export default function formatErrorMessage(code: number, ...args: string[]): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be in @base-ui-components/utils and then imported and used in react package.

1. There hasn't been an update to the semantic meaning of the error message. Error codes need to outlive Base UI versions, so the same code must mean the same thing across versions.
2. There hasn't been a change in parameters, no added and no removed.

In both of those cases, always create a new error code lline in `./src/error-codes.json`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In both of those cases, always create a new error code lline in `./src/error-codes.json`.
In both of those cases, always create a new error code line in `./src/error-codes.json`.

const errorCodesPath = new URL(import.meta.resolve('./docs/public/static/error-codes.json'))
.pathname;
const missingError = process.env.MUI_EXTRACT_ERROR_CODES === 'true' ? 'write' : 'annotate';
const errorCodesPath = new URL(import.meta.resolve('docs/src/error-codes.json')).pathname;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more rebase/merge with my branch should fix the remaining CI issues.

"19": "Base UI: ComboboxGroupContext is missing. ComboboxGroup parts must be placed within <Combobox.Group>.",
"20": "Base UI: ComboboxItemContext is missing. ComboboxItem parts must be placed within <Combobox.Item>.",
"21": "Base UI: <Combobox.Portal> is missing.",
"22": "Base UI: <Combobox.Popup> and <Combobox.Arrow> must be used within the <Combobox.Positioner> component",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is missing a closing dot

Suggested change
"22": "Base UI: <Combobox.Popup> and <Combobox.Arrow> must be used within the <Combobox.Positioner> component",
"22": "Base UI: <Combobox.Popup> and <Combobox.Arrow> must be used within the <Combobox.Positioner> component.",

I would go as far as throwing when an error minified doesn't have a closing dot 😄

@@ -0,0 +1,18 @@
# Production error
Copy link
Member

@oliviertassinari oliviertassinari Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we should change this to improve the DX:

  • People arriving here would see stuff like this in their console:
SCR-20250906-mzzx
  • If we create 73 pages with the same H1 and title, that looks like duplication of content; it feels like it defeats the point of slowing down the build time.

So I would expect to see the error code somewhere, like it does in https://react.dev/errors/130.

SCR-20250906-ngpr

It's in the h1, it doesn't have to be there, but one option.

Copy link
Member

@oliviertassinari oliviertassinari Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-topic. Speaking of the error DX, I wonder about this pattern: having error messages + links to https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout.
But it feels a lot like duplication of content. Meaning this should be in the docs. For example missingSuspenseWithCSRBailout is only documented in their error page 🙈.

So it's probably a no-no, the links in the errors should push back to the docs, that link should be a /r/ link to never get outdated, the docs needs to be clear, and the error message should follow https://www.notion.so/mui-org/Technical-writing-style-guide-7e55b517ac2e489a9ddb6d0f6dd765de#85219822c3194b6f8a49ee08ea82b90a.

Edit: umbrella issue mui/mui-public#222 updated.

@@ -0,0 +1,36 @@
'use client';
import * as React from 'react';
import { useSearchParams } from 'next/navigation';
Copy link
Member

@oliviertassinari oliviertassinari Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to use the Next.js custom API? What if we were using the window API directly? To what I understand, we will never have a server running to render those pages, so it feels like we are using heavy abstractions (suspense, Next.js router) for no benefits.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we were using the window API directly?

You can't build this hook with current stable browser APIs as you can't listen for history change events. Only way to do it is to monkey patch the history methods, or use the unstable navigation API.

we will never have a server running to render those pages, so it feels like we are using heavy abstractions (suspense, Next.js router) for no benefits.

I don't see what a server has to do with it, it's a next.js page, it will create a router object, whether you use it or not. It's also not a heavy abstraction, what makes you assume that?

Copy link
Member

@oliviertassinari oliviertassinari Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The high-level thoughts (behind my first comment), it feels like we are better off:

  • Not using Next.js's specific API when we can use simpler web API for the same outcome. The fewer indirections, the better.
    Also, I think we can have general concerns with Next.js' current product management. It feels that they get distracted in rabbit holes where they are not great at, e.g. turbopack: https://youtu.be/w61mLV5nZK0?si=ZksBJLRnUhAVzkYi&t=2283 rather than using RSpack or Rolldown and focus on fixing bugs, stability, and performance issues. And it feels like the more they build deeply integrated Next.js features into Vercel without also owning the portability, the more the framework loses its edge of being a world-class way to deploy a React app (marketing people's perception of it, like how would we feel if AWS were leading PostgreSQL and perceived as building AWS-specific features in PostgreSQL).
  • Not using Suspense when we can avoid it. It has patterns that need to be learned, and that feel not easy to figure out, e.g., [React 19] Suspense throttling behavior (FALLBACK_THROTTLE_MS) kicks in too often facebook/react#31819. It's about splitting the rendering in chunks, so it feels mostly about supporting: not declaring data dependency in one place (but lazily, e.g., GraphQL), but do we want this by default? It feels like no. Starting with an explicit declaration of data dependencies for the whole page, rendering everything at once, and only breaking down once it hits limits is better.

I don't see what a server has to do with it

Those are error pages; they do not seem to need SEO, so I assumed that we can either SSR them or not.
Now, reading window.location wouldn't work if the page needs to be rendered on the server, but we can also have hybrid handling, core content is SSR, query are client-side rendered.

you can't listen for history change events.

I assumed that history can't trigger (there are no links), that this could be enough:

  React.useEffect(() => {
    const searchParams = new URLSearchParams(window.location.search);
  }, []);

It's also not a heavy abstraction, what makes you assume that?

It's was more about the top ⬆️, me trying to see if we can operate with simpler primitives. In this specific case, yeah, it seems there isn't much we can improve.

Copy link
Member

@oliviertassinari oliviertassinari Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, they have a lot of error codes to render, I imagine we will scale to someone similar:

If the current approach is slow to generate or to deploy for the CDN, I think going with one SSR page for the errors, and each code as a client side rendering, can be a good solution. If it's fast, SSR will be a better UX (I couldn't see a difference right now with 70 pages, maybe Next.js is smart it builds the page once, render each .html concurrenly, and Netlify handles hundreds of thousands of .html without overhead).

Copy link
Contributor

@brijeshb42 brijeshb42 Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could avoid generating potentially more than 70 pages by getting the code as well from query params since at the end, there is going to be a client component. We can just pass the errors json as a prop to the client component and generate the error there on client. We'll only generate one page for all the errors this way.

One other thing. On mui.com, we pass the error string through markdown to html conversion as well for rendering links in errors as <a> tags. We should do something similar here as well, albeit on the server side and pass the transformed errors to the client.

As for avoiding Next.js APIs, I don't see how/why we could/should do that. Our whole docs infra for all our repos is based on Next.js. It anyway won't be easy to switch if/when we decide to. We anyways use them on the actual page component that corresponds to the route, not the imported components. Rest of the components are pretty agnostic of the framework, maybe they use the Link component, but that should be it.

Copy link
Member Author

@Janpot Janpot Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed that history can't trigger (there are no links), that this could be enough:

React.useEffect(() => {
  const searchParams = new URLSearchParams(window.location.search);
}, []);

Probably want to do

const subscribe = () => () => {};
const getSearchParams = memoize((search) => new URLSearchParams(search))
const getSnapshot = () => getSearchParams(window.location.search)
const getServerSnapshot = () => getSearchParams('');
const useSearchParams = () => React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);

Just to get that client-side mount working correctly. Avoiding unnecessary renders caused by the effect. But really, from a maintainability point of view, I think we should avoid building components that only work if they're used on a page that has zero incoming links. That's just a completely undocumented constraint and there is nothing in place to prevent someone from using this component on another page 6 months from now. The Next.js abstraction is not that bad, it's not slow and replacing it with another router would be trivial should the day ever come, every router implementation has this hook in one form or another.

Now if only we could use that sweet sweet navigation API, then a few well placed event handlers can transform this in a fully working hook:

const subscribe = (cb) => {
  window.navigation.addEventListener('navigation', cb);
  return () => {
    window.navigation.removeEventListener('navigation', cb);
  };
};
const getSearchParams = memoize((search) => new URLSearchParams(search))
const getSnapshot = () => getSearchParams(window.location.search)
const getServerSnapshot = () => getSearchParams('');
const useSearchParams = () => React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);

a man can dream...


The full error message:

<ErrorDisplay msg={props.msg} />
Copy link
Member

@oliviertassinari oliviertassinari Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current https://deploy-preview-1463--base-ui.netlify.app/production-error/1

SCR-20250906-ndbn

I doubt this is how this should look. It feels like we should render the error red #ff0000, like people will see the error in their console first. This feels better: https://react.dev/errors/130.

SCR-20250906-ngyo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design can be iterated on by us as well as the Base UI team. The PR is more about setting up the infrastructure for error codes.

pnpm extract-error-codes
```

This will update the `./src/error-codes.json` file with the newly extracted errors.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This will update the `./src/error-codes.json` file with the newly extracted errors.
This updates the `./src/error-codes.json` file with the newly extracted errors.


Important: If you just altered the text of an error, you are allowed to update the existing error code with the new text in `./src/error-codes.json`, but only under the following conditions:

1. There hasn't been an update to the semantic meaning of the error message. Error codes need to outlive Base UI versions, so the same code must mean the same thing across versions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. There hasn't been an update to the semantic meaning of the error message. Error codes need to outlive Base UI versions, so the same code must mean the same thing across versions.
1. There hasn't been an update to the semantic meaning of the error message. Error codes need to outlive Base UI versions, so the same code must mean the same thing across versions.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Sep 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
on hold There is a blocker, we need to wait. performance PR: out-of-date The pull request has merge conflicts and can't be merged. scope: code-infra Involves the code-infra product (https://www.notion.so/mui-org/5562c14178aa42af97bc1fa5114000cd).
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants