How to iterate over components inside components in React - javascript

I have my products components which simply display products, price and description
const Product = (props) =>{
return(
<div>
<p>Price: {props.price} </p>
<p>Name: {props.name}</p>
<p>Description: {props.desc}</p>
</div>
)
}
Which is rendered by the App component which loops thru the data in productsData and renders a product component for each index in the array.
class App extends React.Component {
render() {
const products = productsData.map(product => {
return <Product key={product.id} price={product.price}
name={product.name} desc={product.description} />
})
return (
<div>
{products}
</div>
);
}
}
However, for the sake of learning purposes, I am trying to figure out how I am able to loop thru this array of products components (rendered in App) to only display, for example, prices that are greater than 10 or descriptions that are longer than 10 characters, for example.
productsData looks something like this
const productsData = [
{
id: "1",
name: "Pencil",
price: 1,
description: "Perfect for those who can't remember things! 5/5 Highly recommend."
},
I am assuming I need to use the .filter method inside the products component, but I can't seem to figure out where. I keep getting errors or undefined.
Could someone clear this up, how one would iterate thru components nested inside other components?

Try this:
const products = productsData.filter(product => (
product.price > 10 || product.description.length > 10
)).map(p => (
<Product key={p.id} price={p.price}
name={p.name} desc={p.description}
/>
))
Chaining methods filter with map allows you get the desired result.
Read more here about filter: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

You can add a condition in .map, if condition matches then return the Product else return null.
const products = productsData.map((product) => {
if (product.price > 10 || product.description.length > 10)
return (
<Product
key={product.id}
price={product.price}
name={product.name}
desc={product.description}
/>
);
return null;
});

Related

Increment number for every instance of an element in React

I hope I phrased this question clearly. I have a small recipe app, for the recipe method I want to dynamically add Step 1, Step 2, Step 3 etc. for each step that is passed through via props.
The recipe's steps are passed through as an array of objects:
recipeMethod: Array(2)
0: {step_instructions: 'Boil Water'}
1: {step_instructions: 'Heat Oil'}
length: 2
I am trying to get this array to display as
Step 1
Boil Water
Step 2
Heat Oil
And additionally steps would be added for any further recipe method objects. Currently I can just get the step_instructions to display but cannot get the dynamically incrementing steps (that should start at 1)
Here is the relevant code:
import './MethodStep.css'
import React from 'react'
let methodArray
function mapMethod() {
return methodArray.map((item, idx) => (
<React.Fragment key={idx}>
<div className="method-step small-header"></div>
<div className="method-text">{item.step_instructions}</div>
</React.Fragment>
))
}
function MethodStep(props) {
methodArray = props.recipeMethod || []
return <div className="recipe-method-container">{mapMethod()}</div>
}
export default MethodStep
as #louys mentioned above you can easily achieve that using
Steps {idx + 1 }
Above will print the each index of methodArray after adding 1.
I also noticed you are using Index as key. This is wrong practice as key should be always unique. You can append some string with it to make it unique.
like below:
<React.Fragment key={step-${idx}}>
Here you go, you'll just have to change the initialisation of methodArray to equal props.recipeMethod.
import React from "react";
function mapMethod(methodArray) {
return methodArray.map((item, idx) => (
<React.Fragment key={idx}>
<div className="method-step small-header">Step {idx + 1}</div>
<div className="method-text">{item.step_instructions}</div>
</React.Fragment>
));
}
function MethodStep(props) {
const methodArray = [
{ step_instructions: "Boil Water" },
{ step_instructions: "Heat Oil" },
]; // this is props.recipeMethod;
return (
<div className="recipe-method-container">{mapMethod(methodArray)}</div>
);
}
export default MethodStep;
You need to bind the array to the component state. You could build your own hook handling this for you.
Here's a basic exemple:
function useRecipe(initialRecipe) {
const [recipe, setRecipe] = useState(initialRecipe)
const addStep = step => {
recipe.push(step)
setRecipe(recipe)
}
return [recipe, addStep]
}
function mapMethod(recipe) {
return recipe.map((item, idx) => (
<React.Fragment key={idx}>
<div className="method-step small-header"></div>
<div className="method-text">{item.step_instructions}</div>
</React.Fragment>
))
}
function MethodStep(props) {
const [recipe] = useRecipe(methodArray)
return <div className="recipe-method-container">{mapMethod(recipe)}</div>
}

Insert a React component into an array

I have a query about the best way to go about this. So i have a stateless component called <Banner/> which just displays an image and some text.
I then have an array of objects which generates a list of features on the homepage of my site. There's roughly 15 objects in this listGroups array so it renders 15 <Group/> components one after the other. The code for this is below
{listGroups.map((group, i) => (group?.assets?.length > 0) && (
<Group key={group.id} {...group} showTitle={i !== 0} large={i === 0} />
))}
I would like to insert my <Banner/> component into this list in a specific position, ideally after the first <Group/> is rendered. I can use array.splice and add the component into a specific position into the array but it isn't rendered on the page so I'm obviously missing something here.
The end result would be something like this
<Group/>
<Banner/>
<Group/>
<Group/>
<Group/>
and so on
Any help would be appreciated.
You can create an array of JSX.Elements e.g.
const arr: JSX.Elements[] = [];
listGroups.forEach((group, i) => {
if(i == 1) arr.push(<Banner/>);
// add your Groups
})
and you can render the arr.
You have various ways to achieve this.
In React you can render array of elements inside JSX, just like any other variable. If you wish to render components based some data that comes from api you could as well map your data and pass data to component. "key" property is required in both cases so React knows when structure changes.
Live example on CodeSandbox https://codesandbox.io/s/bold-grass-p90q9
const List = [
<MyComponent key="one" text="im 1st!" />,
<MyComponent key="two" text="im 2nd" />,
<MyComponent key="three" text="im 3rd" />
];
const data = [
{ text: "1st string" },
{ text: "2st string" },
{ text: "3st string" }
];
export default function App() {
return (
<div className="App">
<h3>Render array</h3>
{List}
<h3>Map from data</h3>
{data.map(({ text }) => (
<MyComponent key={text} text={text} />
))}
</div>
);
}
Check this and let me know if this is what you want
Sandbox
https://codesandbox.io/s/crazy-lalande-mx5oz
//Have taken limit as 10 for demo
export default function App() {
function print() {
let group = [];
for (let i = 0; i <= 10; i++) {
group.push(<Group key={i} />);
}
group.splice(1, 0, <Banner/>);// adding Banner at 1st index
return group;
}
return <div>{print()}</div>;
}

How to filter api data in React

So i'm trying to filter my star wars api data through a searchbox and got this so far :
class Card extends Component {
constructor(){
super()
this.state = {
jedi: [],
searchfield: ''
}
}
// Loop through API
componentDidMount(){
fetch('https://swapi.co/api/people/1')
.then(response => { return response.json()})
.then(people => this.setState({jedi:people}))
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value})
console.log(event.target.value)
}
render() {
const {jedi, searchfield} = this.state;
const filteredCharacters = jedi.filter(jedi => {
return jedi.name.toLowerCase().includes(searchfield.toLowerCase());
})
}
}
This is my SearchBox component
import React from 'react';
const SearchBox = ({searchfield, searchChange})=> {
return (
<div className= 'searchbox1'>
<input className = 'searchbox2'
type = 'search'
placeholder = 'search character'
onChange = {searchChange}
/>
</div>
)
}
export {SearchBox};
This is the render in the main app component
render() {
return (
<div className="App">
<header className="App-header">
<img src={lightsaber} className="App-logo" alt="logo"/>
<h1 className="App-title">
Star Wars Character App w/API
</h1>
</header>
<SearchBox searchChange= {this.onSearchChange} />
<div className = 'allcards'>
<Card jedi = {this.filteredCharacters}/>
</div>
</div>
);
}
It keeps giving me the error "jedi.filter is not a function'. Initially I figured since filter only works on Arrays and my data were strings, that i'd use jedi.split('').filter. But that didnt seem to work as I just got "jedi.split isnt a function". What's going on?
After running the code this link 'https://swapi.co/api/people/1' returns a single object (Luke Skywalker). You can't use .filter() on an object it is meant for arrays of data.
{name: "Luke Skywalker", height: "172", mass: "77", hair_color: "blond", skin_color: "fair", …}
When I change the url 'https://swapi.co/api/people/' and remove the 1 I receive an object with another array of objects inside results.
I am assuming you want to search through a list of Jedis. If you want the list returned in the API you have to dig a step further into this object inside results. So you need to refactor your fetch as follows:
.then(people => this.setState({ jedi:people.results }))
This returns 10 objects inside of an array.
Once you have an array of these objects you can use the lodash library to filter through. npm i lodash and require it in as import _ from 'lodash'.
Then in your render right after your destructure you can run the following filter.
const filtered = _.filter(jedi, (item) => {
return item.name.indexOf(searchfield) > -1
})
console.log(filtered)
I am assuming after this you will be returning the list of filtered Jedis to the UI
I hardcoded 'Luke' in my search field state and it returned 1 Jedi successfully

How do I iterate over an array with a map function, infinitely?

I am building a React JS application.
So, I want to print something over and over from an array, but it has only two elements. I am using a custom package called 'Typist' that enables me to give a 'Typing' kind of animation with whatever I type.
I am basically trying to type 'Hi', erase it and then type 'Ola' and then erase it and then start with 'Hi' again and keep repeating this pattern.
Here's what I have right now:
let greetings=["Hi","Ola"];
render() {
return(
<div className={"TypistExample-header"} >
<Typist className={"TypistExample"}>
<Typist.Delay ms={1000} />
{
greetings.map(i =>{
return <li><h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</li>
})
}
</Typist>
P.S I did find a way to do it a few times,still not infinite, by doing this:
let greetings=["Hi","Ola"];
var actualTyping= greetings.map(i =>{
return <li><h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</li>
});
var rows=[];
for(var i=0;i<10;i++){
rows.push(actualTyping)
}
return(
<div className={"TypistExample-header"} >
<Typist className={"TypistExample"}>
<Typist.Delay ms={1000} />
{rows}
</Typist>
</div>
);
You can use Typist's onTypingDone property to restart the animation. Pass the text array via state to Typist. When the typing is done, clear the state, which will remove the rendered Typist, then restore the original text, and it will be typed again (sandbox).
Note: setState is asynchronous, and it batches updates together or defers them to a later time. In this case we want to clear the text (set null), and only after the view is updated repopulate the text. To do so, we can use the 2nd param of setState, a callback that is fired only after the update (null) has been applied.
const greetings = ["Hi", "Ola"];
class ContType extends React.Component {
constructor(props) {
super(props);
this.state = {
text: props.text
};
}
onTypingDone = () => {
this.setState(
{
text: null
},
() =>
// run this callback after the state updates
this.setState({
text: this.props.text
})
);
};
render() {
const { text } = this.state;
return (
text && (
<Typist onTypingDone={this.onTypingDone}>
<Typist.Delay ms={1000} />
{text.map((i, index) => {
return (
<div key={index}>
<h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</div>
);
})}
</Typist>
)
);
}
}
render(<ContType text={greetings} />, document.getElementById("root"));
Better and simple solution would be creating a constant integer array of your choice and then mapping carried out using the value specified for integer.
const a = [1...10000]
let greetings = ["hi", "hello"]
render(){
return(
a.map( i => {
<h1>greeting[0] - greeting[1]</h1>
})
)
}
And always, keep in mind that infinite loop cause react engine to break down. Good practice is to specify an integer value for mapping for such cases.

Filtering array elements in React/Redux

I have a React application which filters users and posts. My navbar can link to either users, posts.
Clicking on posts brings up a list of every post made, by all users (with some extra meta information for each one).
Here is my Posts component:
export default class Posts extends Component {
render() {
return (
<div>
{this.props.posts.map( post =>
<Post {...this.props} post={post} key={post.id} />
)}
</div>
)
}
}
On the users page, each user has a sublink posts. What I want is to be able to click on posts for that user and it to take you to a posts page (like above), but only list the posts for that user.
The posts array looks a bit like this:
[
{ id: 1,
userId: 1
},
{ id: 2,
userId: 1
},
{ id: 3,
userId: 2
},
{ id: 4,
userId: 3
},
{ id: 5,
userId: 3
}
]
Say I clicked on the user with userId of 3, and only wanted to display his/her posts. What is the best way to do this?
I know I can use react-router to pass the userId to the URL, and then pull that back into the component, but I'm not sure how to use this once the Posts component renders. Ultimately I want it to render the posts array filtered to the userId route param, else render all posts. Is this right approach?
Any help appreciated.
UPDATE:
Ok, I know this is not an elegant solution, but I want to start off with a solution, then I will 'trim it down', so to speak.
There's a bug in this code which I cannot figure out:
render() {
const {userId} = this.props.params
if(userId) {
const postsForUser = posts.filter( post => post.userId === userId )
return (
<div>
{postsForUser.map( post =>
<Post {...this.props} post={post} key={post.id} />
)}
</div>
)
}
else {
return (
<div>
{this.props.posts.map( post =>
<Post {...this.props} post={post} key={post.id} />
)}
</div>
)
}
} // end render()
When I log userId, it correctly logs the user id that I have just clicked on, so my this.props.params.userId variable is working correctly.
If I define postsForUser with a hardcoded integer, e.g.
const postsForUser = posts.filter( post => post.userId === 3 )
It returns the filtered array correctly.
So at this point it seems everything is working fine, except when I swap out the hardcoded id for the variable, postsForUser returns as an empty array.
Where am I going wrong?
UPDATE 2 (Fix):
For anyone facing this problem, I incorrectly returned the id from this.props.params.userId as a string, so instead I had to define it using parseInt() like this:
const userId = parseInt(this.props.params.userId)
You can simply use Array's filter method:
render(){
return (
<div>
{
if(this.params.userId) {
const postsForUser = posts.filter(post => post.userId == this.params.userId);
//renderPostList(postsForUser)
} else {
//renderPostList(posts)
}
}
</div>
)
}

Categories

Resources