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>
Related
I'm somewhat new to javascript and react. Working on a personal project, and I'm trying to build functionality where a user can add and remove custom player configurations. At the moment, it seems the best way to do this is to have buttons to add and remove components as need.
This is what I have at the moment:
import React, { Component, useState } from 'react';
import Button from '#material-ui/core/Button'
import ButtonGroup from '#material-ui/core/ButtonGroup';
import Container from '#material-ui/core/Container'
import Grid from '#material-ui/core/Grid'
import DeleteIcon from '#material-ui/icons/Delete'
function Stats({ allStats, editedStats, statPlayerKey, statKey }) {
const removeStats = () => {
const filteredStats = allStats.filter((stat) => stat.statKey !== statKey);
editedStats(filteredStats);
}
return (
<Grid containter>
<Button onClick={removeStats}>
<DeleteIcon />
</Button>
<Grid item>
{statPlayerKey} {statKey}
</Grid>
</Grid>
)
}
function Player({ allPlayers, editedPlayers, playerKey, name }) {
const [stats, setStats] = useState([])
const handleRemove = () => {
const filteredPlayers = allPlayers.filter((player) => player.playerKey !== playerKey);
editedPlayers(filteredPlayers);
};
const statsList = stats.map(({statKey, statPlayerKey}) => (
<Container>
<Stats
allStats={stats}
statKey={statKey}
statPlayerKey={statPlayerKey}
editedStats={setStats}
/>
</Container>
))
const addStat = () => {
setStats([...stats, {statPlayerKey: playerKey, statKey: uuid()}])
}
return (
<Grid container>
<Button onClick={handleRemove}>
<DeleteIcon />
</Button>
<Grid item>
{playerKey} {name}
</Grid>
<Grid item>
{statsList}
</Grid>
<Button onClick={() => addStat()}>
Add Stat
</Button>
</Grid>
);
};
function TestApp() {
const [players, setPlayers] = useState([
{
playerKey: uuid(),
name: "Mario"
},
{
playerKey: uuid(),
name: "Luigi"
},
{
playerKey: uuid(),
name: "Toad"
}
]);
const addPlayer = () => {
setPlayers([...players, {
playerKey: uuid(),
name: 'Bowser'
}])
}
const playersList = players.map(({ playerKey, name }) => (
<Container>
<Player
allPlayers={players}
editedPlayers={setPlayers}
playerKey={playerKey}
name={name}
/>
</Container>
));
return (
<div className="App">
<h1>Team Members ({players.length})</h1>
<Container>{playersList}</Container>
<Button onClick={addPlayer}>ADD</Button>
</div>
);
}
export default TestApp;
Side notes - I'm using uuid's at the moment to track what's happening. Not sure if I should be trying to call those in the remove functions or not. Sorry for the current lack of front end design, planning to use material-ui, but I'm waiting to design the rest until I can get the functionality working as intended.
What am I doing wrong? Components are added easily enough, but the remove functionality is buggy. If you add a stat component to a player component and delete that player component, it passes the stat component to the next player in the list. I'd like the delete player button to delete the stat component(s) associated with the as well. I'd also like to add components within the stat components, so I want to make sure I'm executing this "components within components" method as efficiently as possible.
You have to remove State Value not Component, For that you have to make one function regarding removeState after that you able to remove function and function contains array' value remove from your current array.
Please do like remove array value(object) from state using splice
array.splice(index, 1);
in your case do like this :
removeState(e) {
var array = [...this.state.people]; // make a separate copy of the array
var index = array.indexOf(e.target.value)
if (index !== -1) {
array.splice(index, 1);
this.setState({people: array});
}
},
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>
}
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>
I have two functional components built in React, one is an Item component - it holds some data about stuff, with optional graphics, some text data and price information. On the bottom there is a button, that allows you to select this particular item. It also keeps information in its props on ID of currently selected Item - that's how i planned to solve this problem.
My second component is a ItemList - it basically holds a list of aforemetioned Items - plus it sorts all the items and must keep information about which component is currently selected - the selected one basically looks different - some stuff like the border box and button's colour gets switched via CSS.
My logic to implement goes like this - when user clicks on a "Select" button of a particular Item, the Item should change its look (unless it's already selected, then do nothing), after that somehow propagate info up onto the ItemList, so that it can "disable" the previously selected component. There can be only one selected Item, and once user decide to select another one, the previously selected should change its state and go back to unselected standard graphic style.
I've ran across a solution with state in the ItemList component plus passing a function via props into Item, but that doesn't solve the second part - ItemList needs to get info about a change, so it can rerender all the components according to actual state. What part of React API should I dive into to solve this issue?
Here is code for my components:
Item
interface Props {
receivedObject: itemToDisplay;
selectedItemId: string;
onClick?: () => void;
}
export default function Item(props: Props) {
const {name, description, price} = props.receivedObject;
const imageUrl = props.receivedObject?.media?.mainImage?.small?.url;
const priceComponent = <Price price={price}/>;
const [isItemSelected, setSelection] = useState(props.selectedItemId == props.receivedObject.id);
const onClick = props.onClick || (() => {
setSelection(!isItemSelected)
});
return (
<>
<div className="theDataHolderContainer">
// displayed stuff goes here
<div className="pickButtonContainer">
// that's the button which should somehow send info "upwards" about the new selected item
<Button outline={isItemSelected} color="danger" onClick={onClick}>{isItemSelected ? "SELECTED" : "SELECT"}</Button>
</div>
</div>
</>)
};
ItemList
interface Props {
packageItems: Array<itemToDisplay>
}
export default function ItemList(props: Props) {
const itemsToDisplay = props.packageItems;
itemsToDisplay.sort((a, b) =>
a.price.finalPrice - b.price.finalPrice
);
let selectedItemId = itemsToDisplay[0].id;
const [currentlySelectedItem, changeCurrentlySelectedItem] = useState(selectedItemId);
const setSelectedItemFunc = () => {
/* this function should be passed down as a prop, however it can only
* have one `this` reference, meaning that `this` will refer to singular `Item`
* how do I make it change state in the `ItemList` component?
*/
console.log('function defined in list');
};
return(
<div className="packageNameList">
<Item
key={itemsToDisplay[0].id}
receivedObject={itemsToDisplay[0]}
onClick={setSelectedItemFunc}
/>
{itemsToDisplay.slice(1).map((item) => (
<Item
key={item.id}
receivedObject={item}
onClick={setSelectedItemFunc}
/>
))}
</div>
);
}
In React, the data flows down, so you'd better hold state data in a stateful component that renders presentation components.
function ListItem({ description, price, selected, select }) {
return (
<li className={"ListItem" + (selected ? " selected" : "")}>
<span>{description}</span>
<span>${price}</span>
<button onClick={select}>{selected ? "Selected" : "Select"}</button>
</li>
);
}
function List({ children }) {
return <ul className="List">{children}</ul>;
}
function Content({ items }) {
const [selectedId, setSelectedId] = React.useState("");
const createClickHandler = React.useCallback(
id => () => setSelectedId(id),
[]
);
return (
<List>
{items
.sort(({ price: a }, { price: b }) => a - b)
.map(item => (
<ListItem
key={item.id}
{...item}
selected={item.id === selectedId}
select={createClickHandler(item.id)}
/>
))}
</List>
);
}
function App() {
const items = [
{ id: 1, description: "#1 Description", price: 17 },
{ id: 2, description: "#2 Description", price: 13 },
{ id: 3, description: "#3 Description", price: 19 }
];
return (
<div className="App">
<Content items={items} />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
.App {
font-family: sans-serif;
}
.List > .ListItem {
margin: 5px;
}
.ListItem {
padding: 10px;
}
.ListItem > * {
margin: 0 5px;
}
.ListItem:hover {
background-color: lightgray;
}
.ListItem.selected {
background-color: darkgray;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
I have milestoneCards.
I want to add a sort button, that upon clicking this button the cards will be sorted by the card heading.
The sort takes place, but it does not re-render the list in the sorted order.
please advise.
thank you so much for helping me here.
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { Card, CardBody, CardTitle } from "reactstrap";
const MyMilestones = props => {
let sortClicked = false;
let milestoneCards =
props.milestones.length > 0
? props.milestones.map(m => (
<p key={m.id}>
<Link to={`/milestones/${m.id}`}>{m.attributes.heading}</Link>
</p>
))
: null;
const sortedMilestoneCards = [...props.milestones]
.sort((a, b) => (a.attributes.heading > b.attributes.heading ? 1 : -1))
.map(m => (
<p key={m.id}>
<Link to={`/milestones/${m.id}`}>{m.attributes.heading}</Link>
</p>
));
return (
<div className="MilestoneCards">
{
<Card>
<CardBody>
<CardTitle>
<h4>My Milestones</h4>
</CardTitle>
<button
onClick={() => {
sortClicked = true;
console.log("before", milestoneCards);
milestoneCards = sortedMilestoneCards;
console.log("after", milestoneCards);
return (milestoneCards = sortedMilestoneCards);
}}
>
Sort
</button>
sortClicked ? ({sortedMilestoneCards}) : {milestoneCards}
</CardBody>
</Card>
}
</div>
);
};
const mapStateToProps = state => {
return {
milestones: state.myMilestones
};
};
export default connect(mapStateToProps)(MyMilestones);
It's because you need to have sortClicked to be tracked by React.
When let sortClicked = false is declared inside MyMilestones component, it's declared once on the first component mount and won't be updated when the component is re-rendered.
So you can save sortClicked in a state using React.useState and update it onClick. useState is a one-off way of storing this.state value for Class Component but for one state. (I won't get into it too deep as React documentation has a thorough coverage on Introducing Hooks)
const MyMilestones = props => {
// let sortClicked = false;
// Initialize it to "false" by default.
let [sortClicked, setSortClicked] = React.useState(false)
let milestoneCards = ...;
const sortedMilestoneCards = ...;
return (
<div className="MilestoneCards">
{
<Card>
<CardBody>
<CardTitle>
<h4>My Milestones</h4>
</CardTitle>
<button
onClick={() => {
// Notify "React" to re-render.
setSortClicked(true)
// No need to return a new reference here.
}}
>
Sort
</button>
{/* 👇 Note that {} is wrapped around the whole block. */}
{sortClicked ? sortedMilestoneCards : milestoneCards}
</CardBody>
</Card>
}
</div>
);
};
It's because you're not updating the milestones correctly. Since they're stored on Redux state, you need to add and dispatch the action that modifies the state.
I recommend you look at the Redux documentation.