I'm totally new to using Typescript and having problems rendering a component but also passing in an onClick function. How can I pass in an onClick function to the CarItem Like this? I think it's trying to treat the onMenuClick as a proeprty of ICar but it isn't and shouldn't be. The onMenuClick shouldn't be part of the ICar interface. It is merely a function. I've put a simple example below.
interface ICar {
name: string,
colour: string,
manufacturer: string
}
const CarGrid : React.FC<ICarGridProps> = car => {
return (
<CarItem {...car} onMenuClick={onClick} />
)
}
const CarItem : React.FC<ICar> = (car, onMenuClick) => {
return (
<div onClick={() => onMenuClick(car.name)}>{car.name}</div>
);
}
Thanks all.
There are a few things wrong here:
I've corrected the return of CarGrid and type def and params of CarItem
interface ICar {
name: string,
colour: string,
manufacturer: string
}
const CarGrid : React.FC<ICarGridProps> = car => {
return (
<CarItem car={car} onMenuClick={onClick} />
)
}
const CarItem : React.FC<{car: ICar, onMenuClick: Function}> = ({car, onMenuClick}) => {
return (
<div onClick={() => onMenuClick(car.name)}>{car.name}</div>
);
}
Or if you want to spread the car object into CarItem, it should be refactored:
...
<CarItem {...car} onMenuClick={onClick} />
...
const CarItem : React.FC<ICar&{onMenuClick: Function}> = ({name, onMenuClick}) => {
return (
<div onClick={() => onMenuClick(name)}>{name}</div>
);
}
Related
I am trying to dynamically pass props so I can output different photo galleries on different pages. 1 out of the 3 props works, the rest comes out as undefined with a warning that a title element received an array with more than 1 element as children. I am new to NextJS and couldn't find a solution online.
Here's what I have:
A TS file:
type PostType = {
slug: string
title: string
date: string
photoSrc: string
photoConst: string
coverImage: string
ogImage: {
url: string
}
}
export default PostType
I got this post markdown file here:
title: 'Costa Rica'
coverImage: '/assets/work/costa-rica/cover.jpg'
date: '2022-08-11'
photoSrc: '/assets/work/costa-rica/grid/'
photoConst: 'COSTA_RICA'
I pass these props to a Component:
export default function Post({ post, morePosts, preview }: Props) {
const router = useRouter()
if (!router.isFallback && !post?.slug) {
return <ErrorPage statusCode={404} />
}
return (
<Layout preview={preview}>
<PostHeader
title={post.title}
coverImage={post.coverImage}
date={post.date}
/>
<PostBody content={post.content} />
<PlockGrid title={post.title} photoConst={post.photoConst} photoSrc={post.photoSrc} />
Here are the constants that I want to use as a number to loop through my photos:
export const FRANCE_PHOTOS = 10
export const COSTA_RICA_PHOTOS = 12
export const SWITZERLAND_PHOTOS = 16
Here's the PlockGrid component:
import {FRANCE_PHOTOS, COSTA_RICA_PHOTOS, SWITZERLAND_PHOTOS} from '../lib/constants'
type Props = {
title?: string
photoSrc?: string
photoConst?: string
}
const PlockGrid = ({title, photoSrc, photoConst} : Props) => {
return (
<Plock gap="2rem">
{
[...Array({`${photoConst}_PHOTOS`} - 1)].map((e, i) => {
console.log(photoConst)
return <img
key={i + 1}
src={`/assets/work/${photoSrc}/grid/${i + 1}.jpg`}
alt={`${title} detail`}
className="w-full"
/>
})
}
</Plock>
)
}
export default PlockGrid
Here, the {`${photoConst}_PHOTOS`} things is not working properly & also, this path src={`/assets/work/${photoSrc}/grid/${i + 1}.jpg`} is not working properly. console.log(photoConst) logs undefined, just like console.log(photoSrc) does. It all works when I put in paths manually without any props:
const PlockGrid = ({title, photoSrc, photoConst} : Props) => {
return (
<Plock breakpoints={breakpoints} gap="2rem">
{
[...Array(COSTA_RICA_PHOTOS - 1)].map((e, i) => {
console.log(photoConst)
return <img
key={i + 1}
src={`/assets/work/costa-rica/grid/${i + 1}.jpg`}
alt={`${title} detail`}
className="w-full"
/>
})
}
</Plock>
)
}
What is going wrong?
It looks like the expression ${photoConst}_PHOTOS resolves into a string (name of a constant), like FRANCE_PHOTOS. From what I see, you actually want a value behind, i.e. 10.
So you need to convert that constant name into value. A bad idea would be to use eval:
const FRANCE_PHOTOS = 10;
const COSTA_RICA_PHOTOS = 12;
const photoConst = 'FRNACE';
const x = eval(`${photoConst}_PHOTOS`)
console.log(x)
A better idea coudl be to use object with photos, so you can index it easilty:
const PHOTOS = {
FRNACE: 10,
COSTA_RICA: 12
}
const photoConst = 'FRNACE';
const y = PHOTOS[photoConst]
console.log(y)
The point is, putting variable in curly brackets like this: {variable} does not necessarily get "value" of the variable in javascript. Usually, it is just a syntax error (but not in string interpolation jsx code, but even there it does something a bit different). So in your case, the illustration of the two "options" above could be:
// bad code, dont do this
const PlockGrid = ({title, photoSrc, photoConst} : Props) => {
...Array(eval(`${photoConst}_PHOTOS`) - 1)]
}
// better code, uses object with constants instead of plain constants
const PlockGrid = ({title, photoSrc, photoConst} : Props) => {
...Array(PHOTOS[photoConst] - 1)]
}
There is a problem, when i type anything in search field, suggest variants looks weird ( on the picture) because endings is cut. I want to cut down not a single word, but a sentence( first part of suggest ).
export const SkillAutoSuggestOption = (
props: React.HTMLAttributes<HTMLLIElement>,
option: any,
state: AutocompleteRenderOptionState,
getName: (option: any) => string
): React.ReactNode => {
const matches = match(getName(option), state.inputValue, { insideWords: true })
const parts = parse(getName(option), matches)
const path = () => {
if ('skillPathForAutoSuggest' in option) {
return option.skillPathForAutoSuggest
} else return getNodePath(option)
}
return (
<ListItem {...props} key={option.skill_id}>
{option.isCategory && <SCircleIcon />}
{parts.map((part, index) => (
<SLabel key={index} style={part.highlight ? { color: tannBlue } : {}}>
{part.text}
</SLabel>
))}
<SSkillPath>{path()}</SSkillPath>
</ListItem>
)
}
Can you add the getNodePath, parse and match functions source code/the source library if you use any? The problem is not related to rendering the list part but is related to how these functions work.
I was asking this question with a more complex version of this basic concept
Rel: Can Generic JSX.Elements work in Typescript
I narrowed it down to the core Elements:
This is Object A that takes parameters from TypeA
type TypeA = {
label: string
value: number
}
const ObjA = ({ label, value }:TypeA) => {
return <div>
<div>Label: {label}</div>
<div>Value: {value}</div>
</div>
}
This is Object B that takes parameters from TypeB
type TypeB = {
label: string
value: string
bool: boolean
}
const ObjB = ({ label, value, bool }:TypeB) => {
return <div>
<div>Label: {label}</div>
{bool && <div>Value: {value}</div>}
</div>
}
Now I collect this ComponentGroup inside an array and create a Type out of this Array:
const ComponentCollection = [
ObjA,
ObjB
] as const
type Components = typeof ComponentCollection[number]
Then I create a generic component:
interface GenericProps<T extends Components> {
Component: T
title: string
}
const Generic = <T extends Components,>({ Component, title, ...props }:GenericProps<T>) => {
return (
<div>
<label>{title}</label>
<Component {...props}/>
</div>
)
}
At last I can call the generic component as follows:
<Generic Component={ObjA} title={'Usage A'} label={'Object A'} value={'String A'}/>
<Generic Component={ObjB} title={'Usage B no Bool'} label={'Object B'} value={0}/>
<Generic Component={ObjB} title={'Usage B with Bool'} label={'Object B'} value={0} bool/>
Altough it works really well in JavaScript, I messed something up with the typing.
I setup one TS-Playground and one Codepen:
TS-Playground: https://tsplay.dev/WvVarW
Codepen: https://codepen.io/Cascade8/pen/eYezGVV
Goal:
Convert this code above in correct TypeScript code
Compile without any TS-Errors or /#ts-ignore
Make IntelliSense work, so if you type <Generic Component={ObjA} ... it shows the available type attributes for this Object. In this case: label={string: } value={string: }
What i don't want:
Usage of classes or the old function syntax as our EsLint requires us to use an Arrow-Function if possible.
Passing the Objects as a Child.
I know this works, but it is not the prefered solution as the main Project has a lot of groups like this that get rendered like this.
And why shouldn't something work in TypeScript that works very simple in JavaScript.
Following the way you've constructed your types, you can do it using the definition of your generic inside GenericProps
(Note, I have bundled the props into a new props prop, as this should avoid name collisions should you have naming collisions)
import React from 'React'
type TypeA = {
label: string
value: number
}
const ObjA = ({ label, value }:TypeA) => {
return <div>
<label>{label}</label>
<label>{value}</label>
</div>
}
type TypeB = {
label: string
value: string
bool: boolean
}
const ObjB = ({ label, value, bool }:TypeB) => {
return <div>
<label>{label}</label>
{bool && <label>{value}</label>}
</div>
}
type Components = typeof ObjA | typeof ObjB;
interface GenericProps<T extends (...args: any) => any> {
Component: T
title: string
props: Parameters<T>[0]
}
const Generic = <T extends Components,>({ Component, title, props }:GenericProps<T>) => {
return (
<div>
<label>{title}</label>
<Component {...props as any}/>
</div>
)
}
const Usage = () => {
return <Generic Component={ObjA} title={'Usage'} props={{label: 'ObjectA'}}/>
}
export default Generic
It is possible to do without any kind of type assertions.
Consider this exmaple:
import React, { FC, } from 'react'
type Type = {
label: string;
value: string | number
}
type TypeA = {
label: string
value: number
}
type FixSubtyping<T> = Omit<T, 'title'>
const ObjA = ({ label, value }: FixSubtyping<TypeA>) => {
return <div>
<label>{label}</label>
<label>{value}</label>
</div>
}
type TypeB = {
label: string
value: string
bool: boolean
}
const ObjB = ({ label, value, bool }: FixSubtyping<TypeB>) => {
return <div>
<label>{label}</label>
{bool && <label>{value}</label>}
</div>
}
type Props<T> = T & {
title: string
}
const withTitle = <T extends Type>(Component: FC<FixSubtyping<Props<T>>>) =>
({ title, ...props }: Props<T>) => (
<div>
<label>{title}</label>
<Component {...props} />
</div>
)
const GenericA = withTitle(ObjA)
const GenericB = withTitle(ObjB)
const jsxA = <GenericA title={'Usage'} label={'A'} value={42} /> // // ok
const jsxB = <GenericB title={'Usage'} label={'A'} value={'str'} bool={true} /> // // ok
Playground
This error occurs because props is infered as Omit<T,'title'> so we should assure TS that Component props is compatible with Omit<T,'title'>
However there is a drawback, you need to update props type in other components. If it is not an option, I think the best approach would be to overload your Generic function:
import React, { FC, } from 'react'
type TypeA = {
label: string
value: number
}
type FixSubtyping<T> = Omit<T, 'title'>
const ObjA = ({ label, value }: TypeA) => {
return <div>
<label>{label}</label>
<label>{value}</label>
</div>
}
type TypeB = {
label: string
value: string
bool: boolean
}
const ObjB = ({ label, value, bool }: TypeB) => {
return <div>
<label>{label}</label>
{bool && <label>{value}</label>}
</div>
}
type Props<T> = T & {
Component: FC<T>,
title: string
}
function Generic<T,>(props: Props<T>): JSX.Element
function Generic({ Component, title, ...props }: Props<unknown>) {
return (
<div>
<label>{title}</label>
<Component {...props} />
</div>
)
}
const jsxA = <Generic Component={ObjA} title={'Usage'} label={'A'} value={42} /> // // ok
const jsxB = <Generic Component={ObjB} title={'Usage'} label={'A'} value={'str'} bool={true} /> // // ok
Playground
I have a component FooterScroll. And the main page is TvIndex.
This is the FooterScroll Component
const FooterScroll = (Id: number) => {
const { event } = useEvent(Id);
console.log('Id', Id);
return (
<Marquee speed={85}>
<Grid container>
{event?.notices.map((notice: EventNotice) => {
if (notice.approved) {
return (
<>
<Grid item px={4} key={notice.id}>
<CampaignIcon />
</Grid>
<Grid item alignItems="center">
{notice.body}
</Grid>
</>
);
}
})}
</Grid>
</Marquee>
);
};
export default FooterScroll;
This is the TvIndex Page
import FooterScroll from 'components/pages/members/tv/scrolling-footer';
const TvIndex: React.FC = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<div className={classes.scrollFooter}>
<FooterScroll Id={39324} />
</div>
</div>
);
};
export default TvIndex;
For some reasons that I don't understand, when I pass in this any id number as a prop in my main component for FooterScroll, it gives me this error: Type '{Id: number;}' is not assignable to type 'number'.
Then I decided to console log it inside FooterScroll, and it looks like after the id gets passed in TvIndex, it passes inside the FooterScroll component as an Object, like so: Id > Object { Id: 39324 }.
How can I solve this?
The argument passed to a React component is an object, whose properties are the props passed down by the caller. For example
<FooterScroll Id={39324} />
results in a prop object of:
{
Id: 39324
}
React doing it this way allows for easily adding additional props, or properties to the props object, without changing the function signature much:
<FooterScroll Id={39324} someOtherProp="foo" />
results in a prop object of:
{
Id: 39324,
someOtherProp: 'foo'
}
So, you need to change the function definition to
const FooterScroll = ({ Id }: { Id: number }) => {
or do
const FooterScroll = (props: { Id: number }) => {
const { Id } = props;
Your arguments to the FooterScroll are wrong. Try this:
const FooterScroll = ({ Id }: { Id: number }) => {
...
}
It should work.
I have an array of JavaScript objects that I holding in React State, and on a click, I change the property of one of the objects in the array.
I got the following to work without mutating state, but my current setState() syntax also adds the same object to the end of the array again.
How can I simply change the state of one of the objects in my array of objects in state, without adding another object and without mutating state?
import React, { useState } from 'react';
interface IFlashcard {
noun: string;
article: string;
show: boolean;
}
const initialFlashcards = [
{
noun: 'Dependency',
article: 'die Dependency, die Dependencys',
show: false
},
{
noun: 'Kenntnis',
article: 'die Kenntnis, die Kenntnisse',
show: false
},
{
noun: 'Repository',
article: 'das Repository, die Repositorys',
show: false
},
{
noun: 'Kenntnis',
article: 'die Kenntnis, die Kenntnisse',
show: false
}
];
function LanguageFlashcards() {
const [flashcards, setFlashcards] = useState(initialFlashcards);
const toggleFlashcard = (flashcard: IFlashcard) => {
flashcard.show = !flashcard.show;
setFlashcards([...flashcards, flashcard]);
}
return (
<>
<h2>Language Flashcards</h2>
<ul>
{flashcards.map((flashcard: IFlashcard) => {
return (
<>
<li><span onClick={() => toggleFlashcard(flashcard)}>{flashcard.noun}</span>
{flashcard.show && (
<>
: {flashcard.article}
</>
)}
</li>
</>
)
})}
</ul>
</>
);
}
export default LanguageFlashcards;
Your example is in fact mutating state here:
flashcard.show = !flashcard.show;
At this point, flashcard refers directly to an object in state, so altering one of its properties is a mutation.
You need a way to identify the objects in state so that you can extract one, clone it individually, and then insert it back into a cloned state array in its original position. Without changing any of your data, you could do this by passing the array position of the flashcard when you call toggleFlashcard.
{flashcards.map((flashcard: IFlashcard, i: number) => {
return (
<>
<li><span onClick={() => toggleFlashcard(i)}>{flashcard.noun}</span>
{flashcard.show && (
<>
: {flashcard.article}
</>
)}
</li>
</>
)
})}
Now the toggleFlashcard event handler should look something like this:
const toggleFlashcard = (i: number) => {
const clonedCard = {...flashcards[i]};
clonedCard.show = !clonedCard.show;
const clonedState = [...flashcards];
clonedState[i] = clonedCard;
setFlashcards(clonedState);
}
If you don't want to mutate anything, please try this solution.
const toggleFlashcard = (flashcard: IFlashcard) => {
const flashcardIndex = flashcards.findIndex(f => f === flashcard);
const newFlashcards = [...flashcards];
newFlashcards[flashcardIndex]= { ...flashcard, show: !flashcard.show };
setFlashcards(newFlashcards);
};
And this is not related to the main topic but the key attribute is missing here.
{flashcards.map((flashcard: IFlashcard, index: number) => {
...
<li key={index}><span onClick={() => toggleFlashcard(flashcard)}>{flashcard.noun}</span>
If you don't specify the key attribute, you will see React warnings.
To do what you want you could do something like this
const toggleFlashcard = (flashcardIndex: number) => {
const flashcardsDeepCopy: IFlashcard[] = JSON.parse(JSON.stringify(flashcards));
flashcardsDeepCopy[flashcardIndex].show = !flashcard.show;
setFlashcards(flashcardsDeepCopy);
}
In jsx you need to pass the index
<ul>
{flashcards.map((flashcard: IFlashcard, index) => {
return (
<>
<li><span onClick={() => toggleFlashcard(index)}>{flashcard.noun}</span>
{flashcard.show && (
<>
: {flashcard.article}
</>
)}
</li>
</>
)
})}
</ul>
Your problem is with your toggleFlashcard function you think you aren't mutating the original state but you are given that javascript passes your object by reference. If you were to just do
const toggleFlashcard = (flashcard: IFlashcard) => {
flashcard.show = !flashcard.show;
setFlashcards([...flashcards]);
}
It would work however that isn't really react practice. What you would need to do is return a brand new array without modifying the original object. youll need some sort of identifier to filter out which flash card is which. maybe appending the index of the item to the renderer or pass in something from the backend.