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)]
}
Related
I know the title is not extremely clear so maybe a simplified example would help.
I have a react component in charge of translations (the following is a super-simplified version):
const data = {
foo: {
en: 'hello',
es: 'hola'
},
bar: {
en: 'world',
es: 'mundo'
}
baz: {
en: 'the answer is {PLACEHOLDER}',
es: 'la respuesta es {PLACEHOLDER}'
}
}
const Translation = ({strKey, param, lang}) => {
const getTranslatedString = () => {
const translation = data[strKey][lang]
return (translation.indexOf('{PLACEHOLDER}') > -1 && param)
? translation.replace('{PLACEHOLDER}', param)
: translation
}
return <p>getTranslatedString()</p>
}
Translation.defaultProps = {
lang: 'en'
}
export default Translation;
I use it as follows:
<Translation strKey='baz' param={42} /> // renders as <p>the answer is 42</p>
<Translation strKey='baz' param={42} lang='es' /> // renders as <p>la respuesta es 42</p>
Now I have to modify my Translation component so that I can pass to it a callback function to manipulate the translated string. This function need to take as input some arguments set in the parent component and the string set in the Translation component itself. Here is where I am getting stuck, as I am having troubles understanding how to handle the arguments coming from two different components:
const data = {
foo: {
en: 'hello#world',
es: 'hola#mundo'
}
}
// utility string manipulation function that will be used as callback
function getStringChunk(str, index) {
return str.split('#')[index];
}
// Modified `Translation` component
const Translation = ({strKey, param, lang, callback}) => {
const getTranslatedString = () => {
const translation = data[strKey][lang]
return (translation.indexOf('{PLACEHOLDER}') > -1 && param)
? translation.replace('{PLACEHOLDER}', param)
: translation
}
return <p>{callback ? callback(getTranslatedString()) : getTranslatedString()}</p> // `callback` could be any function with any number of additional arguments
}
const ParentComponent = ({isFirst}) => {
return (
<Translation
strKey='foo'
callback={() => {
const index = isFirst ? 0 : 1;
getStringChunk(index) // the function expects a first `str` argument but that is set inside the Translation component itself
}}
/> // should render either <p>hello</p> or <p>world</p>, depending on the value of `isFirst`
);
}
I am pretty sure there is something very fundamental and elementary I am missing, something that will make me feel as a complete idiot as soon as someone points out the solution, but I am having some kind of "mind block" and I am running in circles without finding a solution.
Thanks in advance.
Maybe you could try something like this
const ParentComponent = ({isFirst}) => {
return (
<div>
<Translation
strKey='foo'
lang={'en'}
callback={(mystr) => () => {
const index = isFirst ? 0 : 1;
return getStringChunk(mystr,index)
}}
/>
</div>
);
}
we make a function that returns the callback function by taking a string.
then you could create the callback function as required inside Translation
return <p>{callback ? callback(getTranslatedString())() : getTranslatedString()}</p>
I am looking to modify a property of one of the items in the array using the updater returned from setState. The function is passed as props to the child, who then call this with their own index to change their status.
const tryMarkHabitAsComplete = (index: number) => {
let list: HabitType[] = [...habitList];
let habit = {
...list[index],
isComplete: true,
};
list[index] = habit;
setHabitList({list});
};
When running with
setHabitList(list); the array gets cleared and becomes undefined, so I am unable to use it after I call this function once.
The error that keeps appearing is:
Argument of type '{ list: HabitType[]; }' is not assignable to parameter of type 'SetStateAction<HabitType[]>'. Object literal may only specify known properties, and 'list' does not exist in type 'SetStateAction<HabitType[]>'
I am treating the state as immutable and attempting (I think) to set the state. When debugging, everytime I click I empty the array for my habitsList. Further up, I define the state as:
const [habitList, setHabitList] = useState<HabitType[]>([]);
If I try to setState in other ways, the array becomes undefined or blank and loses information. I should clarify, this is a side project written w/ react-native.
MRE Edit:
2 components in a react-native app, discarding css & irrelevant imports:
Chain.tsx
export type HabitType = {
text: string;
index: number;
isComplete: boolean;
tryMarkHabit: (index: number) => void;
};
const Chain = (props: any) => {
const [habitList, setHabitList] = useState<HabitType[]>([]);
// Set the initial state once w/ dummy values
useEffect(() => {
setHabitList([
{
text: "1",
index: 0,
isComplete: false,
tryMarkHabit: tryMarkHabitAsComplete,
},
{
text: "2",
index: 1,
isComplete: false,
tryMarkHabit: tryMarkHabitAsComplete,
}
]);
}, []);
// Only is previous item is complete, let habit be marked as complete
const tryMarkHabitAsComplete = (index: number) => {
let list: HabitType[] = [...habitList];
let habit = {
...list[index],
isComplete: true,
};
list[index] = habit;
setHabitList(list);
};
let habitKeyCount = 0;
return (
<View style={styles.container}>
{habitList.map((habit) => (
<Habit key={habitKeyCount++} {...habit} />
))}
</View>
);
};
export default Chain;
Habit.tsx
import { HabitType } from "./Chain";
const Habit = ({ text, index, isComplete, tryMarkHabit }: HabitType) => {
const [complete, setComplete] = useState<boolean>(false);
return (
<TouchableOpacity
style={complete ? styles.completeHabit : styles.uncompleteHabit}
onPress={() => tryMarkHabit(index)}
>
<Text style={styles.chainText}>{text}</Text>
</TouchableOpacity>
);
};
export default Habit;
When I press a habit, it calls tryMarkHabitAsComplete successfully, however it appears to clear the array - the array afterwards becomes undefined as far as I can tell.
Issue
If I had to guess I'd say you've a stale enclosure of your habitList state in the callback, which itself is a stale enclosure in your state.
Solution
I suggest to instead use a functional state update in your handler. A functional update allows your state to be updated from the previous state, not the state from the render cycle the update was enqueued in. It's a small, subtle, but important distinction.
I also suggest to not enclose the handler in an enclosure in state either. Just pass it to Habit as a normal callback, this way you avoid any stale enclosures.
const Chain = (props: any) => {
const [habitList, setHabitList] = useState<HabitType[]>([]);
// Set the initial state once w/ dummy values
useEffect(() => {
setHabitList([
{
text: "1",
index: 0,
isComplete: false,
},
{
text: "2",
index: 1,
isComplete: false,
}
]);
}, []);
// Only is previous item is complete, let habit be marked as complete
const tryMarkHabitAsComplete = (index: number) => {
setHabitList(list => list.map((habit, i) => i === index ? {
...habit,
isComplete: true,
} : habit);
};
let habitKeyCount = 0;
return (
<View style={styles.container}>
{habitList.map((habit) => (
<Habit
key={habitKeyCount++}
{...habit}
tryMarkHabit={tryMarkHabitAsComplete}
/>
))}
</View>
);
};
Edit for isComplete Style
Habit should just consume the passed isComplete prop to set the desired CSS style.
const Habit = ({ text, index, isComplete, tryMarkHabit }: HabitType) => {
return (
<TouchableOpacity
style={isComplete ? styles.completeHabit : styles.uncompleteHabit}
onPress={() => tryMarkHabit(index)}
>
<Text style={styles.chainText}>{text}</Text>
</TouchableOpacity>
);
};
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>
);
}
I have a functional react component. It does not use any props, it just return elements. Have I pass props to it anyway? Are there any agreements about that? Will be code below valid React code?
const HelloComponent = () => (
<div>Hi!</div>
);
No you don't. props are explicitly passed to each component. If you don't use any property from it, just don't declare it. Exactly like in your example. Consider the following
const App = () => <Child />
const Child = props => {
console.log(props) // { }
return <div>hey</div>
}
In this case props is just an empty Object and there is no need to declare the argument. If you don't need to read from it, don't even mention it
const Child = () => <div>hey</div>
It is completly valid, there is no need for props. Its the same as with any other function, if it doesnt have arguments, dont give it any. :-)
As functional react components are just javascript functions the same rules apply to them as they do for any function. You can safely omit unused arguments.
The code you've mentioned is valid.
Pattern A
// span on single line and returns by default..
const Test = () => <div>Hello</div>
Pattern B
// span on multiple lines, still returns by default a single node
const Test = () =>
<div>
Hello
</div>
Pattern C
//span on multiple lines, but grouped by a paranthesis
const Test = () => (
<div>
Hello
</div>
)
WRONG..
//tryin to group with braces..
const Test = () => {
<div>
Hello
</div>
}
Pattern D:
// group by braces, so you have to return explicitly
const Test = () => {
const greeting = 'hello'
return (
<div>
{greeting}
</div>
)
}
Note:
it's not possible to return multiple nodes in react. all the nodes must have a single parent as it's root. why {} braces fails is because, it is used to group multiple nodes or code fragments.
//container
//destructure props from other end
render() {
const obj = {
name: 'john',
isAdmin: true,
}
return <Child {..{ obj }} {...this.props} />
}
// child: destructure from here
const Child = ({ obj, ...props }) => {
const { value1-in-props, value2-in-props } = props
}
//container
//destructure whilst sending
render() {
const obj = {
name: 'john',
isAdmin: true,
}
return <Child {..obj} {...this.props} />
}
// child: destructure
const Child = ({ name, isAdmin, ...props }) => {}
I have two types of chat messages in my chat application
export const UserType = "USER_MESSAGE";
export const SystemType = "SYSTEM_MESSAGE";
export type ChatType = typeof UserType | typeof SystemType;
I use a switch case to determine which type of message I am processing and depending on type choose to render its content.
This is where I run into trouble because depending on the type of the message, it will contain different content object structures.
const ChatMessages = ({ messages, style }: Input) => {
// FLOW ERROR: Property `text`: Property not found in object type
const renderUserMessage = (content: UserContent, i: number) => {
return <p key={`message-${i}`}>
<b>{content.userName}: </b>{content.text}
</p>;
};
// FLOW ERROR: Property `info`: Property not found in object type
const renderSystemMessage = (content: SystemContent, i: number) => {
return <p key={`message-${i}`}>
{content.info}
</p>;
};
return (
<div style={style}>
{messages.map((message, i) => {
switch (message.type) {
case UserType:
return renderUserMessage(message.content, i);
case SystemType:
return renderSystemMessage(message.content, i);
default:
}
})}
</div>
);
};
The rest of my types look like this
export type UserContent = {
userName: string,
text: string,
};
export type SystemContent = {
info: string,
};
export type ChatContent =
UserContent |
SystemContent;
export type MessageType = {
type: ChatType,
content: ChatContent,
};
Changing the type definition from UserContent and SystemContent to a generic Object solves the problem, but that's not as strict as I'd like it to be.
This problem is known issue. There is a solution (or work around) to simply reassign message.type to some variable before Switch statment, or to swap the switch with an appropriate if chain.
So in your case this should work:
return (
<div style={style}>
{messages.map((message, i) => {
let messageType = message.type;
switch (messageType) {
case UserType:
return renderUserMessage(message.content, i);
case SystemType:
return renderSystemMessage(message.content, i);
default:
}
})}
</div>
);