Especially for business oriented apps, most of the models only need a list view with a table, and a detail view displaying some properties and related lists of models.
It works wonderfully.
Agree completely that is the only sane goal - just wondering how you got there?
Then it'd just apply that recursively. For a simple model there'd only be two levels: The view, containing a bunch of columns, and the columns themselves, and the view type knew to iterate over the columns in the view and look up their type and tell them to render themselves. For more complex views frontend devs might e.g. provide a custom view type that'd group columns based on certain attributes, but still delegate to the generic mechanism to render those columns.
So replacing a part just meant writing a component that knew how to render itself from its props, just like any other component. Just that some of those props would be coming from the backend.
We'd generally try to avoid replacing components if the only reason for a component was to override how something was rendered, and to focus on why it should be rendered differently and consider if we could reflect that 'why' in the data, or other ways.
E.g. were certain fields grouped together because they were semantically related? Then reflect that with an attribute in the "meta schema" for those columns, and make the frontend component render related groups according to the type of that group (let's say a bunch of fields reflects a user address). Incidentally this tended to be very simple, as with a relational model when there were groups like that the right thing was often for them to be a separate table/model anyway, and we 'just' needed to distinguish between type handlers for rendering an object 'as the page' vs. 'as a reference' vs. 'as a group within the page', and that was easy enough to distinguish "well enough".
At a top level, we'd look for a registered view-level component (or fall back on the default), within a view if the backend returned the model instance "in-line" we rendered it as a group, and if it returned an id, we'd render it as a reference.
So a "User" object for example would render as a profile view if it was not your user and you went to a page where the associated API call returned the User as the top level model, you personalised profile page w/optional edit view if it was your user, or a "pill" linking to the user profile if it was just a reference.
The idea was that frontend components should focus on how to render specific types of data in a specific context, rather than specifying how specifically to render the page, because it'd make no sense to have the frontend e.g. try to render a field that no longer exists, or try to render a field as a different type it is when the meta data about which fields were available and what type they are was already available.
Then we built a set of generic frontend components for that in React once.
And we then had an instant UI for 50+ models, but the frontend devs could trivially selectively override both the whole page for models where the default was too simplistic, or how to render specific types of fields or for specific fields of specific models.
The "instant UI" was flexible enough that for most "backend" views we never customised it on the frontend. But being able to have the frontend devs customise the user visible things was essential, and thanks to having the ability to replace things bit by bit they could focus on the things that didn't look or work right, instead of "everything". And a lot of their work then made it back into augmenting the backend with info to let the generic frontend apply those improved views elsewhere as well.
I have only had a cursory look at FastUI. If they do something similar to that, then great. If they actually try to have the backend generate all or most of the UI and serve that up instead of serving up metadata, then I fully agree with your issues.