I'm new to react and after a beginners course of learning react. i decided to take a personal project of creating a monthly subscription app that'll help me keep track of all my subscriptions. However, i'm stuck at a point where i need to pass the state of one component into another but that other component is accessible through a route, btw i'm using reach router. is there any way that is possible. below is my code. So what i want to do basically is that when i click on of the subscriptions in my list, (say: Netflix) it should basically take me another page, and show me all the neccesary details regarding that subscription. Here i want state (mylist) to be passed.
App.js
const App = () => {
return (
<div className="container">
<Router>
<List path="/" />
<Details path="/details/:sub" />
</Router>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
List.js
const List = () => {
const [mylist, setList] = React.useState([]);
const [value, setValue] = React.useState({
subscription: "",
startDate: "",
paymentTime: 0,
});
const handleSubmit = (e) => {
mylist.push(value);
setList(mylist);
setValue({ subscription: "", startDate: "", paymentTime: 0 });
e.preventDefault();
};
const handleOnChange = (event) => {
setValue({ ...value, [event.target.name]: event.target.value });
};
return (
<div>
<div className="for_list">
<ul className="list">
{mylist.map((obj) => (
// <Link to={`/details/${obj.subscription}`} key={obj.subscription}>
<li key={obj.subscription}>{obj.subscription}</li>
/* </Link> */
))}
</ul>
</div>
<div className="for_form">
<form>
<input
type="text"
name="subscription"
onChange={handleOnChange}
value={value.subscription}
/>
<input
type="date"
name="startDate"
onChange={handleOnChange}
value={value.startDate}
/>
<input
type="number"
name="paymentTime"
onChange={handleOnChange}
value={value.paymentTime}
/>
</form>
</div>
<button onClick={handleSubmit}>Add Item</button>
</div>
);
};
export default List;
Details.js
const Details = (props) => {
return (
<div>
Dunno what to do ;(
</div>
);
};
export default Details;
React Router uses location objects. One of the properties of a location object is state. You can pass some data field to state, then in your Details page, you will use these data fields to fetch data of each corresponding subscription.
In your List.js:
...
const handleSubmit = (e) => {
e.preventDefault();
mylist.push(value);
setList(mylist);
setValue({ subscription: "", startDate: "", paymentTime: 0 });
props.histoty.push({
pathname: '/details/netflix',
state: {
id: 'netflix-id',
}
})
};
...
export default withRouter(List);
Then in your Details.js:
import React, { useEffect, useState } from 'react'
import { withRouter } from 'react-router-dom'
const Details = (props) => {
const [subInfo, setSubInfo] = useState({})
useEffect(() => {
fetch(`your-server-api/${props.state.id}`).then(res => res.json()).then(data => setSubInfo(data))
}, [])
return (
<div>
...
</div>
);
};
export default withRouter(Details);
One thing to keep in mind is that there will be no state if a user navigates directly to the page, so you will still need some mechanism to load the data when it does not exist.
Related
I tried a lots of things , and this problem does not seem to go away , can someone help me with this ??
this is my app component :
function App() {
const [todo, setTodo] = useState([]);
async function getTodo() {
try {
const todo = await axios.get("http://localhost:5000/api/todos");
// console.log(todo.data)
setTodo(todo.data);
} catch (error) {
console.log("something is wrong");
}
}
useEffect(() => {
// Update the document title using the browser API
getTodo();
}, []);
return (
<div className="App">
<h1>My Todo List</h1>
<h2>My Todo List</h2>
<Task Todor={todo} />
<Write />
</div>
);
}
export default App;
and this is my todos component :
function Todos({ Todor }) {
return (
<div className="Todos">
{Todor.map(T => <Todo post={T} />)}
</div>
);
}
export default Todos;
and this is my todo component :
function Todo({ post }) {
return (
<div className="Todo">
<h2>{post.title}</h2>
</div>
);
}
export default Todo ;
and this my add component :
export default function Write() {
const [inputText, setInputText] = useState({
title: ""
});
function handleChange(e) {
setInputText({
...inputText,
[e.target.name]: e.target.value,
});
}
const [status, setStatus] = useState(false);
async function addItem(e) {
e.preventDefault();
const res = await axios.post("http://localhost:5000/api/todos", inputText);
setInputText(inputText)
console.log("response:", res)
setStatus(true);
setInputText("");
}
return (
<div className="container">
<div className="form">
<input onChange={handleChange} type="text" name="title" />
<button onClick={addItem}>
<span>Add</span>
</button>
</div>
</div>
);
}
the new items dont show until I refresh the page , how to do that without refreshing ?
because obviously that defeats the purpose of React !!
useEffect(() => {
// Update the document title using the browser API
getTodo();
}, []);
The code inside useEffect with empty dependencies array [] only runs on the first render, to run it on every render you should remove the empty array dependencies.
useEffect(() => {
// Update the document title using the browser API
getTodo();
});
Note: It is not a best practice because your component will invoke getTodo() every time rerendered. In your case, you can use a state variable to control where to re-run the getTodo funtion e.g:
const [isAddedSuccess, setIsAddedSuccess] = useState(false)
Everytime you add new item successfully, just setIsAddedSuccess(true) and your useEffect should look like below:
useEffect(() => {
// Update the document title using the browser API
if (isAddedSuccess) getTodo();
}, [isAddedSuccess]);
I am fetching the data of all products from an API call. All these products are objects. I am then checking if the value of search sting is present in all the productsindividually. If yes, that product is added to a different array. All the elements'titles in this different array are displayed as a dropdown menu.
Where am I going wrong? Plz help
Code on React:
import Head from "next/head";
const App = () => {
var resultfoundarray=[];
const [Searchquery,setSearchquery] = useState("");
const [AllProducts,setAllProducts] = useState([]);
const allproducts = () =>{
fetch('https://fakestoreapi.com/products')
.then(res=>res.json())
.then(json=>{console.log(json);
setAllProducts(json);
console.log(AllProducts);
})
}
const search = () =>{
allproducts();
AllProducts.forEach(prod => {
if(Searchquery in prod){
resultfoundarray.push(prod.title);
}
});
}
return(
<>
<StrictMode>
<Head>
<link rel="stylesheet" href="./css/general.css"></link>
</Head>
<div>
<div className="searchbardiv">
<div>
<input type="text" placeholder="Search" onChange={e=>setSearchquery(e.target.value) ></input>
<span><button type="submit" onClick={ e => search()}>Search</button></span>
<div>
<select>
{resultfoundarray.map((prodtitle) => {
<option>
{prodtitle}
</option>
})}
</div>
</div>
</StrictMode>
</>
)
}
export default App;
The main problem is that the fetch is running in the background, updating AllProducts asynchronously. In your search function, you trigger the fetch and access AllProducts immediately after starting the fetch, before the results came back.
You should trigger the fetch when the component mounts:
useEffect(() => allProducts(), []);
And then react to state changes in AllProducts
useEffect(() => AllProducts?.forEach(prod => {
if (Searchquery in prod) {
resultfoundarray.push(prod.title);
}
}, [AllProducts]);
There are more minor issues (e.g. it would be better to use useState for managing resultfoundarray too), but I guess you'll figure that out.
The full code would look like this (just copied & pasted, there is no guarantee it works):
import React from "react";
const App = () => {
var resultfoundarray = [];
const [Searchquery, setSearchquery] = useState("");
const [AllProducts, setAllProducts] = useState([]);
const allproducts = () => {
fetch("https://fakestoreapi.com/products")
.then((res) => res.json())
.then((json) => {
console.log(json);
setAllProducts(json);
console.log(AllProducts);
});
};
// load all products when component mounts
useEffect(() => allproducts(), []);
// update results found after products are loaded
// or the when search query changes
useEffect(
() =>
AllProducts?.forEach((prod) => {
if (Searchquery in prod) {
resultfoundarray.push(prod.title);
}
}),
[AllProducts, SearchQuery]
);
return (
<>
<StrictMode>
<Head>
<link rel="stylesheet" href="./css/general.css"></link>
</Head>
<div className="searchbardiv">
<div>
<input
type="text"
placeholder="Search"
onChange={(e) => setSearchquery(e.target.value)}
></input>
<span>
<button type="submit" onClick={(e) => search()}>
Search
</button>
</span>
<div>
<select>
{resultfoundarray.map((prodtitle) => {
<option>{prodtitle}</option>;
})}{" "}
</select>
</div>
</div>
</div>
</StrictMode>
</>
);
};
export default App;
There are issues as others have pointed out. But the issue you are not seeing any result being displayed is because of this condition
if(Searchquery in prod){
You are searching by the product property name instead of the product title. To correct that, the condition should be
if (prod.title.includes(Searchquery ))
I created my first 2 components in react using functional components, (I didn't understand how to use class based components but that's not the point)
I have logical problems, I do not understand what to do in this situation.
1-) I don't know if I have to send the full state of my child component to the parent
either
From the child component modify the state of the parent component
My code has the following structure:
Parent Component:
RequirementCreate (it is a view or page)
Contains the following fields:
Title of the requirement
Description
code:
const RequirementCreate = function () {
const [requirement_state, setRequirement_state] = useState({
title: "",
description: "",
participants: [],
});
return (
<Content>
<Box>
<Title>
<IconTitle icon="icon" width="30" height="30" />
Requirement Create
</Title>
</Box>
<BoxContent>
<InputGroup inputLabel={"Titulo"} iconName={"ic:baseline-abc"} />
<br />
<Label>Description</Label>
<TextArea rows="10" />
<EmailTag /> <-- CHILD COMPONENT
<ButtonContainer>
<Button to="/dashboard/requirement">Add</Button>
</ButtonContainer>
</BoxContent>
</Content>
);
};
export default RequirementCreate;
Child component:
EmailTag(array of emails)
This component checks
If it is a valid email address, if not it is repeated.
PS: this component works perfect, 100%
code:
const EmailTag = function () {
const [email_state, setEmail_State] = useState({
input_value: "",
emails: ["example#anydomain.com", "contac#google.com", "admin#stackoverflow.com"],
ErrorMessage: "",
});
const { emails, input_value, message } = email_state;
const handleChange = function (e) {
setEmail_State({ ...email_state, input_value: e.target.value });
};
const handleKeyDown = function (e) {
// logic Handle key Down...
};
const handleDelete = function (item) {
// logic handle delete here
};
const isInList = function (email) {
// check if the email address you are trying to enter is already added
};
const isEmail = function (email) {
// check if it is an email address
};
return (
<>
<TagContainer>
<div>
{emails.map((item, index) => (
<TagItem key={index}>
{item}
<TagButton type="button" onClick={() => handleDelete(item)}>
<DeleteButton icon="icon1" />
</TagButton>
</TagItem>
))}
</div>
<Input
placeholder="Participants"
value={input_value}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<span>{ErrorMessage}</span>
</TagContainer>
</>
);
};
export default EmailTag;
In React when this happens you typically want to maintain the state in the parent component, and pass it to the child component as a prop.
You can also pass a function as a prop into your child component and call it from the child.
So something like:
<EmailTag
requirement_state={requirement_state}
setRequirement_state={setRequirement_state}
/>
Then in your child component you would have access in your props such as:
props.setRequirement_state(...) which would call the function in your parent component.
I would recommend giving this a read.
Parent Component:
const RequirementCreate = function () {
const [requirement_state, setRequirement_state] = useState({
title: "",
description: "",
participants: [],
});
return (
<Content>
<Box>
<Title>
<IconTitle icon="icon" width="30" height="30" />
Requirement Create
</Title>
</Box>
<BoxContent>
<InputGroup inputLabel={"Titulo"} iconName={"ic:baseline-abc"} />
<br />
<Label>Description</Label>
<TextArea rows="10" />
<EmailTag
requirement_state={requirement_state}
setRequirement_state={setRequirement_state}
/>
<ButtonContainer>
<Button to="/dashboard/requirement">Add</Button>
</ButtonContainer>
</BoxContent>
</Content>
);
};
export default RequirementCreate;
Child Component will look like this and as an example I am modifying the parent state from handleChange method, you can create a separate method as per your needs to modify parent component state:
const EmailTag = function (props) {
const [email_state, setEmail_State] = useState({
input_value: "",
emails: ["example#anydomain.com", "contac#google.com", "admin#stackoverflow.com"],
ErrorMessage: "",
});
const { emails, input_value, message } = email_state;
const handleChange = function (e) {
setEmail_State({ ...email_state, input_value: e.target.value });
props.setRequirement_state({
title: "Anything as per your needs...",
description: "Anything as per your needs...",
participants: ["Anything as per your needs..."]})
};
const handleKeyDown = function (e) {
// logic Handle key Down...
};
const handleDelete = function (item) {
// logic handle delete here
};
const isInList = function (email) {
// check if the email address you are trying to enter is already added
};
const isEmail = function (email) {
// check if it is an email address
};
return (
<>
<TagContainer>
<div>
{emails.map((item, index) => (
<TagItem key={index}>
{item}
<TagButton type="button" onClick={() => handleDelete(item)}>
<DeleteButton icon="icon1" />
</TagButton>
</TagItem>
))}
</div>
<Input
placeholder="Participants"
value={input_value}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<span>{ErrorMessage}</span>
</TagContainer>
</>
);
};
export default EmailTag;
I tried to put a form in a separate reusable component but when used that way I can't type anything into the input. I observed, that after entering one letter (it does not appear in the input box) it seems that React rerender the whole component and the name is updated with the inserted letter.
in the version 2 the same code works correctly.
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
// VERSION 1. doesn't work
const FormEdit = () => (
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
return (
<Layout>
<div>
{name} //<-it shows only one letter
<FormEdit />
</div>
</Layout>
);
// VERSION 2 -> works properly
return (
<Layout>
<div>
{name} //<-the updated name is shown immediately
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
</div>
</Layout>
);
};
export default User;
The issue is directly related to declaring the FormEdit component within the other component. Here's why:
In a functional component, everything declared inside gets destroyed and re-created each render. It's no different than a normal function call. This is what makes React's hooks so special. They keep track of values in between renders and make sure they are re-created with the correct values.
You're declaring the FormEdit component inside a function, which means not only is it re-declared every render, but as a side-effect it also un-mounts and remounts each render as well.
This has a few different effects:
The component's input loses focus every render.
It's impossible for it to maintain its own state.
It's not very performant.
Below is a working example to demonstrate.
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
As for why you only see the first character; You are not giving the input a value, only an onChange. If the component does not unmount, this just makes it an "uncontrolled" component. The input still gets it's value updated, you just can't programatically control it. But, since it is unmounting and re-mounting every render, it loses its last value every time the user types.
Making it a controlled input would fix this:
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={name} onChange={handleChange("name")} type="text"/>
// ^ Add this
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
This is a little better, but still not ideal. Now it keeps the value each update, but it still loses focus. Not a very good user experience.
This final solution is to never declare a component within another component.
const {useState, useEffect} = React;
const FormEdit = (props) => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={props.name} onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
}
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
return (
<div>
{name}
<FormEdit name={name} handleChange={handleChange} submitEdit={submitEdit} />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Now it only mounts once, keeps focus, and updates as expected.
You would have to pass your form handlers to the child component as props so that the lifted state can be manipulated from the child.
// Parent Component
...
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
...
};
const submitEdit = event => {
...
};
return (
<Layout>
<div>
{name}
<FormEdit handleChange={handleChange} submitEdit={submitEdit}/>
</div>
</Layout>
);
and then in the child:
// Child Component
const FormEdit = (props) => (
<form>
<div className="form-group">
<input onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
Your FormEdit component which is inside the App component is causing the entire App component to re-render when the state gets updated onChange and hence you can only enter only one character at a time. It is generally not a great idea to declare a component within a component. Refer this link for more info. All you have to do is pull the FormEdit component out of the App component in its own separate function and pass the change handlers as props to the FormEdit component. Have a look at the working code below.
import React, { useState } from 'react';
const FormEdit = ({ handleChange, submitEdit, name }) => {
return (
<form>
<div className='form-group'>
<input onChange={handleChange('name')} type='text' value={name || ''} />
</div>
<button onClick={submitEdit} type='submit'>
Submit
</button>
</form>
);
};
export default function App() {
const [userdata, setUser] = useState();
const { name } = userdata || {};
const handleChange = key => event => {
setUser(prevState => {
return { ...prevState, [key]: event.target.value };
});
event.persist();
event.preventDefault();
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
return (
<div>
<div>
{name || ''}
<FormEdit
handleChange={handleChange}
submitEdit={submitEdit}
name={name}
/>
</div>
</div>
);
}
I'm creating a project-planning app using React, Redux, and Firebase. A single project record in my Firestore database contains a Title and some Content. When I go to update a project, I have the input fields' defaultValues set to the correct data for the project I want to edit. However, updating only works if I make changes to both the Content and Title input fields. Otherwise, upon submitting these values the data gets deleted because the local state has not seen any changes and therefore updates the untouched field to the empty string: ""
I have tried setting the local state of the EditProject component in the render method, but this is not possible:
render() {
const { project, auth } = this.props;
if (!auth.uid) return <Redirect to="/signin" />;
if (project) {
this.setState({
title: project.title,
content: project.content
});
...
I have also tried setting the state in during componentDidMount like so:
componentDidMount = () =>{
const { project } = this.props;
this.setState({
title: project.title,
content: project.content
})
}
But the issue with this is that the project prop does not get mapped by mapStateToProps before componentDidMount
Lastly, I've tried passing the project prop from the parent component, which is projectDetails, but I am unable to successfully do so. I might be doing this part wrong so please let me know if there is a good way to do this with the code I have. In ProjectDetails:
<Link to={"/edit/" + docId} key={docId}>
<button className="btn pink lighten-1 z-depth-0">Edit</button>
</Link>
This links to the 'broken' EditDetails component I am trying to fix.
Here is my code for the EditProject component
class EditProject extends Component {
state = {
title: "",
content: ""
};
handleChange = e => {
this.setState({
[e.target.id]: e.target.value
});
};
handleSubmit = e => {
e.preventDefault();
let localProject = this.state;
let docId = this.props.docId;
this.props.editProject(localProject, docId);
const projectDetailURL = "/project/" + docId;
this.props.history.push(projectDetailURL);
};
render() {
const { project, auth } = this.props;
if (!auth.uid) return <Redirect to="/signin" />;
if (project) {
return (
<div className="container section project-details">
<div className="card z-depth-0">
<div className="card-content">
<form onSubmit={this.handleSubmit} className="white">
<h5 className="grey-text text-darken-3">Edit Project</h5>
<div className="input-field">
<label htmlFor="title" className="active">
Title
</label>
<input
onChange={this.handleChange}
type="text"
id="title"
defaultValue={project.title}
/>
</div>
<div className="input-field">
<label htmlFor="content" className="active">
Edit Project Content
</label>
<textarea
id="content"
onChange={this.handleChange}
className="materialize-textarea"
defaultValue={project.content}
/>
</div>
<div className="input-field">
<button className="btn pink lighten-1 z-depth-0">
Update
</button>
</div>
</form>
</div>
<div className="card-action grey lighten-4 grey-text">
<div>
Posted by {project.authorFirstName} {project.authorLastName}
</div>
<div>{moment(project.createdAt.toDate()).calendar()}</div>
<div className="right-align" />
</div>
</div>
</div>
);
} else {
return (
<div className="container center">
<p>Loading project...</p>
</div>
);
}
}
}
const mapStateToProps = (state, ownProps) => {
//id = the document id of the project
const id = ownProps.match.params.id;
const projects = state.firestore.data.projects;
const project = projects ? projects[id] : null;
return {
project: project,
auth: state.firebase.auth,
docId: id
};
};
const mapDispatchToProps = dispatch => {
return {
editProject: (project, docId) => dispatch(editProject(project, docId))
};
};
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
),
firestoreConnect([
{
collection: "projects"
}
])
)(EditProject);
Upon visiting the edit page, I would like the data to remain unchanged if a user does not make any changes to an input field.
I was able to properly update my local state by using React Router to pass props to my EditProject component from its "parent component". I used the React router to do this since the EditProject component is not actually nested inside this "parent component".
Here's how you can pass props to other components using React Router:
Specify where you want to send your props and what you want to send:
//ProjectDetails Component
<Link to={{
pathname: "/edit/" + docId,
state: {
title: project.title,
content: project.content
}
}}>
<button className="btn">Edit</button>
</Link>
Aquire props in the componentDidMount() lifecycle method and update the local state using setState().
//EditProject Component (component recieving props from ProjectDetails)
class EditProject extends Component {
state = {
title: "",
content: ""
};
componentDidMount = () => {
//Aquire proprs from React Router
const title = this.props.location.state.title
const content = this.props.location.state.content
//Update the local state
this.setState({
title: title,
content: content
})
}
I hope this helps!