Increment number for every instance of an element in React - javascript

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>
}

Related

React how to render only parts of an array?

I am still a bit new to react and how it works. I have a projects.js file with a list of objects that look like this:
id: 0,
name: "Placeholder Project 1",
description: "Project description",
image: "./assets/images/placeholder-01.png"
There are 6 objects in this array. I am trying to render only 3 project objects at a time, and then include a button that will "load more" projects later. However, I am having trouble with just the rendering part. My component looks like this:
import React, { Component } from "react";
import NavTab from "./NavTab";
import { Card } from "react-bootstrap";
import { PROJECTS } from "../shared/projects";
function RenderProject({projects, projectsDisplayArray}) {
const tempArray = projectsDisplayArray;
return(
<div className="row">
{
projects.map(project => {
tempArray.indexOf(project) > -1 ? console.log('in array already') : tempArray.push(project)
console.log(tempArray.length)
if (tempArray.length >= 3){
console.log('in the if')
return (
<Card key={project.id}>
<Card.Img variant="top" src={project.image} />
<Card.Body>
<Card.Title>{project.name}</Card.Title>
<Card.Text>
{project.description}
</Card.Text>
</Card.Body>
<button className="btn align-self-center">Go somewhere</button>
</Card>
)
}
else {
return(<div>Else return div</div>)
}
})
}
</div>
)
}
export default class Projects extends Component {
constructor(props){
super(props);
this.state = {
projectsPerScreen: 3,
currentPage: 0,
projects: PROJECTS,
projectsDisplayArray: []
}
}
modifyProjectsDisplayArray = props => {
this.setState({projectsDisplayArray: [...this.state.projectsDisplayArray, props]})
}
render() {
let i = 0;
return(
<React.Fragment>
<NavTab/>
<div className="projects">
<div className="container">
<button type="button" className="btn">Click</button>
<h1>Projects: </h1>
<RenderProject projects={this.state.projects} projectsDisplayArray={this.state.projectsDisplayArray} />
<button type="button" className="btn" onClick={() => console.log(this.state.projectsDisplayArray)}>console log</button>
</div>
</div>
</React.Fragment>
)
}
}
I am very confused on how the return method for RenderProject is working. When I begin the mapping process, I want to add each project to an array so I can keep track of how many and what projects are being rendered. When the array length hits three, I want it to stop rendering. But whenever I do this, my line if (tempArray.length <= 3) behaves in a way I don't expect it to. With how it is now, it won't return the <Card> and will instead return the else <div> for all 6 objects. But if I change the if statement to be if (tempArray.length >= 3) it will render all 6 objects inside of the array and no else <div>s. What should I be doing instead?
You can use state to keep track of how many array items to render by keeping track of the index you want to slice your array at. Then use a new sliced array to map over. Then you can have a button that increases the index state by 3 every time it is clicked:
import { useState } from 'react';
const Component = () => {
const [index, setIndex] = useState(3)
const yourArray = [...]
const itemsToRender = yourArray.slice(0, index);
return (
<>
<button onClick={() => setIndex(index + 3)}>load more</button>
<ul>
{ itemsToRender.map((item) => <li>{item}</li>) }
</ul>
</>
);
}
I would try something like that, using the index parameter (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). For sake of simplicity I will only add the part with the mapping function.
<div className="row">
{
projects.map((project, index) => {
if (index < 3){
return (
<Card key={project.id}>
<Card.Img variant="top" src={project.image} />
<Card.Body>
<Card.Title>{project.name}</Card.Title>
<Card.Text>
{project.description}
</Card.Text>
</Card.Body>
<button className="btn align-self-center">Go somewhere</button>
</Card>
)
}
else {
return (<div>Else return div</div>);
}
})
}
</div>
I would however consider using filter (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) or anyway create a subarray with the needed elements, then map over that array.
The important thing to remember with React is that you don't add things to the DOM in the traditional way - you update state. So, in this contrived example, I've added some data to state, and I also have a count state too.
count is initially set to 1 and on the first render the getItems function returns a new array of three items which you can then map over to get your JSX.
When you click the button it calls handleClick which sets a new count state. A new render happens, and a new set of count * 3 data is returned (six items).
And so on. I also added a condition that when the count is greater than the number of items in your data the button gets disabled.
const { useState } = React;
// So here I'm just passing in some data
function Example({ data }) {
// Initialise the items state (from the data passed
// in as a prop - in this example), and a count state
const [ items, setItems ] = useState(data);
const [ count, setCount] = useState(1);
// Returns a new array of items determined by the count state
function getItems() {
return [ ...items.slice(0, count * 3)];
}
// Sets a new count state when the button is clicked
function handleClick() {
setCount(count + 1);
}
// Determines whether to disable the button
function isDisabled() {
return count * 3 >= items.length;
}
return (
<div>
{getItems().map(item => {
return <div>{item}</div>;
})}
<button
onClick={handleClick}
disabled={isDisabled()}
>Load more
</button>
</div>
);
};
// Our data. It gets passed into the component
// as a property which then gets loaded into state
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
ReactDOM.render(
<Example data={data} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

How can I render an array of components in react without them unmounting?

I have an array of React components that receive props from a map function, however the issue is that the components are mounted and unmounted on any state update. This is not an issue with array keys.
Please see codesandbox link.
const example = () => {
const components = [
(props: any) => (
<LandingFirstStep
eventImage={eventImage}
safeAreaPadding={safeAreaPadding}
isActive={props.isActive}
onClick={progressToNextIndex}
/>
),
(props: any) => (
<CameraOnboarding
safeAreaPadding={safeAreaPadding}
circleSize={circleSize}
isActive={props.isActive}
onNextClick={progressToNextIndex}
/>
),
];
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={`component-key-${index}`} isActive={isActive} />;
})}
</div>
)
}
If I render them outside of the component.map like so the follow, the component persists on any state change.
<Comp1 isActive={x === y}
<Comp2 isActive={x === y}
Would love to know what I'm doing wrong here as I am baffled.
Please take a look at this Codesandbox.
I believe I am doing something wrong when declaring the array of functions that return components, as you can see, ComponentOne is re-rendered when the button is pressed, but component two is not.
You should take a look at the key property in React. It helps React to identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity
I think there are two problems:
To get React to reuse them efficiently, you need to add a key property to them:
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={anAppropriateKeyValue} isActive={isActive} />;
})}
</div>
);
Don't just use index for key unless the order of the list never changes (but it's fine if the list is static, as it appears to be in your question). That might mean you need to change your array to an array of objects with keys and components. From the docs linked above:
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. Check out Robin Pokorny’s article for an in-depth explanation on the negative impacts of using an index as a key. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.
I suspect you're recreating the example array every time. That means that the functions you're creating in the array initializer are recreated each time, which means to React they're not the same component function as the previous render. Instead, make those functions stable. There are a couple of ways to do that, but for instance you can just directly use your LandingFirstStep and CameraOnboarding components in the map callback.
const components = [
{
Comp: LandingFirstStep,
props: {
// Any props for this component other than `isActive`...
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
// Any props for this component other than `isActive`...
onNextClick: progressToNextIndex
}
},
];
then in the map:
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
There are other ways to handle it, such as via useMemo or useCallback, but to me this is the simple way — and it gives you a place to put a meaningful key if you need one rather than using index.
Here's an example handling both of those things and showing when the components mount/unmount; as you can see, they no longer unmount/mount when the index changes:
const {useState, useEffect, useCallback} = React;
function LandingFirstStep({isActive, onClick}) {
useEffect(() => {
console.log(`LandingFirstStep mounted`);
return () => {
console.log(`LandingFirstStep unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onClick}>LoadingFirstStep, isActive = {String(isActive)}</div>;
}
function CameraOnboarding({isActive, onNextClick}) {
useEffect(() => {
console.log(`CameraOnboarding mounted`);
return () => {
console.log(`CameraOnboarding unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onNextClick}>CameraOnboarding, isActive = {String(isActive)}</div>;
}
const Example = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const progressToNextIndex = useCallback(() => {
setCurrentIndex(i => (i + 1) % components.length);
});
const components = [
{
Comp: LandingFirstStep,
props: {
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
onNextClick: progressToNextIndex
}
},
];
return (
<div>
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
</div>
);
};
ReactDOM.render(<Example/>, document.getElementById("root"));
.active {
cursor: pointer;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

Deleting an array element of a react state

I know this is not react problem and I suck at coding. Can somebody tell me why my code is running so strange!
my project is a simple Todo List. I have 3 components.
Main app component
TodoList component
TodoItem component
I want to delete a TodoItem when the user clicks the delete icon.
this is app.js:
const [todoLists, setTodoLists] = useState([]);
function setTodoList(index, setFunction) {
setTodoLists(prevTodoLists => {
const newTodoLists = [...prevTodoLists]
newTodoLists[index] = {...setFunction(newTodoLists[index])};
console.log([...newTodoLists])
return [...newTodoLists];
});
}
return (
<section className="todoContainer" id="todocontainer">
{
todoLists.map((todoList, index) => {
return <TodoList todoList={todoList} index={index} setTodoList={setTodoList} />;
})
}
</section>
);
This is TodoList.js
function handleDeleteTodo(cardIndex) {
setTodoList(index, prevTodoList => {
const newTodoList = {...prevTodoList};
newTodoList.cards.splice(cardIndex, 1);
return {...newTodoList};
});
}
return (
<section className="body" ref={todosBodyRef} >
{
todoList.cards.map((todo, cardIndex) => {
return <TodoItem listIndex={index} cardIndex={cardIndex} todo={todo} handleDeleteTodo={handleDeleteTodo} />
})
}
</section>
);
This is TodoItem.js
function deleteButtonOnClick() {
handleDeleteTodo(cardIndex);
}
return (
<>
<p>{todo.name}</p>
<div className="controls">
<i className="far fa-trash-alt deleteCard" onClick={deleteButtonOnClick}></i>
</div>
</>
)
And when I Click On Delete Icon if TodoItem is the last TodoItem it deletes perfectly but if it's not the last item it will delete the next 2 Todoitems instead of itself.
I don't know what I did wrong. It will be great if someone explains to me what is happening :_(
Edit:
I added this if statement at handleDeleteTodo:
if (newTodoList.cards == prevTodoList.cards) {
console.log("True"); // It means both cards references are Same.
}
And it logged True. This means both cards references are same and I have to clone that as well.
Is there anyways to solve this problem without cloning cards Array? Because I am cloning whole todoList Object and I don't want to clone cards too.
I found the problem!
I was using a nested JS object and I was cloning it using spread operator. ( That is a shallow copy and it will not clone objects inside of objects! )
So I used rfdc and deep cloned my object using it. like this in TodoList.js:
import clone from 'rfdc/default';
function handleDeleteTodo(cardIndex) {
setTodoList(index, prevTodoList => {
const newTodoList = clone(prevTodoList); // This is where cloning happens
newTodoList.cards.splice(cardIndex, 1);
return newTodoList;
});
}

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 iterate over components inside components in React

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;
});

Categories

Resources