I'm trying to send a PATCH request to my NodeJS API from my react frontend. I want a situation whereby if you click the edit button, the initial name price appears on the input for necessary editing. Then after editing, you can update it. Displaying the initial data works fine , but saving it doesn't work. I get the error: "Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
I've looked up the cleanup function, but couldn't make a headway.
Below is my code.
const EditUserForm = () => {
const history = useHistory();
const match = useRouteMatch();
let routeId = match.params.id;
console.log(routeId);
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [item, setItem] = useState({});
const [name, setName] = useState();
const [price, setPrice] = useState();
const handleInputChange = (e) => {
console.log(e.target.value)
const { name, value } = e.target;
setItem({ ...item, [name]: value});
};
const handleSubmit = (e) => {
e.preventDefault();
updateProduct();
history.push('/');
}
const updateProduct = () => {
fetch(`/addproducts/${routeId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: item.name,
price: item.price
}),
})
.then((res) => res.json())
.then((result) => setItem(result))
.catch((err) => console.log('error: ', err))
}
useEffect(() => {
fetch(`/products/${routeId}`, requestOptions)
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setName(result.product.name);
setPrice(result.product.price);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}, []);
return (
<form onSubmit={handleSubmit} >
<label>Name</label>
<input
type="text"
name="name"
defaultValue={name}
onChange={handleInputChange}
/>
<label>Price</label>
<input
type="text"
name="price"
defaultValue={price}
onChange={handleInputChange}
/>
<button type="submit">Update</button>
<button>
Cancel
</button>
</form>
)
}
export default EditUserForm
inside "handleSubmit" you are calling "history.push('/')" which produces the error, if you want to change the route then call it in .then of updateProduct
Your useEffect function is applied to each and every rendering and is not cancellable, try rewriting it like this
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch(`/products/${routeId}`, {signal, ...requestOptions})
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setName(result.product.name);
setPrice(result.product.price);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
return controller.abort //so the fetch request can be canceled if the effect is re-executed
}, [routeId]); //only needs to rerun on route change
I am not too sure if the handles submit might cause similar problems, in which case you'd want to do something similar to this.
Related
I want to test the message appearing when the isLoading state is true and I also want to test that the message is not appearing after the fetch is finished. How would I do that?
The idea is that the user presses the submit button and a loading message appears while the request is being made, and as soon as the request ends, the loading message disappears.
Component I want to test
import React from 'react'
const fakeFetch = (url: string) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(null)
}, 2000)
})
}
const SimulateSubmit = () => {
const [loading, setLoading] = React.useState(false)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setLoading(true)
const data = await fakeFetch('/api/submit')
console.log('fetched')
setLoading(false)
}
return (
<div>
<h1>Click on the button</h1>
<form onSubmit={handleSubmit}>
<input type="text" name='name' placeholder='name' />
<button type="submit">Submit</button>
{loading && <p>Loading...</p>}
</form>
</div>
)
}
export default SimulateSubmit
Test
import {
render,
screen,
waitFor,
waitForElementToBeRemoved,
} from '#testing-library/react';
import user from '#testing-library/user-event';
import SimulateSubmit from './simulate-submit';
describe('SimulateSubmit', () => {
test('Should show loading message if submit is clicked', async () => {
render(<SimulateSubmit/>)
user.click(screen.getByText('Submit'))
expect(screen.queryByText('Loading...')).toBeInTheDocument()
})
test('Should remove loading message after fetched success', async () => {
render(<SimulateSubmit/>)
user.click(screen.getByText('Submit'))
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
})
})
})
I'm trying to find a way to update my "user" state, but I'm stuck here for 3 days already, I need some help.
Here is my user context:
import React, {useEffect, useState} from 'react';
export const UserContext = React.createContext({})
const UserProvider = UserContext.Provider;
const UserContextProvider = (props) => {
const [user, setUser] = useState({})
useEffect(() => {
fetch(`${API}/auth/user`, {
method: 'GET',
withCredentials: true,
credentials: 'include'
})
.then (response => response.json())
.then (response => {
setUser(response.user)
})
.catch (error => {
console.error (error);
});
}, [setUser])
console.log(user)
return (
<UserProvider value={{user, setUser}}>
{props.children}
</UserProvider>
)
}
export default UserContextProvider;
Here is where I'm trying to update the user. In my case I'm trying to push an object in user.cart array, cuz everything on the back-end is fine, but in the front-end the state is not updating:
First I'm using the UserContext:
const Products = () => {
const {user, setUser} = useContext(UserContext) ...
And then here I'm trying to update the user state, BUT when I click the button it logged me out:
<button className="addTo--Cart--Button--Container">
<FaShoppingCart onClick={() => {addToCart(user._id, product); setUser(oldState => oldState.cart.push(product))}}/>
</button>
After this logged me out, the console.log(user) which is in UserContextProvider function log only the user.cart updated lenght.
AND one more:
How to remove item from context:
Here is my remove function:
const removeFromContextCart = (id) => {
console.log(id)
const updatedCart = user.cart.filter((item) => item.id !== id);
setUser(oldState => ({
...oldState,
cart: [
...oldState.cart,
updatedCart
]
}))
}
And my button:
<button className="remove--Button" onClick={() => {removeFromCart(user._id, product); setUser(removeFromContextCart(product._id))}}> REMOVE</button>
Try updating the user state in this way
setUser(oldState => ({
...oldState,
cart: [
...oldState.cart,
product
]
}))
I have the following react-native-form:
const { register, handleSubmit, setValue, errors } = useForm();
const onSubmit = (data) => {
console.log(data);
return firebase
.auth()
.signInWithEmailAndPassword(data.email, data.password)
.then((info) => {
console.log(info.additionalUserInfo.profile);
})
.catch((err) => {
console.error(err);
});
};
<View>
<TextInput
placeholder="Email"
testID="email-input"
onChangeText={(t) => setValue("email", t)}
style={styles.loginTextInput}
></TextInput>
<TextInput
secureTextEntry={true}
testID="password-input"
placeholder="Password (min. 8 characters)"
onChangeText={(t) => setValue("password", t)}
style={styles.loginTextInput}
></TextInput>
<TouchableOpacity
onPress={handleSubmit(onSubmit)}
testID={"login-email-button"}
style={[styles.loginButton, styles.loginEmailButton]}
>
<Text style={styles.buttonText}>Login with Email</Text>
</TouchableOpacity>
</View>
I am testing the submission and the call to firebase.auth().signInWithEmailAndPassword using jest in the following test:
test("submit works", async () => {
const { getByPlaceholderText, getByTestId, getByText } = render(
<EmailLogin />
);
const emailInput = getByTestId("email-input");
const passwordInput = getByTestId("password-input");
const submitButton = getByTestId("login-email-button");
const email = "foo#email.com";
const password = "password";
fireEvent.changeText(emailInput, email);
fireEvent.changeText(passwordInput, password);
fireEvent.press(submitButton);
expect(firebase.auth().signInWithEmailAndPassword).toHaveBeenCalledWith(
email,
password
);
});
where I've mocked out signInWithEmailAndPassword as a jest.fn().
When I run this test, it fails with:
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: "foo#email.com", "password"
Received: undefined, undefined
And I noticed the console.log(data) I have in my onSubmit function prints out:
console.log
{}
which means no text was picked up.
How do I go about testing this form?
I think the reason why it returns undefined for you is that you are trying to test asynchronous behaviour in a sync way. I would suggest using Promises in your onSubmit method to wait for the firebase auth call to finish.
Something like this could work
const onSubmit = async (data) => {
console.log(data);
return await firebase
.auth()
.signInWithEmailAndPassword(data.email, data.password)
.then((info) => {
console.log(info.additionalUserInfo.profile);
})
.catch((err) => {
console.error(err);
});
};
This would then ensure that you are waiting for the sign in to happen.
In your test, I would mock the firebase to something like this
jest.mock('firebase', () => ({
auth: jest.fn().mockReturnThis(),
signInWithEmailAndPassword: jest.fn(),
})
);
And then in your test, you would also need to use waitFor() to wait for the sign in to happen so then you can check your results. Something like this could work
await waitFor(() => expect(firebase.auth().signInWithEmailAndPassword).toHaveBeenCalledWith(
email,
password
););
I haven't tested it myself but try the idea of using async and Promises and let me know if that works.
I'm trying to update the posts. The PUT request in the back end works fine, returning 200 and updates posts when tested on Postman however when I try to update a post in the front end (react), I'm not receiving any errors but the updated post isn't being updated on submit and the updated fields (title and body) are null. The updated values are null when I console.log(data) in the front end which is why they aren't being sent to the back end but they are shown correctly in post.
Why are the updated values null inside data? How can I update the post with the new values instead of getting null?
data:
post:
Updated code: Frontend
const EditPost = ({match}) => {
const [values, setValues] = useState({
title: "",
body: "",
error: ""
});
const [post, setPost] = useState({});
const { user, token } = isAuthenticated();
const {
title,
body,
error,
} = values;
const init = (id) => {
read(id).then(data => {
if (data.error) {
setValues({...values, error: data.error})
} else {
setValues({...values,
title: data.title,
body: data.body,
})
setPost({title: values.title, body: values.body})
}
})
}
useEffect(() => {
const id = match.params.id;
init(id);
}, []);
useEffect(() => {
setPost({...values });
}, [values.title, values.body]);
const handleChange = (name) => (event) => {
setValues({ ...values, [name]: event.target.value });
};
const clickSubmit = (event) => {
event.preventDefault();
setValues({ ...values, error: "" });
editPost(match.params.userId, match.params.id, token, post).then((data) => {
if (data.error) {
setValues({ ...values, error: data.error });
} else {
setValues({
...values,
title: "",
body: "",
error: false,
});
console.log(post)
console.log(data)
}
});
};
const newPostForm = () => (
<form onSubmit={clickSubmit}>
<div>
<input
onChange={handleChange("title")} type="text"
name="title"
value={title}
/>
</div>
<div className="form-group">
<textarea
onChange={handleChange("body")}
value={body} name="body"
/>
</div>
<button type="submit">Publish</button>
</form>
);
const showError = () => (
<div
style={{ display: error ? "" : "none" }}>
{error}
</div>
);
return (
<div>
{showError()}
{newPostForm()}
</div>
);
};
export default EditPost;
export const editPost = (userId, id, token, post) => {
return fetch(`${API}/${userId}/${id}/edit`, {
method: 'PUT',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify(post)
})
.then(response => {
return response.json();
})
.catch(err => console.log(err));
};
postsByUser.js
<Link className="mypost_btn edit_btn" to={`/${_id}/${post._id}/edit`}>
Edit
</Link>
Backend code
exports.edit = (req, res) => {
if (!ObjectID.isValid(req.params.id))
return res.status(400).send(`ID is not valid: ${req.params.id}`)
const {title, body} = req.body
const updatedPost = {title, body }
Post.findByIdAndUpdate(req.params.id, {
$set: updatedPost
}, {new:true}, (error, data) => {
if (error) {
return error
} else {
res.send(data)
console.log(data)
}
})
}
Your problem lies here:
editPost(match.params.userId, match.params.id, token, post)
post is not defined.
Since post is not defined, no data is passed. Hence title and body equal to null. What you will need to do is, assuming from what I'm seeing on your code, is to define a state variable called post. I think you intended to do that:
const [post, setPost] = useState({values.title, values.body});
Then ensure that your post is updated whenever your values change using useEffect(),
useEffect(() => {
setPost({...values });
}, [values.title, value.body]);
So by the time you call your editPost() http-put-method, then post has a value. And it should work.
in EditPost.js editPost(match.params.userId, match.params.id, token).then((data) => { here you are missing the 4th arg which is the "post" it self you send to be updated
I am a beginner.I am trying to implement a POST request from React.js in a simple form, but I cannot figure out how to send POST request to database. I guess I need <form action="URL"> as well.
Any help will be appreciated.
Below is the code from React.js(frontend)
import GameTestResult from './GameTestResult';
export default function App() {
const[data, setData] = useState([]);
const [formData, setFormData] = useState("");
useEffect (() => {
fetch('http://localhost:3000/game')
.then(res => res.json())
.then(result => setData(result.rows))
.catch(err => console.log("error"))
},[]);
const handleChange = event => {
setFormData(event.target.value)
}
const eventHandler = event => {
event.preventDefault();
setFormData("");
}
return (
<div className="App">
<form method="post" onSubmit = {eventHandler}>
<input value = {formData} onChange = {handleChange} />
<button type="submit">click</button>
</form>
{data && data.map((element, index)=>(
<GameTestResult
name = {element.name}
key={element.index}
/>
))}
</div>
);
}
here is the code from express.js(backend)
var router = express.Router();
const pool = require("../config.js");
var cors = require('cors');
router.get("/game", cors(), (req, res) => {
pool
.query("SELECT * FROM game")
.then((data) => res.json(data))
.catch((e) => {
res.sendStatus(404), console.log(e);
});
});
router.post("/game", (req, res) => {
const { name } = req.body;
pool
.query('INSERT INTO game(name) values($1);', [name])
.then(data => res.status(201).json(data))
.catch(e => res.sendStatus(404));
});
module.exports = router;
Here is what you can do:
Fetch games when component is mounted. And Submit new game when form is submitted.
export default function App() {
const [data, setData] = useState([])
const [formData, setFormData] = useState('')
useEffect(() => {
fetchGames() // Fetch games when component is mounted
}, [])
const fetchGames = () => {
fetch('http://localhost:3000/game', {
method: 'GET',
})
.then((res) => res.json())
.then((result) => setData(result.rows))
.catch((err) => console.log('error'))
}
const saveGames = () => {
fetch('http://localhost:3000/game', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: formData, // Use your own property name / key
}),
})
.then((res) => res.json())
.then((result) => setData(result.rows))
.catch((err) => console.log('error'))
}
const handleSubmit = (event) => {
event.preventDefault()
saveGames() // Save games when form is submitted
}
const handleChange = (event) => {
setFormData(event.target.value)
}
return (
<div className="App">
{/* method="post" not needed here because `fetch` is doing the POST not the `form` */}
{/* Also, note I changed the function name, handleSubmit */}
<form onSubmit={handleSubmit}>
<input type="text" name="name" value={formData} onChange={handleChange} />
<button type="submit">click</button>
</form>
{data &&
data.map((element, index) => (
<GameTestResult name={element.name} key={element.index} />
))}
</div>
)
}
You can read this about how to use fetch and this about how forms work in RecatJS.
Add name as "name" to input
Listen onChange and set data setFormData({[event.target.key]: event.target.value}) the data will be for example {name: "Tony"}
Call POST request on onClick action of button like code below
JSON.stringify(data) is important to convert js object to JSON when sending it to server
import GameTestResult from './GameTestResult'
export default function App() {
const [data, setData] = useState([])
const [formData, setFormData] = useState({})
useEffect(() => {
fetch('http://localhost:3000/game')
.then((res) => res.json())
.then((result) => setData(result.rows))
.catch((err) => console.log('error'))
}, [])
const handleChange = (event) => {
setFormData({ [event.target.name]: event.target.value })
}
const eventHandler = (event) => {
fetch('http://localhost:3000/game', {
method: 'POST',
body: JSON.stringify(formData),
})
.then((res) => res.json())
.then((result) => {
console.log(result)
setFormData('')
})
.catch((err) => console.log('error'))
}
return (
<div className="App">
<form>
<input name="name" value={formData.name || ''} onChange={handleChange} />
<button onClick={eventHandler}>click</button>
</form>
{data &&
data.map((element, index) => (
<GameTestResult name={element.name} key={element.index} />
))}
</div>
)
}