Building a reusable component to map through different sets of data React - javascript

Just like the title says I'm passing down pokemon data and rickandmorty data. I also happen to be using the tailwind select menu for react thats pretty long. Is there a better way to do it than conditionally map through the data? I know I can do this
{pokemons ? (
{pokemons?.map((pokemon, idx) => (
**30 line long code for the select menu**
))}
) : (
{rickAndMorty?.map((character, idx) => (
**Another 30 long line code for the select menu**
))}
)}
Is this the only way to do it or is there a cleaner way? Any help would be greatly appreciated. Thanks!

I suggest to try and separate any duplicated code out into some generic component, like:
const GenericSelectItem = (props)=>{
return (<>{/* props.itemValues */}</>);
};
const GenericSelectList = (props)=>{
const { selectItems } = props;
return (<SelectList>
{ selectItems.map( selectItem => <GenericSelectItem selectItem={ selectItem } /> ) }
</SelectList>);
};
const Example = (props)=>{
const itemsToDisplay = pokemons || rickAndMorty;
return (<>
{ !itemsToDisplay ? null : <GenericSelectList selectItems={ itemsToDisplay } /> }
</>);
};
In case the SelectItems are very different, add specific components, like:
const PokemonItem = (props)=>{
return (<GenericSelectItem>{/* pokemon specific variations */}</GenericSelectItem>);
};
const RickAndMortyItem = (props)=>{
return (<GenericSelectItem>{/* rickAndMorty specific variations */}</GenericSelectItem>);
};

Related

ReactJS - Can i save specific rendered mapped data, to a variable?

I have data mapped in a component like this:
import React from "react";
import { useState } from "react";
import { useEffect } from "react";
import { get } from "lodash";
const Products = ({ data }) => {
return (
data.map((item, index) = > (
<div id={index}>
<img src={item.img} /> <br />
{item.name} <br />
{get(moreData, `[${item.name.toLowerCase()}].info[0]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[1]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[2]`)}
</div>
I want to be able to store this data:
{item.name}
{get(moreData, `[${item.name.toLowerCase()}].info[0]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[1]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[2]`)}
in a string, like string = {item.name},{moreData.item.name.toLowerCase().info[0]},...//etc
However you cannot declare variables inside of a component (as far as i know, still new to this).
I've tried .concat() - after each line and .push() with array instead of string:
{item.name} <br />
{dataString.concat(item.name)}
{dataArr.push(item.name)}
{get(moreData, `[${item.name.toLowerCase()}].info[0]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[1]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[2]`)}
I was going to use DOM, but i've been told it's bad practice to use DOM in react.
I've also tried using state in the same way:
const [dataString, setDataString] = useState("");
...
{item.name}
{setDataString((dataString += item.name))}
But nothing seems to work as intended for me, and i'm out of ideas.
Edit:
I want to be able to copy the 'string/text' to clipboard eventually. So it can be imported to another site. Their required format is Item1, item1-info1, item1-info2, item1-info3, item2, item2-info1, item2-info2, item2-info3...etc
here is an example of how you can use the data object outside of the mapped objects.
also an example how to convert the data to an string in the format required.
because I don't know the structure of the data object of yours I just created an one, take this code and change it to the data structure
const data = [
{
name: "name1",
info1: "info1-1",
info2: "info1-2",
info3: "info1-3",
},
{
name: "name2",
info1: "info2-1",
info2: "info2-2",
info3: "info2-3",
},
{
name: "name3",
info1: "info3-1",
info2: "info3-2",
info3: "info3-3",
},
];
change this function to one that fits your needs.
const getStringOfData = (data) => {
let stringArray = [];
data.map((item) => {
stringArray.push(item.name);
stringArray.push(item.info1);
stringArray.push(item.info2);
stringArray.push(item.info3);
});
let stringResult = stringArray.join(",");
return stringResult;
};
useEffect(() => {
console.log("onMounte with useEffect");
let stringResult = getStringOfData(data)
console.log(stringResult);
}, []);
you can also call this function on onClick function depend in your requirement , the data object is available almost everywhere in your component
Continuing our discussion from the comments, it looks like there are two different things you are trying to do here:
display the UI to the user in a certain way
to be able to copy the information in a specific stringified format
I would advice to split these two behaviors, because they are totally unrelated to each other in terms of logic.
Start by aligning the information in your components how you need it.
const Product = ({ item, index }) => {
// do whatever you need to do with the information
// to display it correctly
const identifier = name.toLowerCase()
const infos = (moreData[identifier] && moreData[identifier].info) || {}
return (
<div>
{ info[0] && <p> info[0] </p>}
{ info[1] && <p> info[1] </p>}
{ info[1] && <p> info[2] </p>}
</div>
)
}
const Products = ({ data }) => {
const onClick = useCallback(
async () => {
// parse data and copy it using a separate
// util method.
const dataToString = /* actual implementation of the formatting to a string representation */
copyTextToClipBoard(dataToString)
},
[data]
)
return (
<div>
<button onClick={onClick} />
<div>
data.map((item, index) => (
<Product item={item} index={index} key={item.id} />
))
</div>
</div>
)
};
Now this would be your UI, the copyToClipboard method, is a bit of a hack, and looks like so:
const copyTextToClipBoard = (text) => {
const element = document.createElement('textarea');
element.style = { display: 'none' }; // eslint-disable-line
element.value = text;
document.body.appendChild(element);
element.select();
document.execCommand('copy');
document.body.removeChild(element);
};

How to efficiently pass props in nested components?

Say there are nested components, where data is passed from top to root, but only root component needs the data, how to deal with this more efficiently? Check the following example:
const Myform = () => {
return (
<Section title={'Nested component'} />
)
}
const Section = ({title}) => {
return (
<Card title={title} />
)
}
const Card = ({title}) => {
return (
<Title title={title} />
)
}
const Title = ({title}) => {
return (
<h2>{title}</h2>
)
}
Myform passes title data to Section, then passes to Card, then passes to root component Title, where data is only needed in Title component. The 2 middle components only pass data. I can see two problems:
Each middle component needs to accept title, but has no use of title at all;
If more middle components are needed, each of them needs to repeat the same process.
In fact only Myform and Title need to know the data. Is there a more efficient way to deal with this kind of scenario?
You can create the Title component at the top and pass that down instead of the props. This is what Facebook recommends here in their docs on Composition vs Inheritance
EG:
const Myform = () => {
return (
<Section Title={<Title title='Nested component' />} />
)
}
const Section = ({Title}) => {
return (
<Card Title={Title} />
)
}
const Card = ({Title}) => {
return (
<Title />
)
}
const Title = ({title}) => {
return (
<h2>{title}</h2>
)
}
In this example with so many levels it doesn't work that great - in that case the Context API might be better

React props renders are step late

I have the following which is changing the values, but it's always one step behind. For example, when you click on the "paid: false" for a customer, it changes to true but the app doesn't rerender and you have to update another thing on the app in order to see the change. Is there a simple way to fix this in React? I don't know how to research what I'm looking for so a point in the right direction will help a lot.
const [receipt, setReceipt] = useState(receiptData);
// const [currentReceipt, setCurrentReceipt] = useState({});
// For some reason I do not know yet, everything is working but this and onSubmitFromApp are one step behind.
const handlePaid = (index) => {
for (let receiptPaid in receiptData) {
if (receiptPaid === index) {
receiptPaid.paid = !receiptPaid.paid;
console.log(receiptPaid);
}
}
setReceipt(receiptData);
}
Link to full code: https://codesandbox.io/s/korilla-receipts-starter-forked-01xz0?file=/src/App.js:206-675
Your approach is kind of weird and involves mutations. You're better off doing something like this (I removed the form stuff cos that's unrelated):
// App.js
export default function App() {
const [receipts, setReceipts] = useState(receiptData);
// Map over the current state, not the imported array
const handlePaid = (id) => {
setReceipts(
receipts.map((receipt) =>
receipt.id === id ? { ...receipt, paid: !receipt.paid } : receipt
)
);
};
return (
<div className="App">
<Receipts receiptsArr={receipts} handlePaid={handlePaid} />
</div>
);
}
// Receipts.js
const Receipts = (props) => {
const receiptMap = props.receiptsArr.map((receipt, index) => {
return (
<Receipt
...
handlePaid={() => props.handlePaid(receipt.id)}
/>
);
});
return <div className="container">{receiptMap}</div>;
};
// Receipt.js
const Receipt = (props) => {
return (
...
<span onClick={props.handlePaid} >Paid: {props.paid ? 'true' : 'false'}</span>
</div>
)
}
You can check out the sandbox here

Change state of individual div (that is inside a map) instead of changing the state of all div's inside the map

I have a component that is checking if some state is true or false. I show a <p> tag if true and hide a <h3>. I am pulling the data from a gaphQL query so there is a map method and there are three <Card>'s now if I click the card and run my showFull function it shows the p tags on all the elements, instead I just want to isolate the specific one it is clicking on.
Here is my component
<Testimonials className="testimonaials">
{data.allDatoCmsTestimonial.edges.map(({ node: testimonial }) => (
<Card onClick={showFull} background={testimonial.testimonialImage.url}>
{testimonialFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
))}
</Testimonials>
Here is my state and my function
const [testimonialFull, setTestimonialFull] = useState(false)
const showFull = () => {
setTestimonialFull(true)
}
Attempting Alexander's answer. The issue I am having now is Cannot read property 'testimonialImage' of undefined
Here is the component
const IndexPage = ({ data }) => {
const TestimonialCard = ({testimonial})=>{
const [showFull, setShowFull] = useState(false)
const handleClick = useCallback(()=>{
setShowFull(true)
//setShowFull(s=>!s)//If you want toggle behaviour
},[])
return <Card onClick={handleClick} background={testimonial.testimonialImage.url}>
{showFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
}
return (
...
Here is where I invoke it in the map function
...
return (
... (bunch of other jsx/html)
<Testimonials className="testimonaials">
{data.allDatoCmsTestimonial.edges.map(({ node: testimonial }) => (
<TestimonialCard/>
))}
</Testimonials>
...
Wrap the cards in a custom component
const TestimonialCard = ({testimonial})=>{
const [showFull, setShowFull] = useState(false)
const handleClick = useCallback(()=>{
setShowFull(true)
//setShowFull(s=>!s)//If you want toggle behaviour
},[])
return <Card onClick={handleClick} background={testimonial.testimonialImage.url}>
{showFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
}

How can I use map to return a nested object?

What I want to do, using map, is pretty plain.
I want to call this:
<Striper size={3} text={"Hey everybody!"}/>
To get this:
<>
<Stripe>
<Stripe>
<Stripe>
Hey everybody!
</Stripe>
</Stripe>
</Stripe>
</>
I tried this way, but it fails:
const Striper = (props) => {
const contentTop=props.sizer.map((item)=> <Stripe>)
const contentBottom=props.sizer.map((item)=> </Stripe>)
return (
<div>
{contentTop}
{contentBottom}
</div>
)
}
Basically only this works (which isn't what I want):
const contentTop = props.sizer.map((item)=> <Stripe></Stripe>)
How could I get what I want?
The solution ended up being really simple (thank you, Emile): use .reduce.
As it says in the documentation about reduce, it's really useful when you need only one thing returned. And that's what I needed.
As I said in a comment:
What I want to return from <App size={2} text="Hello"/> is really
<Stripe><Stripe>Hello</Stripe></Stripe>, but because I have to
return a whole object, the closest I can come with map is
<Stripe>Hello</Stripe><Stripe>Hello</Stripe>.
So instead, use reduce.
This is the solution, verified to work. (Note: I'm being somewhat handwavey about size={3} when it's actually an array, because I know how to do that stuff, it isn't my question but you can see my implementation in the answer below).
const Striper = (props) => {
const content = props.sizer.reduce(
(total, currentValue) => <Stripe color={props.colors}>{total}</Stripe>
)
return (
<div>
{content}
</div>
)
}
And as it's actually called:
const arr = Array(6).fill("this is my text!");
return (
<div>
<Striper sizer={arr} colors={colours}/>
</div>
);
I guess you can achieve something like with this approach:
import React from 'react';
import { render } from 'react-dom';
const Stripe = () => <span> – </span>
const App = ({ size, text }) => {
const arrFromNum = Array.from(Array(size), (x, i) => i + 1)
return (
<React.Fragment>
{arrFromNum.map(x => <Stripe />)}
{text}
{arrFromNum.map(x => <Stripe />)}
</React.Fragment>
)
}
render(<App size={4} text="Hello" />, document.getElementById('root'));
Does this answer your question?
Here's one possible way of many different possibilities based on your scenario:
<Striper size={3} text={"Hey everybody!"}/>
export default ({ size, text }) => <><Stripe size={size} /> {text} <Stripe size={size} /></>;
export default ({ size }) => <>{ Array(size).fill().map(_ => <h3>Stripe!</h3>) }</>;
https://stackblitz.com/edit/react-tnqv2k

Categories

Resources