So, I've got this view of animal cards called like this:
<AnimalsCard {...elephant}/>
<AnimalsCard {...hippo}/>
<AnimalsCard {...sugar_glider}/>
My AnimalCard code looks like this:
export const AnimalsCard = ({ id, animal, img }) => {
const { t } = useTranslation();
return (
<>
<CardContainer id={id} img={img}>
<CardContent>
<CardTitle>{animal}</CardTitle>
<CardButton to={`${id}`}>Dowiedz się więcej</CardButton>
</CardContent>
</CardContainer>
</>
)
};
And my animal objects look like that:
export const elephant = {
animal: 'Słoń indyjski',
id: 'slon_indyjski',
species: 'mammals',
img: require('../../../images/animals/mammals/elephant.jpg'),
occurance: '',
habitat: '',
diet: '',
reproduction: '',
conservationStatus: '',
funFactOne: ''
}
When I click the button, new page of the animal id opens up:
<Route path="/:id" component={AnimalsDetails} />
On AnimalsDetails page, I would like to display all of the animal object data. I unfortunately have no idea how to pass it because I'm a beginner and hardly know anything about props and stuff.
My current approach is that in the AnimalsDetails I retrieve the ID using useParams();
export const AnimalsDetails = () => {
const [mammal, setMammal] = useState(null);
let { id } = useParams();
useEffect(() => {
const mammal = mammalsData.filter(thisMammal => thisMammal.id === id);
setMammal(mammal);
// console.log(mammal[0].animal);
}, [id]);
return ( <AnimalsDetailsContainer>
{/* <div>{mammal[0].id}</div> */}
</AnimalsDetailsContainer>
)
};
export default AnimalsDetails;
And then using the ID from URL, I'm filtering my mammalsData array that I've created only to test if the approach works (I wish I could use the previously created objects but I don't know if it's possible). It looks like that:
export const mammalsData = [
{
[...]
},
{
animal: 'abc',
id: 'fgh',
species: 'idontknow',
img: require('whatimdoing.JPG'),
occurance: '',
habitat: '',
diet: '',
reproduction: '',
conservationStatus: '',
funFactOne: ''
}
];
For now it works only when I'm on the animals card page and click the button, so the ID is present. If I'm somewhere else on the page and my ID is not declared yet, I receive an error that the data I want to display is undefined, which makes sense obviously. I've tried wrapping the return of the AnimalsDetails to log an error if the ID is not present but it didn't work.
I know there is some decent way to do that (probably with props or hooks defined differently), but I really tried many stuff and I'm utterly lost now. I wish I could pass the data to AnimalsDetails within clicking the button or something but have no idea how to do that. :-(
I'll be grateful for any help. I know it's basic stuff, but it's my first website ever.
I understand your problems.
In my opinion you 'd like to focus useEffect method as below.
useEffect(() => {
if(id != undefined)
// console.log('error);
return (
<div>{'error'}</div>
);
const mammal = mammalsData.filter(thisMammal => thisMammal.id === id);
setMammal(mammal);
// console.log(mammal[0].animal);
}, [id]);
when you want to pass your data with url, in the react router v6 has that capability.
Let say this is your Animals component,
import { Link } from "react-router-dom";
//let assume this is the data that you need to send
const elephant = {
animal: "Słoń indyjski",
id: "slon_indyjski",
species: "mammals",
img: require("../../../images/animals/mammals/elephant.jpg"),
occurance: "",
habitat: "",
diet: "",
reproduction: "",
conservationStatus: "",
funFactOne: "",
};
const Animals = props => {
//elephant.id is dynamic value. You have to add that value dynamically.
return (
<Link to={`${elephant.id}`} state={{ singleAnimal: elephant }}>
<div>this is single animal</div>
</Link>
);
};
In the Link component you have to mention your dynamic route and you cant send your data from state attribute.
In the receiving end,(Let say your receiving end component is SingleanimalData.
import { useLocation } from "react-router-dom";
const SingleanimalData = props => {
const location = useLocation();
//animal variable now has that recieving data.so console it to view data
const animal = location.state.singleAnimal;
return (
<div>
<div>My animal name:</div>
<div> {animal.animal} </div>
</div>
);
};
In here you have to use useLocation hook to grab data. I hope you this make sense to you.
If you are using the latest react router (v6), use element instead of render (here):
<Route path="/:id" element={<AnimalsDetails animal={yourAnimal} />} />
Try this way.
In the parent component add this part
<Route path="/:id" render={()=> <AnimalsDetails item={...animaldata} />} />
In AnimalDetails component
import React, { useEffect } from 'react';
import {useParams} from 'react-router-dom';
const { animalId } = useParams();
let animal;
useEffect(()=> {
if(animalId && props.item) {
animal = props.item.filter(a => a.id === animalId)
}
}, [animalId]);
Related
I found myself stupid and could not get my head around with the logic.
I would like to show a few info triggered byonClick, and only those with matched id.
For example, if I click on the button with id of 1, it would only want to show values in that specific object with id:1 like description, library, etc. Right now, all the data are displayed, and because I am using component in material ui, every drawer component are displayed on top of each other (overlapping).
I know the reason causing this is because I have the drawer component inside the map method, but what could be potential solution?
Below are my simple code,
The structure of my data looks like this,
export const projectdata = [
{
id: 1,
title: "",
subtitle: "",
thumbnail: "",
description:
"",
tech: [
],
library: [""],
WebsiteUrl: "",
GitHubUrl: "",
},
...more data with sequential id number...
]
Original:
const handleDrawerOpen = (event) => () => {
setOpen(!open);
};
...
...
<SwipeableDrawer
open={open}
onClose={handleDrawerOpen(false)}
onOpen={handleDrawerOpen(true)}>
***...only show data with matched id...***
</SwipeableDrawer>
I have already map the array to display the data on webpage inside a div like this,
<div>
{ projectdata?.map(({ id, title, subtitle, thumbnail, description, tech, WebsiteUrl, GitHubUrl, library, index }) => (
<>
<Card>
...some info displayed here...
<button onClick={handleDrawerOpen(id)}></button>
</Card>
<SwipeableDrawer>
***...only show data with matched id...***
</SwipeableDrawer>
</>
))}
<div>
One solution I can think of:
with useState(), pass in id as a prop
const [projectDetailId, setProjectDetailId] = useState(null);
const [projectDetailPage, setProjectDetailPage] = useState(false);
const handleDrawerOpen = (id) => {
setProjectDetailId(id);
setProjectDetailPage(true);
};
const handleDrawerClose = () => {
setProjectDetailId(null);
setProjectDetailPage(false);
};
...
...
{projectDetailId === id ?
<SwipeableDrawer
open={projectDetailPage}
onClose={handleDrawerClose}
></SwipeableDrawer>
: null
}
However, this will trigger strange behavior of the drawer (lagging and no animation), especially with mobile device.
Possibly due to this logic projectDetailId === id ? true : false.
Ok, after your update, your problem is that you create multiple drawers, one for each id. When you click on open and set the open prop to true, all your drawers use this same prop so they all open.
You should move the Drawer out of the for and only create one, and send your object that has the id as a prop to the content of the drawer you have.
something like:
const handleDrawerOpen = (yourData) => () => {
setOpen(!open);
setYourData(yourData)
};
...
// and somewhere on your code
<SwipeableDrawer open={open}>
<SomeComponentToShowTheData data={yourData}/>
</SwipeableDrawer>
I have two pages in a Next.js project, in the first one the user fills out a form to create a post, that info gets stored in a JSON object that has to be passed to the second page so the user can see a preview of the post and if he/she likes it, the post gets created.
The object is a little to big to be passed as query parameters (it has around 25 elements in it) but I can't find how to pass it to the second page props.
The function that gives the object it's values looks like this
async function submitHandler(event) {
event.preventDefault();
validateAndSetEmail();
validateAndSetTitle();
validateAndSetSalary();
if (isFormValidated === true) {
// this checks if the user has created another post
const finalPostData = await checkExistence(Email);
if (finalPostData["user"] === "usernot found") {
setPostData({
title: Title,
name: Name,
city: City,
email: Email,
type_index: TypeNumber,
type: Type,
region_index: regionNumber,
region: region,
category_index: categoryNumber,
category: category,
range: Range,
highlighted: highlighted,
featured: featured,
show_logo: showLogo,
show_first: showFirst,
description: Description,
price: basePrice + cardPrice,
website: Website,
logo: Logo,
application_link: applicationLink,
time_to_pin: weeksToPin,
pin: pin,
post_to_google: shareOnGoogle,
});
} else {
setPostData({
// set other values to the object
});
}
// Here I want to do something like
router.push({
pathname: "/preview/",
post: PostData,
});
}
}
The second page looks something like this:
export default function PreviewPage(props) {
const item = props.postInfo; // <= I want item to recieve the JSON object from the other page
return (
<Fragment>
<Navbar />
<div className="flex inline mt-12 ml-24">
<Card
category={item.category}
Name={item.company}
logo={item.logo}
City={item.company_city}
Website={item.company_website}
/>
<Description
title={item.job_title}
description={item.description}
category={item.category}
region={item.region}
Type={item.type}
Link={item.link}
/>
</div>
<Footer />
</Fragment>
);
}
Try
router.push({
pathname: "/preview/",
query: PostData,
});
export default function PreviewPage() {
const router = useRouter()
const item = router.query
After looking into React Context and Redux thanks to knicholas's comment, I managed to find a solution.
Created a context provider file
I create the Context with 2 things inside:
An empty object representing the previewData that will be filled later
A placeholder function that will become the setter for the value
import React from "react";
const PreviewContext = React.createContext({
Previewdata: {},
setPreviewData: (data) => {},
});
export default PreviewContext;
Then in _app.js
Create a useState instance to store the previewData and have a funtion to pass to the other pages in order to set the value when the form is filled, then, wrap the component with the context provider
function MyApp({ Component, pageProps }) {
const [previewData, setPreviewData] = useState({});
return (
<div className="">
<PreviewContext.Provider value={{ previewData, setPreviewData }}>
<Component {...pageProps} />
</PreviewContext.Provider>
</div>
);
}
export default MyApp;
In the form page
I get the function to set the value of previewData by using of useContext();
const { setJobPreviewData } = useContext(jobPreviewContext);
With that the value for previewData is available everywhere in the app
What I am trying to do
I'm trying to use an array of objects (habits) and render a "Card" component from each one.
Now for each Card (habit) you can "mark" that habit as done, which will update that Card's state.
I've used React.memo to prevent other Cards from re-rendering.
Whenever the user mark a card as done, the card header gets changed to "Edited"
The Issue I'm facing
Whenever a card is marked as done, the header changes alright, but whenever any other card is marked as done, the first card's state gets reverted back.
I've not been able to find other people facing a similar issue, can somebody please help?
Here is the code:
import React, { useState } from "react";
const initialState = {
habits: [
{
id: "1615649099565",
name: "Reading",
description: "",
startDate: "2021-03-13",
doneTasksOn: ["2021-03-13"]
},
{
id: "1615649107911",
name: "Workout",
description: "",
startDate: "2021-03-13",
doneTasksOn: ["2021-03-14"]
},
{
id: "1615649401885",
name: "Swimming",
description: "",
startDate: "2021-03-13",
doneTasksOn: []
},
{
id: "1615702630514",
name: "Arts",
description: "",
startDate: "2021-03-14",
doneTasksOn: ["2021-03-14"]
}
]
};
export default function App() {
const [habits, setHabits] = useState(initialState.habits);
const markHabitDone = (id) => {
let newHabits = [...habits];
let habitToEditIdx = undefined;
for (let i = 0; i < newHabits.length; i++) {
if (newHabits[i].id === id) {
habitToEditIdx = i;
break;
}
}
let habit = { ...habits[habitToEditIdx], doneTasksOn: [], name: "Edited" };
newHabits[habitToEditIdx] = habit;
setHabits(newHabits);
};
return (
<div className="App">
<section className="test-habit-cards-container">
{habits.map((habit) => {
return (
<MemoizedCard
markHabitDone={markHabitDone}
key={habit.id}
{...habit}
/>
);
})}
</section>
</div>
);
}
const Card = ({
id,
name,
description,
startDate,
doneTasksOn,
markHabitDone
}) => {
console.log(`Rendering ${name}`);
return (
<section className="test-card">
<h2>{name}</h2>
<small>{description}</small>
<h3>{startDate}</h3>
<small>{doneTasksOn}</small>
<div>
<button onClick={() => markHabitDone(id, name)}>Mark Done</button>
</div>
</section>
);
};
const areCardEqual = (prevProps, nextProps) => {
const matched =
prevProps.id === nextProps.id &&
prevProps.doneTasksOn === nextProps.doneTasksOn;
return matched;
};
const MemoizedCard = React.memo(Card, areCardEqual);
Note: This works fine without using React.memo() wrapping on the Card component.
Here is the codesandbox link: https://codesandbox.io/s/winter-water-c2592?file=/src/App.js
Problem is because of your (custom) memoization markHabitDone becomes a stale closure in some components.
Notice how you pass markHabitDone to components. Now imagine you click one of the cards and mark it as done. Because of your custom memoization function other cards won't be rerendered, hence they will still have an instance of markHabitDone from a previous render. So when you update an item in a new card now:
let newHabits = [...habits];
the ...habits there is from previous render. So the old items are basically re created this way.
Using custom functions for comparison in memo like your areCardEqual function can be tricky exactly because you may forget to compare some props and be left with stale closures.
One of the solutions is to get rid of the custom comparison function in memo and look into using useCallback for the markHabitDone function. If you also use [] for the useCallback then you must rewrite markHabitDone function (using the functional form of setState) such that it doesn't read the habits using a closure like you have in first line of that function (otherwise it will always read old value of habits due to empty array in useCallback).
I have a form with several layers of child components. The state of the form is maintained at the highest level and I pass down functions as props to update the top level. The only problem with this is when the form gets very large (you can dynamically add questions) every single component reloads when one of them updates. Here's a simplified version of my code (or the codesandbox: https://codesandbox.io/s/636xwz3rr):
const App = () => {
return <Form />;
}
const initialForm = {
id: 1,
sections: [
{
ordinal: 1,
name: "Section Number One",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
},
{
ordinal: 2,
name: "Numero dos",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
}
]
};
const Form = () => {
const [form, setForm] = useState(initialForm);
const updateSection = (idx, value) => {
const { sections } = form;
sections[idx] = value;
setForm({ ...form, sections });
};
return (
<>
{form.sections.map((section, idx) => (
<Section
key={section.ordinal}
section={section}
updateSection={value => updateSection(idx, value)}
/>
))}
</>
);
};
const Section = props => {
const { section, updateSection } = props;
const updateQuestion = (idx, value) => {
const { questions } = section;
questions[idx] = value;
updateSection({ ...section, questions });
};
console.log(`Rendered section "${section.name}"`);
return (
<>
<div style={{ fontSize: 18, fontWeight: "bold", margin: "24px 0" }}>
Section name:
<input
type="text"
value={section.name}
onChange={e => updateSection({ ...section, name: e.target.value })}
/>
</div>
<div style={{ marginLeft: 36 }}>
{section.questions.map((question, idx) => (
<Question
key={question.ordinal}
question={question}
updateQuestion={v => updateQuestion(idx, v)}
/>
))}
</div>
</>
);
};
const Question = props => {
const { question, updateQuestion } = props;
console.log(`Rendered question #${question.ordinal}`);
return (
<>
<div>{question.text}</div>
<input
type="text"
value={question.response}
onChange={e =>
updateQuestion({ ...question, response: e.target.value })
}
/>
</>
);
};
I've tried using useMemo and useCallback, but I can't figure out how to make it work. The problem is passing down the function to update its parent. I can't figure out how to do that without updating it every time the form updates.
I can't find a solution online anywhere. Maybe I'm searching for the wrong thing. Thank you for any help you can offer!
Solution
Using Andrii-Golubenko's answer and this article React Optimizations with React.memo, useCallback, and useReducer I was able to come up with this solution:
https://codesandbox.io/s/myrjqrjm18
Notice how the console log only shows re-rendering of components that have changed.
Use React feature React.memo for functional components to prevent re-render if props not changed, similarly to PureComponent for class components.
When you pass callback like that:
<Section
...
updateSection={value => updateSection(idx, value)}
/>
your component Section will rerender each time when parent component rerender, even if other props are not changed and you use React.memo. Because your callback will re-create each time when parent component renders. You should wrap your callback in useCallback hook.
Using useState is not a good decision if you need to store complex object like initialForm. It is better to use useReducer;
Here you could see working solution: https://codesandbox.io/s/o10p05m2vz
I would suggest using life cycle methods to prevent rerendering, in react hooks example you can use, useEffect. Also centralizing your state in context and using the useContext hook would probably help as well.
laboring through this issue with a complex form, the hack I implemented was to use onBlur={updateFormState} on the component's input elements to trigger lifting form data from the component to the parent form via a function passed as a prop to the component.
To update the component's input elelment, I used onChange={handleInput} using a state within the compononent, which component state was then passed ot the lifting function when the input (or the component as a whole, if there's multiple input field in the component) lost focus.
This is a bit hacky, and probably wrong for some reason, but it works on my machine. ;-)
I'm new to React and made an app that allows searches to be saved. This will pull JSON but is currently pulling from a static array data. I'm having trouble being able to delete searches from the search list.
Here's the jsbin: http://jsbin.com/nobiqi/edit?js,output
Here's my delete button element:
var DeleteSearch = React.createClass({
render: function() {
return (
<button onClick="this.props.deleteSearchItem" value={index}><i className="fa fa-times"></i>
</button>
);
}
});
and my function
deleteSearchItem: function(e) {
var searchItemIndex = parseInt(e.target.value, 10);
console.log('remove task: %d', searchItemIndex);
this.setState(state => {
state.data.splice(searchItemIndex, 1);
return { data: state.data };
});
}
I've tried following tutorials and I'm not sure where to go from here. How can I delete the search items?
Let me guess, Are you looking for something like this?
class Example extends React.Component {
constructor(){
this.state = {
data: [
{id:1, name: 'Hello'},
{id:2, name: 'World'},
{id:3, name: 'How'},
{id:4, name: 'Are'},
{id:5, name: 'You'},
{id:6, name: '?'}
]
}
}
// shorter & readable
delete(item){
const data = this.state.data.filter(i => i.id !== item.id)
this.setState({data})
}
// or this way, it works as well
//delete(item){
// const newState = this.state.data.slice();
// if (newState.indexOf(item) > -1) {
// newState.splice(newState.indexOf(item), 1);
// this.setState({data: newState})
// }
//}
render(){
const listItem = this.state.data.map((item)=>{
return <div key={item.id}>
<span>{item.name}</span> <button onClick={this.delete.bind(this, item)}>Delete</button>
</div>
})
return <div>
{listItem}
</div>
}
}
React.render(<Example />, document.getElementById('container'));
In this example pay attention how i'm binding delete method and pass there new parameter. fiddle
I hope it will help you.
Thanks
OP here. Since I know more about React four years later and this still gets views I figured I'd update this with how I'd go about it now.
SavedSearches.js
import React from 'react'
import { SearchList } from './SearchList'
let data = [
{index: 0, name: "a string", url: 'test.com/?search=string'},
{index: 1, name: "a name", url: 'test.com/?search=name'},
{index: 2, name: "return all", url: 'test.com/?search=all'}
];
let startingIndex = data.length;
export class SavedSearches extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
url: '',
index: startingIndex,
data: data
}
this.deleteSearch=this.deleteSearch.bind(this)
}
deleteSearch(deleteThis) {
console.log(deleteThis);
let newData = this.state.data.filter( searchItem => searchItem.index !== deleteThis.index )
this.setState({
data: newData
})
}
render() {
return (
<div className="search-container">
<SearchList data={this.state.data} onDelete={this.deleteSearch}/>
</div>
)
}
}
Here I created a method called deleteSearch that takes an object as a parameter. It then runs .filter on the this.state.data array to create a new array that includes all items that don't meet the condition. The condition checks if the id of each object in the data array matches the id of the parameter. If so, then it is the one that is being deleted. The new array that is created by .filter is set to a variable called newData, and then I update the state with the newData array.
I then pass this method to the SearchList component in a prop called onDelete.
This method is also bound in the constructor using .bind() so that this will refer to the correct this when the method is passed down the component tree.
SearchList.js
import React from 'react'
import { SearchItem } from './SearchItem'
export class SearchList extends React.Component {
render() {
let searchItems = this.props.data.map((item, i) => {
return (
<SearchItem index={i} searchItem={item} url={item.url} onDelete={this.props.onDelete}>
{item.name}
</SearchItem>
);
});
return (
<ul>
{searchItems}
</ul>
);
}
}
My deleteSearch method is just passing through the component tree here. SearchList receives the method as a props this.props.onDelete and passes it to SearchItem.
The other major key here is that the parameter in the map function is being passed as props: searchItem={item}. This will allow the entire current object to be accessed via props; and if you remember, my deleteSearch function takes an object as a parameter.
SearchItem.js
import React from 'react'
export class SearchItem extends React.Component {
constructor(props) {
super(props);
this.handleDelete=this.handleDelete.bind(this)
}
handleDelete() {
this.props.onDelete(this.props.searchItem)
}
render() {
return (
<li key={this.props.index}> {/* Still getting a console error over this key */}
<a href={this.props.url} title={this.props.name}>
{this.props.children}
</a>
({this.props.url})
<button onClick={this.handleDelete} value={this.props.index}><i className="fa fa-times"></i>
</button>
</li>
);
}
};
Now my method arrives where it will be used. I create a handler method handleDelete and inside I access the deleteSearch method with this.props.onDelete. I then pass it the object of the list item that is being clicked on with this.props.searchItem.
In order for this to work when a user clicks, I had to add an onClick event listener that calls my handler method, like this: onClick={this.handleDelete}. The final step is to bind this.handleDelete in the SearchItem constructor method.
Now, clicking on the button will remove the item from the this.state.data array. For an example of how to add an item to the array, see my repository
Are you looking for somthing like this?
Todos.js
import React from 'react'
import {TodoItem} from "./TodoItem";
export const Todos = (props) => {
let myStyle = {
minHeight: "70vh",
margin: "40px auto"
}
return (
<div className="container" style={myStyle}>
<h3 className="my-3">List</h3>
{props.todos.length===0? "No records to display":
props.todos.map((todo)=>{
console.log(todo.sno);
return (<TodoItem todo={todo} key={todo.sno} onDelete={props.onDelete}/>
)
})
}
</div>
)
}
TodoItem.js
import React from 'react'
export const TodoItem = ({todo, onDelete}) => {
return (
<>
<div>
<h4>{todo.title}</h4>
<p>{todo.desc}</p>
<button className="btn btn-sm btn-danger" onClick={()=>{onDelete(todo)}}>Delete</button>
</div>
<hr/>
</>
)
}
Please see the repository, here you can find add ,delete and list items