Update context without replacing existing values (React) - reactjs

Description of the problem
I'm trying to display different tasks from my context. Every time I submit my form, I want a new Task to appear under the other, instead of that after form submission the old task just gets replaced by the new one. I'm sure it's a mediocre problem, but this is my first react project.
Also, I'm using styled components so I'm leaving the css out of the code blocks for readability.
Context
import { createContext, useState } from "react";
export const TaskContext = createContext();
export const TaskProvider = ({ children }) => {
const [tasks, setTasks] = useState([]);
return (
<TaskContext.Provider
value={{
tasks,
setTasks,
}}
>
{children}
</TaskContext.Provider>
);
};
Form
import React from "react";
import styled from "styled-components";
import { useState, useContext } from "react";
import { TaskContext, TaskProvider } from "../TaskContext";
export default function Form() {
const [text, setText] = useState([]);
const [time, setTime] = useState([]);
const [reminder, setReminder] = useState(false);
const task = useContext(TaskContext);
const submitHandler = (e) => {
e.preventDefault();
task.setTasks([{ text, time, reminder }]);
setText("");
setTime("");
setReminder(false);
};
return (
<StyledForm onSubmit={submitHandler}>
<FormControl>
<Label>Task</Label>
<Input
type="text"
placeholder="Add Task"
value={text}
required
onChange={(e) => setText(e.target.value)}
></Input>
</FormControl>
<FormControl>
<Label>Day & Time</Label>
<Input
type="text"
placeholder="Add Day ß Time"
required
value={time}
onChange={(e) => setTime(e.target.value)}
></Input>
</FormControl>
<FormControlCheck>
<FormControlCheckLabel>Set Reminder</FormControlCheckLabel>
<FormControlCheckInput
type="checkbox"
value={reminder}
onChange={(e) => setReminder(e.currentTarget.checked)}
></FormControlCheckInput>
</FormControlCheck>
<Submit type="submit" value="Save Task"></Submit>
</StyledForm>
);
}
Tasks
import React from "react";
import { useContext } from "react";
import { TaskContext } from "../TaskContext";
import Task from "./Task";
export default function Tasks() {
const context = useContext(TaskContext);
return (
<>
{context.tasks.map((task) => (
<Task text={task.text} time={task.time} reminder={task.reminder}></Task>
))}
</>
);
}
Task
import React from "react";
import styled from "styled-components";
export default function Task(props) {
return (
<StyledTask>
<HeaderThree>{props.text}</HeaderThree>
<Parag>{props.time}</Parag>
</StyledTask>
);
}
Thanks if you took the time to read my post!

You just put a new task object into task array, that's why all old tasks will disappear. The solutions is you spread out the old tasks first, then add new task after that.
in submitHandler
replace task.setTasks([{ text, time, reminder }]);
with task.setTasks([...task.tasks, { text, time, reminder }]);

Related

Passing multiple form input fields' data in a child component to its parent in React (hooks)

I have a child component that isn't re-rendering because the state inside its parent isn't updating. I've recently found out that I need to pass data from child to parent, but I'm not sure how to do that. Most tutorials I've found on the subject show you how to pass one field or piece of information over to the parent by sending a function, but I have multiple fields on a form I need to send over to the parent component. I'm not sure how to go about that.
Here's the parent component.
import React, { useState } from "react";
import { useQuery } from "#apollo/client";
import { getStudents } from "../queries";
import StudentDetails from "./StudentDetails";
const StudentList = () => {
const [student, setStudent] = useState("");
const { loading, error, data } = useQuery(getStudents);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
const handleClick = (student)=> {
//console.log(student)
setStudent(student);
};
let filteredStudents = [];
//console.log(data.students)
for(let i = 0; i < data.students.length; i++){
//console.log(data.students[i].class.name)
if(data.students[i].class.name === "1FE1"){
//console.log(data.students[i].name)
filteredStudents.push(data.students[i])
}
}
//console.log(filteredStudents);
return (
<div>
<ul id="student-list">
{data.students.map((student) => (
<li key={student.id} onClick={(e) => handleClick(student)}>{student.name}</li>
))}
</ul>
{
student ? <StudentDetails student={student} />
: <p>No Student Selected</p>
}
</div>
);
};
export default StudentList;
And here is the child component called StudentDetails which displays a student's individual information that isn't re-rendering because StudentList's state isn't changing.
import React from "react";
import { useEffect, useState } from "react";
import { getStudentQuery } from "../queries";
import { useQuery } from "#apollo/client";
import DeleteStudent from "./DeleteStudent"
import EditStudent from "./EditStudent";
const StudentDetails = (props)=> {
console.log(props)
const [astudent, setStudent] = useState(props)
return (
<div id="student-details" >
<h2>Name: {props.student.name}</h2>
<h3>Age: {props.student.age}</h3>
<h3>Class: {props.student.class.name}</h3>
<h3>Test 1 Score: {props.student.test1}</h3>
<DeleteStudent id={props.student.id}/>
<EditStudent id={props.student.id} />
</div>
)
}
export default StudentDetails;
Inside StudentDetails is another child component called "EditStudent" which is where I need to somehow pass the information submitted in the form's fields over to StudentList.
import React, { useEffect, useState } from "react";
import { useMutation } from "#apollo/react-hooks";
//import { getStudents } from "../queries";
import StudentDetails from "./StudentDetails";
import { editStudentMutation, getStudentQuery, getStudents } from "../queries/index";
const EditStudent = (props) => {
console.log(props)
const [name, setName] = useState();
const [age, setAge] = useState();
const [test, setTest] = useState();
const [editStudent] = useMutation(editStudentMutation);
const astudent = props
return (
<form id="edit-student"
onSubmit={(e) => {
e.preventDefault();
editStudent({
variables: {
id: props.id,
name: name,
age: age,
test1: test
},
refetchQueries: [{ query: getStudents}]
});
}}>
<div className="field">
<label>Student Name:</label>
<input type="text"
value={name}
onChange={(e) => setName(e.target.value)}/>
</div>
<div className="field">
<label>Age:</label>
<input type="text"
value={age}
onChange={(e) => setAge(e.target.value)}/>
</div>
<div className="field">
<label>Test One:</label>
<input type="text"
value={test}
onChange={(e) => setTest(e.target.value)}/>
</div>
<button>submit</button>
</form>
)
}
export default EditStudent;
So yeah, I think I understand what I need to do but I don't know where to start on how to pass all the info from EditStudent over to StudentList. As I mentioned, all the tutorials on the subject show how to send one individual piece of information, but not several pieces. Could anyone suggest any pointers on how to achieve this?
I think what you are looking for is lifting a state up; essentially the parent passes a state to the child component and they both can access and change the state. For your case I would suggest passing multiple states down to the child.
Here is an example that does this: enter link description here

Enzyme Returns More Nodes Than Exist When Using Mount

component.tsx
import React, { ChangeEvent, FormEvent, useEffect, useState } from "react";
import { Form, FormControl, FormGroup, FormLabel } from "react-bootstrap";
import Button from "react-bootstrap/Button";
import Card from "react-bootstrap/Card";
import { useHistory } from "react-router-dom";
import { StorageKeys } from "../ProtectedRoute";
import "./styles.scss";
const Login = () => {
const history = useHistory();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSetEmail = (event: ChangeEvent<HTMLInputElement>) =>
setEmail(event.target.value);
const handleSetPassword = (event: ChangeEvent<HTMLInputElement>) =>
setPassword(event.target.value);
const handleSubmit = (event: FormEvent<HTMLElement>) => {
event.preventDefault();
console.log("email::", email);
console.log("password::", password);
localStorage.setItem(StorageKeys.TOKEN, "TODO: Auth");
history.push("/");
};
useEffect(() => {
localStorage.removeItem(StorageKeys.TOKEN);
}, []);
return (
<div id="login">
<Card id="loginCard">
<Card.Header>Login</Card.Header>
<Card.Body>
<Form onSubmit={handleSubmit}>
<FormGroup>
<FormLabel>Email address</FormLabel>
<FormControl type="email" id="email" placeholder="Enter email"
value={email} onChange={handleSetEmail}
required={true} />
</FormGroup>
<FormGroup>
<FormLabel>Password</FormLabel>
<FormControl type="password" id="password" placeholder="Password"
value={password} onChange={handleSetPassword}
required={true} />
</FormGroup>
<div className={"button-container"}>
<Button id="submit" variant="primary" type="submit">
Submit
</Button>
</div>
</Form>
</Card.Body>
</Card>
</div>
);
};
export default Login;
This works when using shallow to render the component:
login.test.tsx
import { mount, shallow } from "enzyme";
import React from "react";
import Login from "./index";
describe("Login Component", () => {
test("can properly submit form", () => {
jest.spyOn(window.localStorage.__proto__, "removeItem");
const wrapper = shallow(<Login />);
// This works just find, finds only the one #email input.
const emailInput = wrapper.find("#email");
emailInput.simulate("change", { target: { value: testLoginData.email } });
});
});
When using mount this throws the error:
Error: Method “simulate” is meant to be run on 1 node. 2 found instead
import { mount, shallow } from "enzyme";
import React from "react";
import Login from "./index";
describe("Login Component", () => {
test("can properly submit form", () => {
jest.spyOn(window.localStorage.__proto__, "removeItem");
const wrapper = mount(<Login />);
const emailInput = wrapper.find("#email");
// This will now complain about there being too many nodes.
emailInput.simulate("change", { target: { value: testLoginData.email } });
});
});
What gives? I need to use mount for the test that I'm working on, why is it finding multiple elements when there is for sure ONLY ONE.
I can patch it to work using the following, but I shouldn't have to... right?!
emailInput.at(0).simulate("change", { target: { value: testLoginData.email } });
So it's because your <FormControl is the first with this id and <input is second(or vice versa).
There are wide list of approaches:
.at(0) will work, but this way you will never know if you(because of error in the code) renders multiple elements. It might happen if conditions in conditional rendering {someFlag && <.... that suppose to be mutually exclusive are not. So really, it's a bad way.
Mock FormControl to be final element - so <input will not be returned anymore by .find()(honestly never used that, just assume it will work - but still looks messy and need additional boilerplate code for each test file, so not really handful way):
jest.mock('../FormControl.jsx', () => null);
use hostNodes() to filter only native elements(like <span> to be returned):
const emailInput = wrapper.find("#email").hostNodes();
I vote for 3rd option as most reliable and still safe for catching code logic's errors.

useEffect with local variable

I am trying to call useEffect funtion onchange of local variable, but its not working is only works if i use it with useState variable, I know there might be some basic thing here that I am not aware of.
sandbox link: https://codesandbox.io/s/affectionate-gareth-igyv7?file=/src/demo.js
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function Demo() {
const [value, setValue] = useState("");
let valueOne, valueTwo;
const setValueOne = (value) => {
valueOne = value;
};
useEffect(() => {
console.log(value);
console.log(valueOne);
}, [value, valueOne]);
return (
<div>
<h1>Demo</h1>
<input
placeholder="useState"
onChange={(e) => setValue(e.target.value)}
/>
<input
placeholder="function"
onChange={(e) => setValueOne(e.target.value)}
/>
{/* {console.log(valueOne)} */}
</div>
);
}
setValueOne will not rerender your component, If you want to fire a re-render, useEffect function needs to have a useState which basically hold state between re-renders.
You can try managing your state like below, its more readable and it will work too.
import React, { useState } from "react";
import "./styles.css";
export default function Demo() {
const [valueOne, setValueOne] = useState("");
const [valueTwo, setValueTwo] = useState("");
const handleValueOne = (e) => {
setValueOne(e.target.value);
};
const handleValueTwo = (e) => {
setValueTwo(e.target.value);
};
return (
<div>
<h1>Demo</h1>
<input
value={valueOne}
placeholder="useState"
onChange={handleValueOne}
/>
<input
value={valueTwo}
placeholder="function"
onChange={handleValueTwo}
/>
{/* {console.log(valueOne)} */}
</div>
);
}

React: send data from Child component to Parent component

This is a react beginners exercise so I'm looking for the simplest solution. I'm currently learning React so any helpful comments would also be appreciated. Thanks in advance!
This is the exercise:
In a functional component, create a div which contains 2 inputs and one button.
Each of these should be a separate child component, all rendered by a parent component called App.
You should collect the data from the inputs and make it available in the parent component (using a function would work for this).
Input 1 should collect the user's email.
Input 2 should collect the user's password.
When the button (also a separate component) is clicked then you should alert the data collected by the two inputs.
You should use the onChange and onClick events
I'm not quite sure what I have to do with the Button component. This is what I have so far for the exercise but I think it's wrong?......
APP.JS
import React from 'react'
import Email from './components/Email'
import Password from './components/Password'
const App = () => {
getData = data => '${data}'
return ( <div>
<Email getData = {getData} />
<Password getData = {getData} />
</div>
)
}
export default App
EMAIL.JS
import React from 'react'
const App = () => {
let email = '';
return (
<button onClick ={()=>props.getData({email})}></button>
);
}
export default Email;
PASSWORD.JS
import React from 'react'
const App = () => {
let password = '';
return (
<button onClick ={()=>props.getData({password})}></button>
);
}
export default Password;
BUTTON.JS
I have created a solution and a sandbox for the same.
App.js
import React, { useState } from "react";
import Email from "./components/Email";
import Password from "./components/Password";
import Button from "./components/Button";
const App = () => {
const [email, setEmail] = useState("");
const [password, setpassword] = useState("");
const [submit, setSubmit] = useState(false);
if (submit) {
console.log("email ", email);
console.log("password ", password);
}
return (
<div>
<Email setEmail={setEmail} />
<Password setpassword={setpassword} />
<Button setSubmit={setSubmit} />
</div>
);
};
export default App;
Password.js
import React from "react";
const Password = ({ setpassword, submit }) => {
return (
<>
Get Password
<input type="password" onChange={e => setpassword(e.target.value)} />
</>
);
};
export default Password;
Email.js
import React from "react";
const Email = ({ setEmail }) => {
return (
<>
Get Email
<input type="text" onChange={e => setEmail(e.target.value)} />
</>
);
};
export default Email;
Button.js
import React from "react";
const Button = ({ setSubmit }) => {
return (
<button
onClick={() => {
setSubmit(true);
}}
>
Submit
</button>
);
};
export default Button;
I would strongly suggest you go to reactjs.org and go through the documents before starting any course. Please don't copy paste. I hope it helps.

Custom react hook to for text field seems to be broken

I have been trying to come up with a custom hook to make the textfield configurable, i.e pass the set of data to a custom hook which would give me the text field that needs to be used.
The text field using the hook is being rendered as expected but I do not understand why this approach is breaking the input created using the custom hook. After every keystroke the input is losing focus and is not working as the other input that is using useState directly. It would be great if someone can explain what is going wrong and what I failed to understand.
App.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import useTextFieldBroken from "./useTextFieldBroken";
import "./styles.css";
function App() {
const [notBrokenValue, notBrokenSetValue] = useState("");
const [TextFieldBrokenInputOne] = useTextFieldBroken(
"brokenOne",
"Broken Input One",
""
);
const notBrokenOnChange = e => {
notBrokenSetValue(e.target.value);
};
return (
<div>
<label htmlFor="notBroken">
<h3>Not Broken Input</h3>
<input
id="notBroken"
onChange={notBrokenOnChange}
value={notBrokenValue}
/>
</label>
<TextFieldBrokenInputOne />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
customHook.js
import React, { useState } from "react";
const useTextFieldBroken = (id, label, initialValue = "") => {
const [value, setValue] = useState(initialValue);
const handleChange = e => {
setValue(e.target.value);
};
const TextField = () => {
console.log("Rendered the input field");
return (
<label htmlFor={id}>
<h3>{label}</h3>
<input
type="text"
name={id}
id={id}
onChange={handleChange}
value={value}
/>
</label>
);
};
return [TextField, value, setValue];
};
export default useTextFieldBroken;
https://codesandbox.io/s/4xj382vj40
Your input is losing focus because you're completely re-rendering the tree that creates it on each change.
The good news is that you don't need a hook to do this, just convert your hook into a functional component instead:
App.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import TextFieldBroken from "./useTextFieldBroken";
import "./styles.css";
function App() {
const [notBrokenValue, notBrokenSetValue] = useState("");
const notBrokenOnChange = e => {
notBrokenSetValue(e.target.value);
};
return (
<div>
<label htmlFor="notBroken">
<h3>Not Broken Input</h3>
<input
id="notBroken"
onChange={notBrokenOnChange}
value={notBrokenValue}
/>
</label>
<TextFieldBroken label="Previously Broken" id="previously-broken" />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
customHook.js
import React, { useState } from "react";
const TextFieldBroken = ({ id, label, initialValue = "" }) => {
const [value, setValue] = useState(initialValue);
const handleChange = e => {
setValue(e.target.value);
};
return (
<label htmlFor={id}>
<h3>{label}</h3>
<input
type="text"
name={id}
id={id}
onChange={handleChange}
value={value}
/>
</label>
);
};
export default TextFieldBroken;

Resources