Passing data to a Child Component React - reactjs

I have read:
react passing data from parent to child component
and
https://www.freecodecamp.org/news/pass-data-between-components-in-react/
While they outline the desired method. It does not seem to be working for the following example. I must be doing something obvious, but I cannot seem to figure out what I am doing wrong.
Structure
The structure is a wizard style form (4 steps) below which is contained in newrequest.js. Below each step is a list table component with various items associated with each step.
Think of it like a shopping cart, except you can have multiple carts (step 1), you can leave them in the store filled with all sorts of things (step 2-3) for quite some time before buying anything (step 4).
A user many want to create several carts all at the time, and then add/create/edit other items to the carts over the course of several months before finalizing. The carts, and the items in those carts need to stay grouped together, and they need to be found easily enough by the user to keep working on them at some later date.
User Action:
The user goes to step 1, either makes a new cart, or grabs an existing cart to edit and proceeds to step 2. For the edit option, I have been able to pass the id of an existing cart (list-request-saved.js) to (newrequest.js), edit, save etc. I can also tie the items to the cart with a cartid prop when this is happening the first time.
The Problem:
The issue is I cannot seem to figure out how to pass the cartid akarequestID, to step 2 when editing an existing cart akaSavedRequest so the database can fetch the items which belong in that cart. Without the cartid, the database just spits out all items in any cart by anyone. To fix this, I would need to get the cartid to "newrequest.js" from the child component "list-requests-saved.js " in step 1 (This works ok) and then pass the cartid to child component "list-components.js" in step 2 (This does not work ok).
newrequest.js (parent) // Removed the non-impacting sections
//queries
import { listTestRequests } from '../../graphql/queries'
import { listComponents } from '../../graphql/queries'
import { getSavedRequest } from '../../graphql/queries'
import { getComponent } from '../../graphql/queries'
//components
import SavedRequests from "../../pages/list-requests-saved"
import Components from "../../pages/list-components"
const initialTestRequestState = getInitialState(listTestRequests)
const initialComponentState = getInitialState(listComponents)
function NewRequest(props) {
const [testRequest, setTestRequest] = React.useState(initialTestRequestState)
const [component, setComponent] = React.useState(initialComponentState)
const [editRequest, setEditRequest] = React.useState('')
const [editComponent, setEditComponent] = React.useState('')
const requestTableToParent = (requestID) => {
setEditRequest(requestID)
}
const componentTableToParent = (componentID) => {
setEditComponent(componentID)
}
useEffect(() => {
fetchRequest(editRequest)
async function fetchRequest(id) {
if (!id) return
const requestData = await API.graphql({ query: getSavedRequest, variables: { id } })
setTestRequest(requestData.data.getSavedRequest)
}
}, [editRequest])
useEffect(() => {
fetchComponent(editComponent)
async function fetchComponent(id) {
if (!id) return
const componentData = await API.graphql({ query: getComponent, variables: { id } })
setComponent(componentData.data.getComponent)
}
}, [editComponent])
return (
<form
autoComplete="off"
noValidate
{...props}
>
{
activeStep == 0
? <SavedRequests requestTableToParent={requestTableToParent}/>
: null
}
{
activeStep == 1
? <Components componentTableToParent={componentTableToParent} requestID={editRequest}/>
: null
}
</form >
)
}
export default NewRequest
list-requests-saved.js (child-step-1) // Removed the non-impacting sections
function SavedRequests({requestTableToParent}) {
const renderEditButton = (params) => {
return (
<strong>
<Button
variant="text"
color="primary"
size="small"
style={{ marginLeft: 16 }}
onClick={() => requestTableToParent(params.row.id)}
>
Edit
</Button>
</strong>
)
}
export default (SavedRequests)
list-components.js (child-step-2) // Removed the non-impacting sections
import { listComponents } from '../graphql/queries'
import { deleteComponents as deleteComponentsMutation } from '../graphql/mutations'
function Components({componentTableToParent}, {requestID}) {
const [component, setComponent] = useState([])
const classes = useStyles();
console.log("requestID", requestID) //this is undefined
}
export default (Components)
Additional Commentary: editRequest in "newrequest.js" has the correct value which was set by the edit button in "list-requests-saved.js". When I'm trying to catch the value in the child component "list-components.js", requestID is reporting as undefined. What am I doing wrong?

Related

Is there a way to update context's state concurrently from multiple components in React?

I'm building a screen to display products available for sale. At the same time, each product would show its favorited status by the logged in user. To do this, each product loads its corresponding favoriteItem data from the backend.
If successful, 2 things happen:
1-the data is added to an array which is a state in a global context
2- the color of the heart is updated based on the state(by finding it)
The problem is the state always starts with the initial value(empty) before subsequent data is added to the array. As a result, the state ends up with only one item.
Below is some code for demo(I've simplified and omitted code for this demo):
// AccountProvider.tsx
const AccountProvider = ({children}: Props)=> {
const [favorites, setFavorites] = useState([])
const loadFavoriteItemForProduct = async (productId: string)=> {
const favoriteItem = await apiManager.getFavoriteItem(productId)
// favorites always resets to its inital value when this function is called
setFavorites([...favorites, favoriteItem])
}
const account: Account = {
loadFavoriteItemForProduct
}
return (
<AccountContext.Provider value={account}>
{children}
</AccountContext.Provider>
);
}
// ProductCard.tsx
// On mount, each productCard loads its favoriteItem data.
const ProductCard = ({product}: ProductCardProps) => {
const userAccount = useContext(AccountContext)
useEffect(()=>{
// loads product's favoritedItem. Favorites ought to contain
// as many favoriteItems in the backend. But, favorites is reinitialized before
// the new item is added. As a result, favorites contain only the favoriteItem of //the last ProductCard. See Screenshot attached
userAccount.loadFavoritedItemForProduct(product.productId)
}, [])
return (
<div>product.title</div>
)
}
// Products.tsx
const Products = () => {
const explore = useContext(ExploreContext)
return (
<div>
{explore.products.map(product => <ProductCard product={product} />)}
</div>
)
}
// index.tsx
...
<AccountProvider>
<ExploreProvider>
<Products />
</ExploreProvider>
</AccountProvider>
...
I'm just trying to use Context to model the userAccount which contains the users favorites. favorites ought to contain all favoriteItem data from the backend. But it only contains that of the last product. See attached screenshot.
Try passing an updater function to setFavorites.
setFavorites((favorites) => [...favorites, favoriteItem]);
React Docs:
https://beta.reactjs.org/apis/react/useState#updating-state-based-on-the-previous-state

React Redux how to retrieve object from state array based on button click

I'm working on a project where a user can input multiple exercise names/weights. These go into a state array as objects. For example:
[
{
name: bench
weight: 100
},
{
name: squat:
weight: 200
}
]
Each of these objects becomes a button that displays the name and weight. What I want to happen is allow the user to click the button and navigate to another page where that specific name and weight are displayed, but I can't figure out how to do that. Currently, I am getting the whole state array instead of just the specific one clicked.
Here is my button component:
const MovementButtons = (props) => {
const navigate = useNavigate();
const {
move
} = props;
const mapMoves = move.map((movement) => {
return (
<Button
key={movement.name}
onClick={() => navigate(`/movement/${movement.name}/${movement.weight}`)}
>
{movement.name} - {movement.weight}
</Button>
)
})
return <div>{mapMoves}</div>
};
const mapStateToProps = state => {
return {
move: state.moveReducer
}
};
export default connect(mapStateToProps)(MovementButtons);
Here is the page where the button redirects to and where I want to try and get the name of the button that was clicked:
const PercentChart = (props) => {
const {
move
} = props;
return (
<div>
{move.name}
{move.weight}
</div>
);
};
const mapStateToProps = (state) => {
return {
move: state.moveReducer,
}
};
export default connect(mapStateToProps)(PercentChart);
So right now if I click on the button labeled "bench" for example, I get redirected to the PercentChart page, where instead of getting just the "bench" object I get the whole state array. Can anyone point me in the right direction on how I can get the specific object based on the button that was clicked? If I am missing code that needs to be shown let me know and I'll update.

State rendering differently on page and in console

Having a state-nightmare that has had me stumped all week. Hope you can help.
I'm making a web app for gym-goers to record and track their progress on various different exercises. The exercises get stored in the user object, in a nested object, and then themselves have a nested object called history which the date and weight of their attempts, so an entry in users.exercises might look like:
{
exercise: "exercise 1",
target: "10",
history: [
{ date: 1, weight: 5 },
{ date: 2, weight: 6 },
{ date: 3, weight: 6 },
],
};
I then have a component which renders the specific exericse, called Exercise.js. At the moment, all it renders is the history.length:
<h1>History array length: {exerciseProp.history.length}</h1>
I also have a button which logs to the console that same property:
<button onClick={() => console.log(exerciseProp.history.length)}>
log
</button>
Initially, they give the same number. So far so good.
There's then a child component which allows users to add another entry to the history property. That is successfully updating the entry in MongoDB, but it isn't changing the number being rendered on the page. It is, however, changing the number that gets logged when I press the button. So the page will render, for example, "5", but when I press the button it logs "6". But I can't for the life of me work out why!
One thing I tried was this - I wondered if the problem was that it was getting the users array, using const users = useSelector((state) => state.auth); first and then not updating it when a child component updated state. So I've passed a handleUpdate prop down to the child component that then, as well as the component updating state, it updates the exerciseProp value with a newEntry:
const handleUpdate = (formData) => {
formData._id = "tempID";
let newEntry = exerciseProp;
newEntry.history = [...newEntry.history, formData];
setExerciseProp(newEntry);
};
I'd of course rather not have to do that and have it just refresh and update the users prop when the state changed, but I couldn't make that work.
I've tried to strip out all the irrelevant bits of code, but here's the component at the moment:
import React, { useState, useEffect } from "react";
import { ExerciseFooter } from "./ExerciseFooter";
import { useLocation } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { getUsers } from "../actions/auth";
export const Exercise = (props) => {
const location = useLocation();
const dispatch = useDispatch();
//get full list of users
useEffect(() => {
dispatch(getUsers());
}, []);
const users = useSelector((state) => state.auth);
//get local user
const localUser = JSON.parse(localStorage.getItem("profile"));
//initialise user and specific exercise variables
const [user, setUser] = useState("");
const [exerciseProp, setExerciseProp] = useState({
history: [""],
target: 0,
});
//set user and exercise to match params/local storage
useEffect(() => {
localUser &&
localUser?.result &&
users.length > 0 &&
setUser(
users.filter(
(filteredUser) => filteredUser._id == props.match.params.userId
)[0]
);
if (!localUser) setUser("");
setExerciseProp(
user?.exercises?.filter(
(exercise) => exercise._id == props.match.params.exerciseId
)[0]
);
}, [users]);
//force update to state
const handleUpdate = (formData) => {
formData._id = "tempID";
let newEntry = exerciseProp;
newEntry.history = [...newEntry.history, formData];
console.log("new Ent", newEntry);
setExerciseProp(newEntry);
};
if (exerciseProp) {
return (
<div style={{ marginTop: "150px" }}>
<h1>History array length: {exerciseProp.history.length}</h1>
<button onClick={() => console.log(exerciseProp.history.length)}>
log
</button>
{exerciseProp && (
<ExerciseFooter
user={user}
exercise={exerciseProp}
handleUpdate={(formData) => handleUpdate(formData)}
/>
)}
</div>
);
} else {
return <>Loading...</>;
}
};
So the main problem is: the state on the page isn't changing when I expect it to, and the secondary one - though I think my workaround for it might be okay - was that the state wasn't changing in the parent component when the child dispatched a change to it.
Any help very much welcome!

component state doesnt change even after replacing data

My image component displays images with a heart over it every time a user submits a search. The heart changes colors if the image is clicked, and should reset to white (default color) when user submits a new search. For some reason, the clicked-color persists even after a search. What am I not understanding about react states? This isn't simply something that changes on the next render. It just stays like that until I change it manually.
const Image = ({image, toggleFav, initialIcon, initialAlt}) => {
const [fav, setFav] = useState(false);
const [heartIcon, setHeartIcon] = useState(initialIcon)
const [heartAlt, setHeartAlt] = useState(initialAlt)
const handleClick = () => {
setFav(fav => !fav);
toggleFav(image.id, fav);
if (heartIcon == "whiteHeartIcon") {
setHeartIcon("redHeartIcon")
}
else {
setHeartIcon("whiteHeartIcon")
}
if (heartAlt == "white heart icon") {
setHeartAlt("red heart icon")
}
else {
setHeartAlt("white heart icon")
}
};
return (
<Grid item xs={4} key={image.id}>
<div className={`${fav ? "fav" : ""}`} onClick={handleClick}>
<div className="imgBox">
<img src={image.url} className="image"/>
<Heart icon={heartIcon} alt={heartAlt} className="heart"/>
</div>
</div>
</Grid>
);
}
This is the handle submit func for the component:
const searchAllImages = async (keyword) => {
const response = await searchImages(keyword);
const imageObjects = response.data.message.map((link, index) => {
let newImage = {
url: link,
id: link,
fav: false
};
return newImage;
});
dispatch({type: 'SET_IMAGES', payload: imageObjects});
};
I render the images through a redux store where it replaces the image state every time a new search is done. The state resides in Store.js where image is initially set to an empty list. The dispatch method comes from Reducer.js where the method is defined.
case "SET_IMAGES":
return {
...state,
images: action.payload
}
Have you tried setting the initial image to a different variable initially, then use a useEffect that checks the image variable for changes, and if it changes assign the value to the variable mentioned above. Anyways more like using useEffect or useCallback for actions.

How to link to a show view from an index using react hooks with firestore data

I am trying to figure out how to define a link to reference that can use a firebase document id to link to a show view for that document. I can render an index. I cannot find a way to define a link to the document.
I've followed this tutorial - which is good to get the CRUD steps other than the show view. I can find other tutorials that do this with class components and the closest I've been able to find using hooks is this incomplete project repo.
I want to try and add a link in the index to show the document in a new view.
I have an index with:
const useBlogs = () => {
const [blogs, setBlogs] = useState([]); //useState() hook, sets initial state to an empty array
useEffect(() => {
const unsubscribe = Firebase
.firestore //access firestore
.collection("blog") //access "blogs" collection
.where("status", "==", true)
.orderBy("createdAt")
.get()
.then(function(querySnapshot) {
// .onSnapshot(snapshot => {
//You can "listen" to a document with the onSnapshot() method.
const listBlogs = querySnapshot.docs.map(doc => ({
//map each document into snapshot
id: doc.id, //id and data pushed into blogs array
...doc.data() //spread operator merges data to id.
}));
setBlogs(listBlogs); //blogs is equal to listBlogs
});
return
// () => unsubscribe();
}, []);
return blogs;
};
const BlogList = ({ editBlog }) => {
const listBlog = useBlogs();
return (
<div>
{listBlog.map(blog => (
<Card key={blog.id} hoverable={true} style={{marginTop: "20px", marginBottom: "20px"}}>
<Title level={4} >{blog.title} </Title>
<Tag color="geekblue" style={{ float: "right"}}>{blog.category} </Tag>
<Paragraph><Text>{blog.caption}
</Text></Paragraph>
<Link to={`/readblog/${blog.id}`}>Read</Link>
<Link to={`/blog/${blog.id}`}>Read</Link>
</Card>
))}
</div>
);
};
export default BlogList;
Then I have a route defined with:
export const BLOGINDEX = '/blog';
export const BLOGPOST = '/blog/:id';
export const NEWBLOG = '/newblog';
export const EDITBLOG = '/editblog';
export const VIEWBLOG = '/viewblog';
export const READBLOG = '/readblog/:id';
I can't find a tutorial that does this with hooks. Can anyone see how to link from an index to a document that I can show in a different page?
I did find this code sandbox. It looks like it is rendering a clean page in the updateCustomer page and using data from the index to do it - but the example is too clever for me to unpick without an explanation of what's happening (in particular, the updateCustomer file defines a setCustomer variable, by reference to useForm - but there is nothing in useForm with that definition. That variable is used in the key part of the file that tries to identify the data) - so I can't mimic the steps.
NEXT ATTEMPT
I found this blog post which suggests some changes for locating the relevant document.
I implemented these changes and while I can print the correct document.id on the read page, I cannot find a way to access the document properties (eg: blog.title).
import React, { useHook } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
function ReadBlogPost() {
let { slug } = useParams()
// ...
return (
<div>{slug}
</div>
)
};
export default ReadBlogPost;
NEXT ATTEMPT:
I tried to use the slug as the doc.id to get the post document as follows:
import React, { useHook, useEffect } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
function ReadBlogPost() {
let { slug } = useParams()
// ...
useEffect(() => {
const blog =
Firebase.firestore.collection("blog").doc(slug);
blog.get().then(function(doc) {
if (doc.exists) {
console.log("Document data:", doc.data());
doc.data();
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
});
return (
<div>{blog.title}
</div>
)
};
export default ReadBlogPost;
It returns an error saying blog is not defined. I also tried to return {doc.title} but I get the same error. I can see all the data in the console.
I really can't make sense of coding documentation - I can't figure out the starting point to decipher the instructions so most things I learn are by trial and error but I've run out of places to look for inspiration to try something new.
NEXT ATTEMPT
My next attempt is to try and follow the lead in this tutorial.
function ReadBlogPost(blog) {
let { slug } = useParams()
// ...
useEffect(() => {
const blog =
Firebase.firestore.collection("blog").doc(slug);
blog.get().then(function(doc) {
if (doc.exists) {
doc.data()
console.log("Document data:", doc.data());
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
},
[blog]
);
return (
<div><Title level={4} > {blog.title}
</Title>
<p>{console.log(blog)}</p>
</div>
)
};
export default ReadBlogPost;
When I try this, the only odd thing is that the console.log inside the useEffect method gives all the data accurately, but when I log it form inside the return method, I get a load of gibberish (shown in the picture below).
NEXT ATTEMPT
I found this tutorial, which uses realtime database instead of firestore, but I tried to copy the logic.
My read post page now has:
import React, { useHook, useEffect, useState } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
import { Card, Divider, Form, Icon, Input, Switch, Layout, Tabs, Typography, Tag, Button } from 'antd';
const { Paragraph, Text, Title } = Typography;
const ReadBlogPost = () => {
const [loading, setLoading] = useState(true);
const [currentPost, setCurrentPost] = useState();
let { slug } = useParams()
if (loading && !currentPost) {
Firebase
.firestore
.collection("blog")
.doc(slug)
.get()
.then(function(doc) {
if (doc.exists) {
setCurrentPost(...doc.data());
console.log("Document data:", doc.data());
}
}),
setLoading(false)
}
if (loading) {
return <h1>Loading...</h1>;
}
return (
<div><Title level={4} >
{currentPost.caption}
{console.log({currentPost})}
</Title>
</div>
)
};
export default ReadBlogPost;
Maybe this blog post is old, or maybe it's to do with it using .js where I have .jsx - which I think means I can't use if statements, but I can't get this to work either. The error says:
Line 21:9: Expected an assignment or function call and instead saw
an expression no-unused-expressions
It points to the line starting with Firebase.
I got rid of all the loading bits to try and make the data render. That gets rid of the above error message for now. However, I still can't return the values from currentPost.
It's really odd to me that inside the return statement, I cannot output {currentPost.title} - I get an error saying title is undefined, but when I try to output {currentPost} the error message says:
Error: Objects are not valid as a React child (found: object with keys
{caption, category, createdAt, post, status, title}). If you meant to
render a collection of children, use an array instead.
That makes no sense! I'd love to understand why I can log these values before the return statement, and inside the return statement, I can log them on the object but I cannot find how to log them as attributes.
First of all: is your useBlog() hook returning the expected data? If so, all you need to do is define your <Link/> components correctly.
<Link
// This will look like /readblog/3. Curly braces mean
// that this prop contains javascript that needs to be
// evaluated, thus allowing you to create dynamic urls.
to={`/readblog/${blog.id}`}
// Make sure to open in a new window
target="_blank"
>
Read
</Link>
Edit: If you want to pass the data to the new component you need to set up a store in order to avoid fetching the same resource twice (once when mounting the list and once when mounting the BlogPost itself)
// Define a context
const BlogListContext = React.createContext()
// In a top level component (eg. App.js) define a provider
const App = () => {
const [blogList, setBlogList] = useState([])
return (
<BlogListContext.Provider value={{blogList, setBlogList}}>
<SomeOtherComponent/>
</BlogListContext.Provider>
)
}
// In your BlogList component
const BlogList = ({ editBlog }) => {
const { setBlogList } = useContext(BlogListContext)
const listBlog = useBlogs()
// Update the blog list from the context each time the
// listBlog changes
useEffect(() => {
setBlogList(listBlog)
}, [listBlog])
return (
// your components and links here
)
}
// In your ReadBlog component
const ReadBlogComponent = ({ match }) => {
const { blogList } = useContext(BlogListContext)
// Find the blog by the id from params.
const blog = blogList.find(blog => blog.id === match.params.id) || {}
return (
// Your JSX
)
}
There are other options for passing data as well:
Through url params (not recommended).
Just pass the ID and let the component fetch its own data on mount.
I found an answer that works for each attribute other than the timestamp.
const [currentPost, setCurrentPost] = useState([]);
There is an empty array in the useState() initialised state.
In relation to the timestamps - I've been through this hell so many times with firestore timestamps - most recently here. The solution that worked in December 2019 no longer works. Back to tearing my hair out over that one...

Resources