I've noticed that a thing that happens a lot in my code is a ternary operation that narrows a type so I could use it inside the jsx, for example:
user: { name: { first: string, last: string } } | null;
...
return (
<div>
{user ? (
<>
<span>{user.name.first}</span>
<span>{user.name.last}</span>
</>
) : <LoadingSpinner />}
</div>
);
I would like to do is make a component to replace this piece of code (that repeats itself in multiple places) and do the filter for me, something more like:
const Loading = ({ loading, children }: { loading?: boolean, children: React.ReactNode }) => {
if (loading) return <LoadingSpinner />;
return <>{children}</>
}
...
...
...
return (
<div>
<Loading loading={!user}>
<span>{user.name.first}</span>
<span>{user.name.last}</span>
</Loading>
</div>
)
the problem with this is that typescript doesn't consider this as a type narrower / type guard and will say that user
can be null (and for a good reason, this actually might cause a bug if you assume the prop is not null / undefined, because the JSX children will throw error when user is null)
So my question is, is there a good concise way to create such a component that would both narrow the type somehow and protect me from errors (in a generic way, not just specifically for a certain prop or the other). So far the way I thought of requires a lot of function wrapping and props (using Required
and NonNullable
to be really safe and generic, but I'm just not sure it's worth replacing all of my occurrences for that, because it just might be longer
Be glad to hear your ideas!
Update 1:
Okay, I think I kind of figured out that what I want is not really possible, which is using a component as a type guard. I myself went for a bit more strongly typed solution, inspired by some of the comments here (Thanks guys!) which I'm adding here (keep in mind that it's not perfect, it was just suited for my usage)
type PartialOrNull<T> = {
[K in keyof T] ?: T[K] | null;
};
type RequiredNonNull = {
[P in keyof T]-?: NonNullable<T[P]>;
}
type RequiredRenderFunction<T> = (data: RequiredNotNull<T>) => React.ReactNode;
type NotRequiredRenderFunction<T> = (data?: T | null) => React.ReactNode;
interface LoadingProps<T extends object> {
loading?: boolean;
data?: T | PartialOrNull<T> | null;
render: RequiredRenderFunction | NotRequiredRenderFunction;
require?: boolean;
}
const notUndefinedOrNull = <V,>(value: V | null | undefined): value is V => !(value === null || value === undefined)
function Loading<T extends object>({ loading = false, data, render, require = false }: LoadingProps<T>) {
if (loading) {
return <LoadingSpinner />;
}
if (!require) {
return (<>{(render as NotRequiredRenderFunction<T>)(data)}</>);
}
if (!data) return null;
const values = Object.values(data);
if (!values.length || values.filter(notUndefinedOrNull).length !== values.length) {
return null;
}
return (<>{(render as RequiredRenderFunction<T>)(data as RequiredNotNull<T>)}</>);
}
And then the usage is:
interface User {
name: {
first: string;
last: string;
}
};
const UserComponent = ({ user: User }) => {
return (
<>
<span>{user.name.first}</span>
<span>{user.name.last}</span>
</>
);
};
// ...
// ...
// ...
const App = () => {
const user = User | null = useGetUser();
return (
<div>
<Loading loading={!user} require data={{user}} render= {UserComponent} />
</div>
);
}
CodePudding user response:
You can use a generic Loading
component with a render prop. I know it requires a bit of refactoring, but you get a reusable, generic Loading
component:
import React from 'react'
const Loading = <T,>({ loading, render, data }: { loading?: boolean, data: T, render:(data: T) => React.ReactNode }) => {
if (loading) return <div>loading</div>;
return <>{render(data)}</>
}
interface User {
name: {
first: string;
last: string
}
}
const C = () =>{
const user: User = {name:{first: 'rwithik', last: 'manoj'}}
return (
<div>
<Loading loading={!user} data={user}
render={(data) => {
return <><span>{data.name.first}</span>
<span>{data.name.last}</span></>
}}
/>
</div>
}
You can even remove the loading
props and just check if data
is valid.
CodePudding user response:
Applying KISS, I would go for something simpler
Syntax Solution
First of all, maybe you want to move those few lines to a new component
const UserComponent = ({user}) => {
return (
<>
<span>{user.name.first}</span>
<span>{user.name.last}</span>
</>)
}
And then, readability wise it would look like:
<div>
{user ? <UserComponent user={user}/> : <LoadingSpinner />}
</div>
Another option could be
<div>
{!usser && <LoadingSpinner /> }
{user && <UserComponent user={user}/>}
</div>
I know it is not exactly building a new component, but maybe it is enough for your use case.
Using Suspense
Another option could be using Suspense
, which looks like what you are trying to achieve. It was designed for fetch cases.
<Suspense fallback={<LoadingSpinner />}>
<UserComponent />
</Suspense>
Let me know if it helped.
CodePudding user response:
You can use optional chaining with your original method:
<Loading loading={!user}>
<span>{user?.name.first}</span>
<span>{user?.name.last}</span>
</Loading>
Where Loading
is defined as:
interface LoadingProps {
loading?: boolean;
children: React.ReactNode; // required since react 18
}
const Loading: React.FC<LoadingProps> = React.memo(({ loading = false, children }) => {
if (loading)
return <LoadingSpinner />;
return <>{children}</>;;
})
Typescript shouldn't complain with this.