Dan Abramov asked me to help fix React
HTML-код
- Опубликовано: 15 мар 2023
- useRef is pretty weird in TypeScript. Dan Abramov agreed - and he asked me to try to find any more weirdness with TypeScript and React.
The tweet: / 1636144047248031744
Become a TypeScript Wizard with my free beginners TypeScript Course:
www.totaltypescript.com/tutor...
Follow Matt on Twitter
/ mattpocockuk
Join the Discord:
mattpocock.com/discord
Using forwardRef with generics is an absolute nightmare and require a bunch of hacks. Would love that it would just work (actually I would love that forwardRef wasn’t even needed)
Was just about to comment this but I knew it would already be here
I remember when I first came across this bug, wasted hours trying to understand why the heck my generics arent working. The only thing I've came up with is creating my own type definition for it.
I double that. The most broken thing in React typings now. Also refs, but not as much.
this is a problem with typescript itself. It is impossible to have a higher order function that somehow preserves the generic type argument. The only thing the react team could do is forward refs by default.
@@fallenpentagon1579 no it is not, look at the type declaration I use for it
import "react";
declare module "react" {
function forwardRef(
render: (props: P, ref: Ref) => ReactElement | null
): (props: P & RefAttributes) => ReactElement | null;
}
It works with any number of generic arguments for component props, just like it should.
The only problem I have with it is that I cant do MyComponent.displayName = "MyComponent" on those, but theres probably a way to fix it, I just coundn't bother.
One confusing thing is the amount of types available to describe components, and what happens with them. Whether you're talking about the function (or class) that implements the component, an instance of this class (whatever that means for FCs), the return value when it's invoked, the valid types for children, the render prop pattern..
IMO, the highest-value thing to be changed (not necessarily "fixed") is Component types:
JSX.Element, ReactNode, ReactElement, FC, ComponentType, ComponentElement, ReactComponentElement
Totally understand I'm massively oversimplifying the complexity of this (esp. backwards compat), but from a DX perspective, having all of these types exposed is a maddening experience.
I would love if there was just a single type exported, something like `ReactComponent` (which somehow, through all these types, does not yet exist...)
the problem is that most of these do different things. ReactElement is what a React functional component (FunctionComponent, also aliased as FC) returns, while ComponentProps (which I think is what you are referring to with ComponentType? I've also never seen ComponentElement and ReactComponentElement, sorry) is a utility to get the props type from a component or intrinsic element (for example "div"). The only thing here that *could* be fixed is that typescript infers ReactElements as JSX.Element, even tho it could be more precise here. ReactElement is also an important type for React.cloneElement. FC already is what you want as ReactComponent, it just had to be called FunctionComponent/FC because class components also exist and are very different to typescript, a union type wouldn't be helpful here at all.
JSX.Element cant really be fixed by the react team, it comes directly from Typescript and is a black box.
From the typescript docs (under /docs/handbook/jsx.html#the-jsx-result-type):
"By default the result of a JSX expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box."
I use React.ElementType for the component, and React.ReactNode for the element
yeah indeed, seem a bit confusing somehow. what am i supposed to use for this one and that one.
Matt - HOC patterns with TS and React are a complete nightmare. And we need to be able to add constraints for children components!
Constraints for children components cannot be added by the react team. Typescript's JSX.Element interface is a black box.
From the typescript docs (under /docs/handbook/jsx.html#the-jsx-result-type):
"By default the result of a JSX expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box."
Being able to explicitly type children would be a gamechanger. I think a ton of API's use props, like renderProps, just because it's difficult/impossible to constrain children.
I'd love to see a video of you fixing this issue and explaining things along the way.
+1 to this suggestion!
It's long past time Flow was put out to pasture. As an aside, it amazes me that the world's most prevalent front-end library doesn't even ship with its own types. I imagine this is because of their inability to cut ties with Flow.
I was shocked to hear Flow was not dropped years ago!
@@malvoliosf Not only that, but Flow actually has a more advanced type system than TypeScript in many ways
Flow is actually pretty cool tho. I miss it when encountering TS “deficiencies”
I work with flow and typescript in my job and... flow is real pain to maintain! So many breaking changes, missing some features like module augmentation, some basic utilities of typescript and very small community. Can you list some features where flow is better than typescript ?
@@cmmarttifacebook IS the creator of flow and maintain flow for his own needs only. Nothing surprising there.
You really are the TypeScript wizard 🤯
3:20 the forwardRef, i was working with react hook form, creating a custom controller and it is totally unusable with typescript
As a workaround, u could use a *.d.ts file with the following:
import "react";
declare module "react" {
function forwardRef(
render: (props: P, ref: Ref) => ReactElement | null
): (props: P & RefAttributes) => ReactElement | null;
}
The only downside - u cant do MyComponent.displayName = "MyComponent" on those components. And obviously, u could restrict generic for props here
You're doing an extremely wonderful job, Matt! I can't skip any of your videos since these are all so valuable! You rock!
forwardRef, standardized representation of HTML element props, components with/without ref
One thing that I really suffer when using TS with react are the event properties and this one you just mentioned, the ref hook.
About the events are that they're not aware of the type of event they might end up using, that's because they happen to suffer from the same as the ref, and that is that they're not aware of which tags and attributes compose them and therefore can't infer the type automatically.
You really take on all the typescript pet peeves I have! Thank you!
Thanks for making the typescript space better!
Great initiative and video! As for type improvement suggestions, tweaking the type definitions for memo / forwardRef in order to make them work with generic component props would be pretty sweet.
Literally had this problem yesterday
100%
this is a problem with typescript itself. It is impossible to have a higher order function that somehow preserves the generic type argument.
ReactElement is typed in a very weird way, because it takes `` as type arguments. So it takes in the shape of the properties of a component constructor but then passes `any` along for the JSXExlementConstructor, essentially decoupling the element props from the constructor props?
Would be much cooler (and intuitive to use!) if `ReactElement` could infer out the property typings of a single generic parameter defining the component type.
That's Amazing!
You know what's really annoying? useReducer. It shows errors in such a non-obvious way! Create some redocer that handles complex object. Change some types in handlers, in states or somewhere else. Or mb even in init function. Or Init value. It always (well mb not always, but inconsistency is also bad, innit?) tells some bullshit in error and underlines whole reducer, not the initial values. Sometimes it happend that error in one of handlers, but reducer is still highlighted as errored. Reducer mechanics and errors are so messy in typescript. Sry for my English btw.
Discriminated unions of props. I'd love to be able to have an Input component where based on the type prop I pass to it it's going to take a different set of propsz or even just different types on value and onChanfe. You can do this with plain objects using typescripts discriminated unions, but in react this doesn't work well at all. I often end up passing a data prop which has the discriminated union there, but it doesn't feel right.
another thing that I *think* is a bug in react's types package is that when a component returns undefined/void (which is allowed since React 18) you still get an error when using the component saying that the function can't be used as a JSX expression. So I'm still stuck doing `return null`.
Edit: I think the reason this is not that easy to change is due to other users of JSX such as solid which are more strict about stuff like not allowing early returns. Backwardscompatibility shouldn't be the big problem here since you're *supposed* (nobody does, I know y'all have @types/node at 20.x even if you're actually using 16 or 18) the major (and sometimes even minor) versions of types packages with the installed version of the actual library, so users of React 17 should be using @types/react@^17
Same thing with returning strings or arrays. Have to resort in ugly fragment wrapping, like this hello
Off topic : do you have a video or a link that describes the tools and devices (so your setup) you are using to record and mount your youtube videos (cam / microphone / Softwares /../..) ?
A few things, the JSX typings are not all properly matched to their respective HTML interfaces, e.g. the details element I believe is one of several that is associated to the wrong underlying element. So speaking of refs, you’ll have to assign a ref of the wrong type because of this! Also, the forwardRef function makes typing higher order components extremely difficult, I can share examples if you like!
Oh yeah. I've recently had to redefine forwardRef because it was eating my component generics. The solution was on stack overflow and apparently the custom forwardRef definition I'm using uses a feature of ts called something something higher order functions? I think?
I ran into that exact use ref problem over the weekend
Matt! This is probably the most annyoing typing problem when using React with Typescript (which is a standard these days imo)
If you manage to fix, that'd be awesome!
I always stumble upon an issue with a comma after generic type in component. For some reason it doesn’t understand that it is type unless you put a comma after this generic type or extend it from somewhere
From now on I will call you a TS doctor
I’ve been beating this exact drum for ages!!!
the forwardRef function is also hard to work with in typescript.
Seems like if you type createContext it requires you to init the context with the type and doesn't allow null
It's not possible to strongly type 'children' to the specific component type. You can force 'children' to be a primitive type or a component, but not a specific component.
I really wish there was a way to type child components. In plain old HTML, I nest option elements inside of a select element. If I want to replicate this exact same API with a custom "Select" component and a custom "Option" component, I have no way to limit the allowed child components to only be my custom "Option" components.
What I can do today is to pass an array of option entries to my "Select" component - but this is far from ideal: Let's say my custom "Select" component can display text options so I accept entries of type string array. Now this changes and I want to be able to show a small image next to the text. So, I have to change the "Select" component types to accept that. Now I want to be able to show a larger image without any text as an option. I have to change the "Select" component again. This sucks... It would be WAY NICER to have a slightly different custom "Option" component (like "TextOption", "TextWithImageOption", "ImageOption" etc.) that can be passed into my custom "Select" component. Adding or changing one of the "Option" component would not affect the internals of the "Select" component.
Currently, that's not possible so I only have two options: Accept to not have type safety for my "Option" components (and thus not preventing putting the "MainNavigation" component inside of the "Select" component (for whatever reason this might happen)) or loose the API flexibility
Typing child components cannot be added by the react team. Typescript's JSX.Element interface is a black box.
From the typescript docs (under /docs/handbook/jsx.html#the-jsx-result-type):
"By default the result of a JSX expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box."
Step 1: Live vicariously through Pocock
Step 2: ...
I was breaking my head the other day trying to make Component generics a thing together with ForwardRef, AND at the same time being able to do Component.displayName="something" so it's shows when debugging. Couldn't find a solution for this
I remember there was a bit tricky to type the useContext with an initial value, can't remember exactly how but it wasn't straight-forward as one would expect.
It is quite tricky. The default value has to be initialized in global context, when you usually don’t have enough information yet, so you initialize it to null, and then *really* initialize it inside your App component. This messes up the typing, since it is apparently null-able, even though that can never happen in the lifetime of a component.
@@malvoliosf what I've always done is to create a hook like useMyContext() which does the null-check and either throws or returns the non-nullable value, which was a fair amount of boilerplate
Yes! Yes! Yes! Finally, I will not have to suffer this abomanation of typing logic any longer!
Matt, how you are adding the Typescript errors as comments to your code automatically?
Ok is that I wishlist? I wish I could enforce children to be a specific component type, eg: can only have .
🤞
I found this same issue a long time back, I should have tweeted then 😂
I would appreciate if TS could trust the JSX a little more.
{ bar && }
name won't be set to bar if bar is undefined. But TS isn't convinced..
With using forwardRef, you have to write component.displayname, otherwise it cannot understand. I dont understans why, eslint starting to error and also, if I have some errors in that component, reacts error handler cannot provide the exact position of error
fix/explore forwardRef types pleaseee
Not sure if it's about type script but I am a newbie and have problems with ESM or something in that area.
Update node-fetch from 2.6.1 to 3.1.x and the code goes nuts about require, it wants import.
The issue is that the error message suggests some solutions with adding a type in the package.json with module or commonjs and changing the file from js to mjs or cjs and nothing really seems to work.
Maybe I don't understand something really small and fundamental in the move from JS to TS but I want to avoid moving to TS because it's a major change as I see it and the code is not strongly typed.
It's a lot of work and I don't have the time to do it.
People on the internet say to update to the latest of 2.6 which what I did and it works.
What I'm saying is that error messages need to be more helpful...
forwardRef, HOC, passing custom components as props.
Enabling
return boolVar &&
instead of
return boolVar ? : null
JavaScript enables to return Boolean so why typescript not?
forwardRef when component is generic is pain and requires casting and lots of hacks.
Something I would like is a generic component. Lets say I am making an element to display a list, any list, maybe it could be typed like this: `const DisplayList: FC JSX.Element }> = ...`
You can do that already.
const Component = (props: Props) => ...
@@fallenpentagon1579 Thanks, but that’s what I do now! I just want it integrated into the React standard types, using something equivalent to FC.
@@malvoliosf IMO you simply shouldn't be using FC in the first place. It does not work with generics and it never will. The solution you suggested is simply not possible with typescript. The benefit of FC is questionable anyway. What is the benefit over just using const Component = (props: Props) => ... or even function Component(props: Props) {...} ?
When using forwardRef, the generics needed for the props and ref are opposite to the arguments, which is small but super annoying.
Amazing. Will this fix be included as part of ts-reset?
Logic says no. The ts-reset package is general use. Adding anything specific to another library would not make any sense.
Could potentially make ts-reset-react, but I'm hopeful that we can fix these in React itself.
Nice video! One thing that would be cool is to see your PR done to fix this issue so people can know exactly what you did to fix it :)
At this point, is it not just better to merge "type" and "interface" or remove on or the other?
No, because they serve different purposes.
@@mattpocockuk Thank you for your reply Matt! I understand that, and I agree, however they are often used interchangeably, so I was wondering if this was remotely feasible.
You even said it yourself (apologies for the finger-pointing) that you changed your mind many times or went through different phases from using one or the other, though I hope your third phase is the last one too! :D
My point is why should we keep going through these phases, making it confusing for everyone?
@FlyingR That’s like saying we should remove var because const and let exist. var acts slightly differently and removing it would break legacy code.
In the same vein, type and interface are slightly different. You cannot extend a type because it is intentional strict. You can extend an interface, because it is intentionally ambiguous. These distinctions are important.
@@nadavrot
I mean, I'm sure lot's of people wouldn't mind removing var either :D But yeah, I sort of get it now, thanks!
Generic functions wrapped into memo are broken. I should cast type of memo's result by 'as unknown as typeof MyComponent'
how does that solve anything? Typescript simply has no way of representing generic higher order functions. There is an issue (microsoft/TypeScript/issues/1213) which has been open for a long time that describes this problem and it appears that the consensus is that typescript should get higher kinded types.
@@fallenpentagon1579
function MyComponentInner(){}
export const MyComponent = memo(MyComponentInner) as unknown as typeof MyComponentInner
What's the proper way of defining props with React?
Is it
const Component: React.FC = (props) =>
or
const Component = (props: PropsType) =>
or something else..
The second one is the best solution IMO. It does not validate propTypes but who uses those together with typescript anyway
The second one is preferable since React.FC was loosely deprecated if I remember correctly.
Note to self:
1) fork React
2) `rename js ts *.js`
3) fix errors
4) release brand new, *blazingly fast* Tcaer framework
5) profit
Ok lets talk about polymorphic component and as or component prop please!!!
no way pog
Why not show us the code you wrote to fix the types?
cloneElement required type omitting
Example:
const Foo = (props: { foo: string; bar: string }) => ...
const BarWithSlot = (props: { action: ReactElement }) => {cloneElement(props.action, { foo: 'baz' })}
// "foo" is required to pass even tho BarWithSlot will override it
It would be nice to have strongly typed JSX. Rather than JSX.Element for everything
Typing children or react element e.g. react.reactNode vs react.element vs react.fc …etc
You’re simply a wizard
Compound components that have implicit props are really not great when you add TypeScript into the mix. Right now our workaround is to make all implicit props on the child components optional, but as you can imagine that's far from ideal.
we need higher kinded types
Do you actually get anything done or just fight your tools lol
what does useRef do?
Stores a reference to a value that is persisted throughout the lifecycle of a component. It is generally used for hooking into the actual DOM nodes.
@@wlockuz4467 does component lifecycle ends when the ui rerenders?
@@jimshtepa5423 No
useRef can be used for two main things:
const myRef = useRef()
1. Associate an element in the return statement ( e.g. Hello World ). You can then in JavaScript say myRef.current.propertyName to alter the element property values directly.
or
2. You can use it to remember a value WITHOUT re-rendering (returning HTML). Think of it like useState (which triggers returning new HTML again) - it saves the value AND triggers. useRef just saves he value. To access the value you refer to it in JavaScript using myRef.current (e.g. myRef.current = “hello world”).
Honestly, typing the props of a component is a pain. I'd rather have an inferred way with a simple interface than needing something like React.FC and also inferred returns (if this is even recommended)
Like can we just make a function with an interface param be a component straight away?
So happy with Svelte when I see things like this.
forwardRef should be easier to use, for me it’s never inferring the function argument props and I need to manually pass the generic arguments.
forwardRef should also have any as default type for the ref, having a strict type for the ref argument is never useful in my experience
fix enums!
Matt, I have an issue that you might be awesome for. JSX isn't checked in any IDE (not even by ESLint) as HTML. Because it isn't. The problem is that people make many errors and not being aware of it. How difficult (very difficult, I am aware, probably) would it be to have JSX be validated as HTML by TypeScript? Could TS error out when someone puts a inside an ? Could it also bubble up from custom components e.g. asd and throw an error, if that MyButton component renders a button element?
I'm seeing many websites with mistakes exactly like that, and it's honestly driving me crazy. Good HTML matters for SEO, accessibility, and many other reasons, and (unfortunately) too many people are simply not familiar of the W3C validator ;)
What would your take be? Could TS do this, and do it with good performance?
Refs are just confusing all-up
forwardRef types are a bit annoying too.
That you cannot use "ReactNode" for this:
type TitleProps = {
value: ReactNode;
};
const Title: FC = ({ value }) => {
const isString = Object.prototype.toString.call(value) === '[object String]';
if (isString) {
return (
{value}
);
}
return value;
};
Return type 'Element[]' is not a valid JSX element.
You look younger than in your profile pics
CSSProperties don't allow custom propierties, but in TS now they could add support for template literal types now, so `--${string}` should be a valid property
React has been a lost cause the day it was released to the public.
Cool but I need Typescript fixed in Vue
I don't think it's possible.
For one, Vue uses reactive objects that are not easily typed. Vue lies to you about the true type of objects. If Vue says that your state is an array, it is not actually an array, but a proxy to an array with added functionality.
And Vue templates is a domain specific language that is not actual JavaScript or TypeScript. Typescript in Vue templates is a hack where they parse out the JavaScript parts from the templates, create a temporary function where they mock the context in which the code is going to be executed in, sends it to the typescript service, and map the resulting errors etc back to the corresponding character positions in the template. (Something like it, I'm not an expert).
@@majorhumbert676 isn't a proxy to an array still an array, so Vue isn't technically lying?
@@FunctionGermany I think if a framework is telling me that something is a string[], I expect that it behaves like a regular array. For example, try to pass this object to Window.postMessage and you'll get an error, even though passing a regular array works just fine. Or if I stringify it and then parse it back into an array, I expect the same it ro behave the same as it dif before.
@@majorhumbert676Might be true that there are some fundamental flaws about the Typescript support, but I think there are some fixable things, like being able to import a type and use it in macros like `defineProps` (which I believe they are working on for version 3.3).
About the proxies, I'm fine with that. I don't see it as lying, just hiding some internal mechanism, and isn't that a good thing in programming?^^
React needs a hard reset
This wasted a lot of time for someone learning react with typescript
Typescript is the problem. Eliminate that, problems gone
we should just stop using react
Dynamic forward ref
Example:
type TypographyProps = (
({ component: 'span' } & HTMLAttributes) |
({ component: 'a' } & AnchorHTMLAttributes) |
({ component: 'time' } & TimeHTMLAttributes) |
) & { ... }
const Typography = forwardRef(({ component: Component, ...props }: Props, ref: /* ??? */) => (
))
How to get good at dev? 🥲
Practice
Create react from scratch
The same way you would get good at anything else.
Don't listen to these nitwits that suggest practicing. Only very weak devs write code.