Reputation: 4050
I'm getting content in React like this:
const data = [
{
header: 'my header1',
copy: [
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'Etiam et risus quam. Proin a mollis dolor.'
]
},
{
header: 'another header',
copy: [
{
tag: 'ul',
content: [
'something',
'else',
'here'
]
}
]
}
]
Now the goal is to have this kind of markup:
<h2>my header1</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<p>Etiam et risus quam. Proin a mollis dolor.</p>
<h2>another header</h2>
<ul>
<li>something</li>
<li>else</li>
<li>here</li>
</ul>
Now I'm new to react in terms of how to properly nest these kinds of things especially with handling the different things in the copy
key in JSX. I've done things like Vue or Svelte before where you can do something like this:
Fully functioning svelte code: 16 lines
{#each data as dataRow}
<h2>{dataRow.header}</h2>
{#each dataRow.copy as copyRow}
{#if typeof copyRow === 'string'}
<p>{copyRow}</p>
{:else if copyRow.tag === 'ul'}
<ul>
{#each copyRow.content as listItem}
<li>{listItem}</li>
{/each}
</ul>
{:else}
<p>OH NO ERROR</p>
{/if}
{/each}
{/each}
I've managed simply JSX so far:
{data.map(row => (
<h2>{row.header}</h2>
))}
but when I try to do another {row.copy.map(copyRow => (<p>stuff here</p>))}
inside of that first map statement like this:
{services.map(row => (
<h2>{row.header}</h2>
{row.copy.map(rowCopy => (
<p>hi</p>
))}
))}
I just get a
Parsing error: Unexpected token, expected ","
which is hard to debug, considering I didn't have a single comma in the code there.
Just looking for some info on how to do nested pieces like that the proper React way.
Upvotes: 0
Views: 163
Reputation: 166
Something like this should work, if you create a variable by mapping over your data and convert it to the appropriate JSX above the return statement, and then just return that variable below.
const rows = data.map((row)=>{
<div>
<h2>{row.header}</h2>
{typeof row.copy[0] === 'string' && <p>{row.copy[0]}</p>}
{row.copy[0].tag === 'ul' &&
<ul>
row.copy[0].content.map(li => <li>{li}</li>)
</ul>}
</div>
});
return (
{rows}
);
Upvotes: 0
Reputation: 196
I think you could use some composition here, where you would break it down into smaller components that you'll compose to structure and complete the view you are trying to achieve.
Here's just an suggested approach you could use, but may still need improvements.
Note on my use of memo, I'm assuming you have huge stuff to render, that may provoke component rerender and stuff. https://reactjs.org/docs/react-api.html#reactmemo
const HeaderItem = memo(({ headerText }) => {
return <h2>{headerText ? headerText : 'Oh no! missing header text'}</h2>
});
-------
const ContentParagraphs = memo(({ paragraph }) => {
return <p>{paragraph ? paragraph : ''}</p>
});
------
const ContentList = memo(({ listArrayData=[] }) => {
return (
<ul>
{
listArrayData?.map((itemData, index) => {
// Note on keys:
/**
* Per react docs
* React does not recommend using indexes for keys if the order of items may change.
* This can negatively impact performance and may cause issues with component state.
*
* The best way to pick a key is to use a string that uniquely identifies a list item among its siblings
*
* https://reactjs.org/docs/lists-and-keys.html
*/
return <li key={itemData.id}>{itemData}</li>;
})
}
</ul>
);
});
----
const DataItemDisplay = memo(({ myComplexDataArr = [] }) => {
return (
<>
{
myComplexDataArr?.map((datum, index) => {
// going to use Fragment since I don't know if your requirements
// needs just the header + paragraph | ul
// focus is mostly on separation of concerns to keep things neat and tidy
// NOTE: SEE my comment on using indexes as key in ContentList components above
return (
<Fragment key={index}>
<HeaderItem headerText={datum?.header} />
{
datum?.copy?.map((copyItem, index) => {
if (copyItem?.tag === 'ul' && copyItem?.content?.length > 0) {
return <ContentList key={index} listArrayData={copyItem?.content} />;
}
return <ContentParagraphs key={index} paragraph={copyItem} />;
})
}
</Fragment>
);
})
}
</>
)
});
Upvotes: 0
Reputation: 4112
I would recommend creating a bunch of components, mainly to keep the code clean (otherwise you land in indentation hell real quick).
I've coded up a solution using TypeScript:
type StringCopy = string;
interface ComplicatedCopy {
tag: keyof HTMLElementTagNameMap,
content: string[];
}
type Copy = StringCopy | ComplicatedCopy;
interface Post {
header: string;
copy: Copy[];
}
interface PostRendererProps {
data: Post[];
}
function PostRenderer({data}: PostRendererProps): ReactElement {
return (
<>
{data.map((post, index) => (
<React.Fragment key={index}>
<h2>{post.header}</h2>
{post.copy.map((copy, index) => <CopyRenderer key={index} copy={copy} />)}
</React.Fragment>
))}
</>
)
}
interface CopyRendererProps {
copy: Copy;
}
function CopyRenderer({copy}: CopyRendererProps): ReactElement {
if (typeof copy === "string") return <StringCopyRenderer copy={copy} />
return <ComplicatedCopyRenderer copy={copy} />
}
interface StringCopyRendererProps {
copy: StringCopy;
}
function StringCopyRenderer({copy}: StringCopyRendererProps): ReactElement {
return <p>{copy}</p>
}
interface ComplicatedCopyRendererProps {
copy: ComplicatedCopy;
}
function ComplicatedCopyRenderer({copy}: ComplicatedCopyRendererProps): ReactElement {
// If you have more than the tag "ul" here would be a good place to create
// even more components :)
if (copy.tag === "ul") {
return (
<ul>
{copy.content.map((value, index) => <li key={index}>{value}</li>)}
</ul>
)
}
throw new Error(`copy of type "${copy.tag}" not implemented`)
}
<></>
or React.Fragment
You said you wrote some Vue or swelt. I would generally recommend creating many components. As you can see, well it's all in one textbox... but still, the functions are much cleaner than if you had put a bunch of if
s and others into the code, adding indents every time.
Technically the PostRenderer
is a PostListRenderer
... but I'm sure you can figure that out :).
You see that I added Renderer
to many (all :D) of the components. That is mainly because the typescript interface for (for example) Post
is already named "Post", so I couldn't name the function "Post". If you don't use Typescript feel free to just call the component "Post" (way too many posts in this sentence). Or even better follow some sort of naming convention.
I hope you're not confused with the TypeScript, it's just JavaScript with type validation... way easier to work with.
Upvotes: 1
Reputation: 300
first of all the way you structured that json is not very frontend friendly since the structure of a single key ("copy" here) should always be the same. it can't be an array of strings and an object later. if it has to be this way you have to figure out a way to detect it's type then return the proper element.
secondly, in react, each function should always return only one element. you have to wrap them all in a single div or react.fragment. plus you have syntax error in that map function. you can omit the "return" keyword when you only return in a single line.
you can map through that json like this:
data.map((item,idx)=>{
return (
<div key={idx}>
<h4>{item.header}</h4>
{item.copy.map((element,index)=>{
if(typeof element === "string"){
return <p key={index}>{element}</p>
}
else
{
return <ul>
{element.content.map(listItem)=> <li key={index}>{listItem}</li>}
</ul>
}
})}
</div>
)
})
Upvotes: 0