How to show objects from array one by one , in single component - javascript

I'm trying to build a component who going to have multiple objects from the array.
I want to show them one by one.
Let's assume, I have 3 objects inside the array. Then first object-related content should be shown first until the user opts to continue or skip for the next object until all 3 objects have been shown. How can I do this inside the One page?
For example, this is a minimal Code that how I'm going to make a component, I want to handle the Data like that each object should be shown independently and move to next on user input. Whether the user wants to skip or continue, the next object should be shown on the same page.
import { Fragment } from 'react'
import Data from 'Data'
const Main = () => {
const data = [
{ id: 1, more: '...' },
{ id: 2, more: '...' },
{ id: 3, more: '...' }
]
const submitHandler = () => {
// some action
}
return (
<Fragment>
<Card style={{ minHeight: '40rem' }}>
<Card.Body>{data ? data.map((el) => <div key={el.id} >
<Data obj={el} /> // Passing to child
</div>) : null}
</Card.Body>
<Card.Footer>
<Button variant="outline-danger" onClick={submitHandler} className="mx-1">
Skip
</Button>
<Button variant="primary" onClick={submitHandler}>
Continue
</Button>
</Card.Footer>
</Card>
</Fragment>
)
}
export default Main
Edit:
#jasonmzx below suggested some solution but it's giving type error. Can anybody fix this here , CodeSandBox

Here you could use the state to track which index the user is currently on and render the output based on your array
import { Fragment, useState } from 'react';
import Data from 'Data';
const Main = () => {
const [index,setIndex] = React.useState(0); // Starting from the beginning
const data = [
{ id: 1, more: '...' },
{ id: 2, more: '...' },
{ id: 3, more: '...' }
]
// I'm making this a function for cleanliness
// This renders the array at the index of the state
const showDat = (data, index) => {
return
<p>{data[index].more}</p>
}
const submitHandler = () => {
// Skip or cont: update state!
setIndex(index+1);
}
return (
<Fragment>
<Card style={{ minHeight: '40rem' }}>
<Card.Body>{data ? data.map((el) => <div key={el.id} >
<Data obj={el} /> // Passing to child
</div>) : null}
</Card.Body>
<Card.Footer>
<Button variant="outline-danger" onClick={submitHandler} className="mx-1">
Skip
</Button>
<Button variant="primary" onClick={submitHandler}>
Continue
</Button>
{showDat(data, index)}
</Card.Footer>
</Card>
</Fragment>
)
}
export default Main
Basically here is what will happen (when you code the submitHandler); the submitHandler updates the state, the state adds 1 onto index, and since the state has updated, a re-render happens, meaning the showDat() function being called within the Main component's render is being called again with the new index, to display the next index in ur array

Related

Delete a component always returning the last item of the array

I'm trying to delete a component receiving your id as param for my function deleteCard(). This component is wraped in an array, but at trigger the function this always deletes the last item of the array. I'm using map to render my array of cards.
Please check the code in my GitHub. I'm using react Modal for control the cards rendering and the deleteCard function is invoked from an external file:
https://github.com/pablolucio97/places-to-know
Card compoenent:
const Card = ({
id,
local,
countryName,
countryFlag,
goalDate,
openModalDelete,
openModalEdit
} : countryCardTypes) => {
return (
<CardContainer key={id}>
<TopContainer>
<CountryInfoContainer>
<ImageFlag src={countryFlag} />
<CountryTitle>{countryName}</CountryTitle>
</CountryInfoContainer>
<ButtonsContainer>
<EditButton onClick={openModalEdit}>
<MdEdit size={18} color='#333'/>
</EditButton>
<CloseButton onClick={openModalDelete}>
<MdClose size={18} color='#333'/>
</CloseButton>
</ButtonsContainer>
</TopContainer>
<Divider />
<BottomContainer>
<Text>Local: {local}</Text>
<Text>Meta: {goalDate}</Text>
</BottomContainer>
</CardContainer>
)
}
export default Card
Function to delete card:
async function deleteCard(id: number){
await axios.delete(`http://localhost:3333/cards/${id}`)
}
Rendering the array of cards:
{
countriesCards?.map(card => (
<Card
id={card.id}
local={card.local}
goalDate={card.goalDate}
countryName={card.countryName}
countryFlag={card.countryFlag}
openModalEdit={() => { setShowModalEdit(true) }}
openModalDelete={() => { setShowModalDelete(true) }}
/>
))
}
What to do for my deleteCard() deletes the current card that trigger this function?
Please check the code in my GitHub. I'm using react Modal for control the cards rendering and the deleteCard function is invoked from an external file:
https://github.com/pablolucio97/places-to-know

How do I add an list element seperately to 2 lists?

I am pretty new to react. So I have one parent component which has two child components. These 2 children are the lists that should be displayed. So far I figured out how to transfer the data between two lists by checking the status property of the data. I am not able to understand how to add data into the separate lists and edit them since the parent component renders the 2 lists. Can anyone explain how to add and edit new data that the user will enter? Should I create new states and props on the Items page or should I create them on the child component page? I am pretty confused.
import React,{useState,useEffect} from 'react'
import { Completed } from './Completed'
import { Pending } from './Pending'
export const Items = () => {
const [items,setItems]=useState([
{
id: 1,
title:'Workout',
status:'Pending'
},
{
id: 2,
title:'Read Books',
status:'Pending'
},
{
id: 3,
title:'Cook Pizza',
status:'Pending'
},
{
id: 4,
title:'Pay Bills',
status:'Completed'
},
{
id: 5,
title:' Watch Big Short',
status:'Completed'
},
{
id: 6,
title:' Make nutrition Plan',
status:'Pending'
}
])
const updateStatus=(id,newStatus)=>{
let allItems=items;
allItems=allItems.map(item=>{
if(item.id===id){
console.log('in here')
item.status=newStatus;
}
return item
})
setItems(allItems)
}
return (
<div class="items">
<Pending items={items} setItems={setItems} updateStatus={updateStatus}/>
<Completed items={items} setItems={setItems} updateStatus={updateStatus}/>
</div>
)
}
import React from 'react'
export const Pending = ({items,setItems,updateStatus}) => {
return (
<div className="pending">
<h1>LEFT</h1>
{
items && items.map(item=>{
if(item && item.status==='Pending')
return <><p className="item" key={item.id}>{item.title} <button className="mark_complete" key={item.id} onClick={()=>{updateStatus(item.id,'Completed')}}>Move Right</button></p></>
})
}
</div>
)
}
import React from 'react'
export const Completed = ({items,setItems,updateStatus}) => {
return (
<div className="completed">
<h1>RIGHT</h1>
<form onSubmit={this.addItem}>
<input placeholder="enter task">
</input>
<button type="submit">add</button>
</form>
{
items && items.map(item=>{
if(item && item.status==='Completed')
return <><p className="item" key={item.id}>{item.title} <button className="mark_pending" key={item.id} onClick={()=>{updateStatus(item.id,'Pending')}}> Move Left</button></p> </>
})
}
</div>
)
}
I have attached the 3 components which are Items, Pending and Completed above.
It's almost always better to have the state in the parent and pass down props to the children. So you want to keep your items state where it is. You can create an addItem function and pass it down as a prop to any child.
I don't think it makes sense to be able to add items from both lists since new items should be 'Pending'. So I would recommend that you put your add form in a new component AddItem which would be a third child of Items. Once AddItem calls the addItem function from props, that item will get saved to the state in items and it will show up in the Pending list automatically.
If all new items have status 'Pending' then the only information that we should need to add an item is the title of the task.
This function goes in Items:
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
Your AddItem component uses a controlled input to create the text for the new item.
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
Inside the return of Items, include your form:
<AddItem addItem={addItem} />
Unrelated to the question at hand, there are a few other improvements that you can make to your code.
Your updateStatus function actually mutates the current item. You should instead create a new object for the changed item by copying everything except the status.
You are getting warnings about unique keys because the key must be on the outermost component inside the .map(). You put a fragment <> outside the <p> which has the key, so remove the fragment.
In my opinion the filtering of which item goes in each list should be done by the parent. Your Completed and Pending components are extremely similar. You should combine them into one component. Everything that is different between the two, such as texts and class names, can be controlled by the props that you pass in.
import React, { useState } from "react";
export const ItemsList = ({
items,
title,
className,
buttonText,
onClickButton
}) => {
return (
<div className={className}>
<h1>{title}</h1>
{items.map((item) => (
<p className="item" key={item.id}>
<span className="item_title">{item.title}</span>
<button
className="move_item"
key={item.id}
onClick={() => {
onClickButton(item.id);
}}
>
{buttonText}
</button>
</p>
))}
</div>
);
};
// example of how to compose components
// this keeps the same setup that you had before, but without repeated code
export const Completed = ({ items, updateStatus }) => {
return (
<ItemsList
title="RIGHT"
buttonText="Move Left"
className="completed"
items={items.filter((item) => item.status === "Completed")}
onClickButton={(id) => updateStatus(id, "Pending")}
/>
);
};
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
export const Items = () => {
const [items, setItems] = useState([
{
id: 1,
title: "Workout",
status: "Pending"
},
{
id: 2,
title: "Read Books",
status: "Pending"
},
{
id: 3,
title: "Cook Pizza",
status: "Pending"
},
{
id: 4,
title: "Pay Bills",
status: "Completed"
},
{
id: 5,
title: " Watch Big Short",
status: "Completed"
},
{
id: 6,
title: " Make nutrition Plan",
status: "Pending"
}
]);
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
const updateStatus = (id, newStatus) => {
setItems((current) =>
// arrow function without braces is an implicit return
current.map((item) =>
item.id === id
? // copy to new item if id matches
{
...item,
status: newStatus
}
: // otherwise return the existing item
item
)
);
};
return (
<div className="items">
<AddItem addItem={addItem} />
{/* can set the props on ItemsList here */}
<ItemsList
title="LEFT"
buttonText="Move Right"
className="pending"
items={items.filter((item) => item.status === "Pending")}
// create a function that just takes the `id` and sets the status to "Completed"
onClickButton={(id) => updateStatus(id, "Completed")}
/>
{/* or do it in a separate component */}
<Completed items={items} updateStatus={updateStatus} />
</div>
);
};
export default Items;
Code Sandbox Link

Deleting list Item in react keep older items instead

I would like to delete selected item from list.
When I click on delete the right item get deleted from the list content but on UI I get always the list item fired.
I seems to keep track of JSX keys and show last values.
Here's a demo
const Holidays = (props) => {
console.log(props);
const [state, setState] = useState({ ...props });
useEffect(() => {
setState(props);
console.log(state);
}, []);
const addNewHoliday = () => {
const obj = { start: "12/12", end: "12/13" };
setState(update(state, { daysOffList: { $push: [obj] } }));
};
const deleteHoliday = (i) => {
const objects = state.daysOffList.filter((elm, index) => index != i);
console.log({ objects });
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
};
return (
<>
<Header as="h1" content="Select Holidays" />
<Button
primary
icon={<AddIcon />}
text
content="Add new holidays"
onClick={() => addNewHoliday(state)}
/>
{state?.daysOffList?.map((elm, i) => {
console.log(elm.end);
return (
<Flex key={i.toString()} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
key={i.toString()}
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>
<span>{JSON.stringify(state.daysOffList)}</span>
</Flex>
);
})}
</>
);
};
export default Holidays;
Update
I'm trying to make a uniq id by adding timeStamp.
return (
<Flex key={`${JSON.stringify(elm)} ${Date.now()}`} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
primary
key={`${JSON.stringify(elm)} ${Date.now()}`}
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>{" "}
</Flex>
);
I was hoping that the error disappear but still getting same behaviour
Issue
You are using the array index as the React key and you are mutating the underlying data array. When you click the second entry to delete it, the third element shifts forward to fill the gap and is now assigned the React key for the element just removed. React uses the key to help in reconciliation, if the key remains stable React bails on rerendering the UI.
You also can't console log state immediately after an enqueued state update and expect to see the updated state.
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
React state updates are asynchronous and processed between render cycles. The above can, and will, only ever log the state value from the current render cycle, not the update enqueued for the next render cycle.
Solution
Use a GUID for each start/end data object. uuid is a fantastic package for this and has really good uniqueness guarantees and is incredibly simple to use.
import { v4 as uuidV4 } from 'uuid';
// generate unique id
uuidV4();
To specifically address the issues in your code:
Add id properties to your data
const daysOffList = [
{ id: uuidV4(), start: "12/12", end: "12/15" },
{ id: uuidV4(), start: "12/12", end: "12/17" },
{ id: uuidV4(), start: "12/12", end: "12/19" }
];
...
const addNewHoliday = () => {
const obj = {
id: uuidV4(),
start: "12/12",
end: "12/13",
};
setState(update(state, { daysOffList: { $push: [obj] } }));
};
Update handler to consume id to delete
const deleteHoliday = (id) => {
const objects = state.daysOffList.filter((elm) => elm.id !== id);
setState(update(state, { daysOffList: { $set: objects } }));
};
Use the element id property as the React key
{state.daysOffList?.map((elm, i) => {
return (
<Flex key={elm.id} gap="gap.small">
...
</Flex>
);
})}
Pass the element id to the delete handler
<Button
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(elm.id)}
/>
Use an useEffect React hook to log any state update
useEffect(() => {
console.log(state.daysOffList);
}, [state.daysOffList]);
Demo
Note: If you don't want (or can't) install additional 3rd-party dependencies then you can roll your own id generator. This will work in a pinch but you should really go for a real proven solution.
const genId = ((seed = 0) => () => seed++)();
genId(); // 0
genId(); // 1

How to send pass function as props to child component

I have a parent component and some child components and I want send function from parent to child and in child component call it.
parent:
const buttons = [
{
key: 'driveFormulaButton',
title: 'Drive Formula',
icon: faGitAlt,
type: 'primary',
function: (message) => {
console.log('fasf');
},
},
];
return (
<Child
buttons={buttons}
></Child>
);
Child Component:
const Child = (props) => {
return (
<Button size="small" type={props.buttons.type} onClick={props.buttons.function('test')}> //not work after set propery
<FontAwesomeIcon icon={props.buttons.icon} />
</Button>
);
});
You call the function instead of passing it an onClick callback and you should map the buttons array:
const Child = (props) => {
return props.buttons.map((prop) => (
<Button
key={prop.key}
size="small"
type={prop.type}
onClick={() => prop.function("test")}
>
<FontAwesomeIcon icon={prop.icon} />
</Button>
));
}
Your parent component needs to map through the children components:
Parent
function renderChildren() {
const buttons = []; // <--- populate this as in your OP
return buttons.map((button) => {
return (
<Button size="small" type={button.type} onClick={() => button.function('test')}>
<FontAwesomeIcon icon={button.icon} />
</Button>
)
})
}
return (
<>
{renderChildren()}
</>
);
Well you send an array of buttons as props to you single Child-Component. Then you try to access a single object of that array in the Button-Component. This cant work. Better map throught you button in the parent and send a single button down as props:
const buttons = [{*Button1*, *Button2*}]
render(
{buttons.map((button) => <Child button={button}>)}
)
Then you can access the button as you intend to in your Child-component.
Next time you ask a question please provide better information to what you are actually trying to do and what the problem (e.g. error message) is. Ask specific questions. This makes it easier to answer you.

onClick in button element not triggering re-render in react-table when using react hooks with graphql

I'm trying to render a table using react-table, however, this table has different states that are being pulled from a GraphQL database. Each button should effectively render the same UI for the table, but only display the shipments that have the correct status associated with what button the user clicked.
My shipments query is as follows:
import { gql } from 'apollo-boost';
export const GET_SHIPMENTS = gql`
{
shipments {
created_at
id
status
orders {
order_items
}
}
}
`;
My table component using the GET_SHIPMENTS query looks like this:
import React, { useState } from "react";
import { graphql } from 'react-apollo';
import { GET_SHIPMENTS } from '../graphql/ShipmentQueries';
import ReactTable from 'react-table';
import {
Card,
CardBody,
Row,
ButtonGroup,
Button
} from "reactstrap";
function OrderTable ({ loading, shipments }) {
const [shownShipment, setShownShipment] = useState({status: "created"});
const columns = [
{
Header: 'ID',
accessor: 'id',
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Item Count',
accessor: 'orders[0].order_items'
},
{
Header: 'Time Stamp',
accessor: 'created_at',
},
];
if (loading) return <p>Loading...</p>;
return (
<div className="content">
<ButtonGroup className="center">
<Button
name="created"
onClick={() => setShownShipment(shownShipment.status === "created")}
>
Created
</Button>
<Button
name="awaiting driver"
onClick={() => setShownShipment(shownShipment.status === "awaiting_driver")}
>
Awaiting Driver
</Button>
<Button
name="delivered"
onClick={() => setShownShipment(shownShipment.status === "delivered")}
>
Delivered
</Button>
</ButtonGroup>
<Row className="mt-5">
<Card>
<CardBody>
<ReactTable
data={shipments}
columns={columns}
sortable={true}
resizable={false}
minRows={10}
/>
</CardBody>
</Card>
</Row>
</div>
);
}
export const OrderTableWithData = graphql(GET_SHIPMENTS, {
props: ({data: { loading, shipments, shownShipments }}) => ({
loading,
shipments,
shownShipments,
}),
})(OrderTable);
This is my first introduction into using hooks, so I know that I'm probably not utilizing them properly. I'm not sure if I have to use the useEffect hook or not. I've scoured the Hooks docs and can't seem to find a clear answer. I feel like useState should work. Do I have to re-render the entire ReactTable element?
If you want shownShipment.status to be the string corresponding to the delivery status (such as 'delivered', 'awaiting_driver') etc, your buttonClick code should look like this:
onClick={() => setShownShipment({status: "delivered"}) }
Try adding an effect to the react component like this to see the status updating after you click each button:
useEffect(() => { console.log(shownShipment); }, [shownShipment]);
Now that you have shownShipment set to the desired status, you can filter your total list of shipments from GQL based on this. UseState again for the list of shipments you will actually give to your table. useEffect will be helpful here as well. Something like this:
// at the top of the component
var [shipmentsToDisplay, setShipmentsToDisplay] = useState([]);
useEffect(() => {
// loop through all shipments and create a new array for only the ones
// you want to show.
const filteredShipments =
shipments &&
shipments.map(shipment => {
if (shipment.status === shownShipment) {
return shipment;
}
}
setShipmentsToDisplay(filteredShipments);
}, [shownShipment, shipments]);
// Render your table with these shipments rather than props.shipments
<ReactTable
data={shipmentsToDisplay}
columns={columns}
sortable={true}
resizable={false}
minRows={10}
/>
The solution for this code was as follows:
(Some code removed for brevity)
function OrderTable ({ loading, shipments }) {
const [shipmentsToDisplay, setShipmentsToDisplay] = useState([]);
useEffect(() => {
if(shipments) {
filterShipments("created");
}
}, [shipments]);
function filterShipments(status) {
let shownShipments = [];
shipments.forEach(element => {
if(element.status === status) {
shownShipments.push(element);
}
});
setShipmentsToDisplay(shownShipments);
}
if (loading) return <Loading />;
return (
<div className="content">
<ButtonGroup className="center">
<Button name="pending" onClick={() => filterShipments("created")}>
Pending
</Button>
<Button name="ready" onClick={() => filterShipments("awaiting_driver")}>
Ready
</Button>
<Button name="completed" onClick={() => filterShipments("delivered")}>
Completed
</Button>
</ButtonGroup>
<Row className="mt-5">
<ReactTable
data={shipmentsToDisplay}
columns={columns}
sortable={true}
resizable={false}
defaultPageSize={5}
className="-striped -highlight"
getTrProps={getTrProps}
SubComponent={row => {
return (
<ShipmentDetails
shipments={shipments}
/>
)
}}
/>
</Row>
</div>
);
}
export const OrderTableWithData = graphql(GET_SHIPMENTS, {
props: ({data: { loading, shipments }}) => ({
loading,
shipments
}),
})(OrderTable);
The solution was to land on the Pending shipments, so for my state, I used the useEffect hook to land on the shipments that have created as a status.
For my onClick function, I decided to filter through each shipment starting with the created shipments and update the state based on which status is associated with the button click. I don't know if that makes sense or not. I'm new to hooks and GraphQL.

Categories