React state one step behind despite useEffect - javascript

I have a React form for user login, everything works fine except for a setting a successful or unsuccessful message. Once i login, i set the value of a useState variable [res, setRes] to either successful or usuccessful depending on whether the user is registered or not. Problem is, even if the user is registered and username and password is correct, i get the message "invalid credentials" at least and most once. Subsequent calls from the same user result in the correct message being displayed. I searched and found that state is one step behind, and the solution is to use useEffect, but i am already using it. Can anyone help me figure out what the problem is? Code is as follows
export const Login = () => {
const email = useField('string')
const password = useField('password')
const [cred, setCred] = useState({})
const send = (e:any) => {
e.preventDefault()
setCred({'email':email.value, 'password':password.value})
showToast()
}
const [toastIsShown, setToastIsShown] = useState(false);
const showToast = () => {
setToastIsShown(true);
}
const [res,setRes] = useState('')
const hook = () => {
axios
.post('http://localhost:5000/auth', cred)
.then(response => {
console.log('response is ',response.data)
setRes('Login Successful')
})
.catch(err => {
console.log("error is ",err.response)
setRes('Invalid username or password')
})
}
useEffect(hook,[cred])
return (
<>
<form onSubmit = {send}>
<IonText>Enter Name</IonText>
<br />
<input {...email} />
<br />
<IonText>Enter Password</IonText>
<br />
<input {...password} />
<br />
<button>Send</button>
</form>
<IonToast
isOpen={toastIsShown}
onDidDismiss={() => setToastIsShown(false)}
message={res}
duration={3000}
/>
</>
)
}
I am using Ionic, which is why you see Toast there. Also, the language is Typescript.
Thanks

The useEffect hook is always called when a component mounts, and after that every time a value in its dependency array changes. Since an empty object is presumably not a valid log in, you're always going to get an unsuccessful attempt when the component mounts. You could do some simple validation like:
cred.email && cred.password && axios.post('http://localhost:5000/auth', cred)...
However, the root of the problem is that you are misusing useEffect. A log-in attempt is (usually, and in your case) a one time event, not a side-effect that occurs as the result of previous significant action. The side-effect in this scenario happens after the log-in attempt, when you trigger a Toast which contains a notification about the result:
export const Login = () => {
const email = useField('string');
const password = useField('password');
const [res, setRes] = useState('');
const [toastIsShown, setToastIsShown] = useState(false);
const send = (e:any) => {
e.preventDefault();
const cred = {
email: email.value,
password: password.value
};
axios
.post('http://localhost:5000/auth', cred)
.then(response => {
console.log('response is ',response.data)
setRes('Login Successful');
})
.catch(err => {
console.log("error is ",err.response)
setRes('Invalid username or password');
});
};
useEffect(() => {
res && setToastIsShown(true);
}, [res]);
return (
...
)
}
This is just to demonstrate a more reasonable use of useEffect. In reality I would probably not even use one here and instead just call setToastIsShown from inside send after setting res. A useEffect really comes in handy when you have two correlated pieces of data which are updated by multiple uncorrelated methods.

Related

React function takes two button clicks to run

I have an array of Notes that I get from my database, the notes objects each have a category assigned to it. There are also buttons that allow the user to filter the notes by category and only render the ones with the corresponding one.
Now, it's all working pretty well but there's one annoying thing that I can't get rid of: whenever I click on any of the buttons: <button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>, the filterNotes() function is only called on the second click. I suspect it has to do something with me calling setState() twice, or maybe with the boolean that I set in the functions, but I tried various combinations to call the function on the first click, but to no avail so far.
Here's my MainArea code:
import React, { useState, useEffect } from "react";
import Header from "./Header";
import Footer from "./Footer";
import ListCategories from "./ListCategories";
import Note from "./Note";
import axios from "axios"
function CreateArea(props) {
const [isExpanded, setExpanded] = useState(false);
const [categories, setCategories] = useState([])
const [notes, setNotes] = useState([])
const [fetchB, setFetch] = useState(true)
const [filterOn, setFilter] = useState(false)
const [note, setNote] = useState({
title: "",
content: "",
category: ''
});
useEffect(() => {
fetch('http://localhost:5000/categories')
.then(res => res.json())
.then(json => setCategories(json))
}, [])
useEffect(() => {
if(fetchB) {
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log(json)
setNotes(json)
setFetch(false)
})
}
}, [fetchB])
function handleChange(event) {
const { name, value } = event.target;
console.log("handleChange called")
setNote(prevNote => {
return {
...prevNote,
[name]: value
};
});
}
function submitNote(e){
e.preventDefault();
axios.post("http://localhost:5000/notes/add-note", note)
.then((res) => {
setNote({
category: '',
title: "",
content: ""
})
setFetch(true)
console.log("Note added successfully");
console.log(note)
})
.catch((err) => {
console.log("Error couldn't create Note");
console.log(err.message);
});
}
function expand() {
setExpanded(true);
}
function filterNotes(category){
if(filterOn){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log("filter notes")
setNotes(json)
setNotes(prevNotes => {
console.log("setNotes called with category " + category)
return prevNotes.filter((noteItem) => {
return noteItem.category === category;
});
});
setFilter(false)
})
}
}
return (
<div>
<Header/>
<ListCategories categories={categories} notes={notes} filterNotes={filterNotes} setFilter={setFilter} filterOn={filterOn} setFetch={setFetch}/>
<form className="create-note">
{isExpanded && (
<input
name="title"
onChange={handleChange}
value={note.title}
placeholder="Title"
/>
)}
<textarea
name="content"
onClick={expand}
onChange={handleChange}
value={note.content}
placeholder="Take a note..."
rows={isExpanded ? 3 : 1}
/>
<select
name="category"
onChange={handleChange}
value={note.category}>
{
categories.map(function(cat) {
return <option
key={cat.category} value={cat.value} > {cat.category} </option>;
})
}
</select>
<button onClick={submitNote}>Add</button>
</form>
<Note notes={notes} setFetch={setFetch}/>
<Footer/>
<button onClick={()=>{setFetch(true)}}>All</button>
</div>
);
}
export default CreateArea;
And ListCategories where I get call the function and get the chosen category from the buttons:
import React, { useState } from "react";
import CreateCategory from "./CreateCategory";
export default function ListCategories(props) {
function handleClick(category){
props.setFilter(true)
props.filterNotes(category)
}
return (
<div className="category-group">
<CreateCategory/>
<div className="btn-group">
{props.categories.map((categoryItem, index) =>{
return(
<button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>
)
})}
</div>
</div>
)
}
I'm not sure what the best practice is with such behaviour - do I get the notes from the database each time as I'm doing now or should I do something completely different to avoid the double-click function call?
Thanks in advance for any suggestions!
Your issue is this function:
function handleClick(category){
props.setFilter(true)
props.filterNotes(category)
}
Understand that in React, state is only updated after the current execution context is finished. So in handleClick() when you call setFiler(), that linked filterOn state is only updated when the rest of the function body finishes.
so when your filterNotes() function is called, when it evaluates filterOn, it is still false, as it was initially set. After this function has executed, the handleClick() function has also finished, and after this, the filterOn state now equals true
This is why on the second click, the desired rendering effect occurs.
There are multiple ways to get around this, but I normally use 'render/don't-render' state by including it as an embedded expression in the JSX:
<main>
{state && <Component />}
</main>
I hope this helps.
You diagnosed the problem correctly. You shouldn't be using state like you would a variable. State is set asynchronously. So, if you need to fetch some data and filter it, do that and THEN add the data to state.
function filterNotes(category){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
const filtered = json.filter((noteItem) => (noteItem.category === category));
setNotes(filtered);
})
}
}
It's not clear to me why you would need the filterOn state at all.
Depending on how your frequently your data is updated and if you plan on sharing data across users, the answer to this question will vary.
If these notes are specific to the user then you should pull the notes on load and then store them in a local state or store. Write actions that can update the state or store so that this isn't coupled with your react UI rendering. Example: https://redux.js.org/ or https://mobx.js.org/README.html.
Then update that store and your remote database accordingly through dispatching actions. This avoids lots of calls to the database and you can perform your filtering client-side as well. You can then also store data locally for offline use through this method so if it's for a mobile app and they lose internet connection, it'll still render. Access the store's state and update your UI based on that. Specifically the notes and categories.
If you have multiple users accessing the data then you'll need to look at using websockets to send that data across clients in addition to the database. You can add listeners that look for this data and update that store or state that you will have created previously.
There are many approaches to this, this is just an approach I would take.
You could also create a context and provider that maintains your state on the first load and persists after that. Then you can avoid passing down state handlers through props

useState is not updating initial value in useEffect

I am working on like functionality for my web-app. The problem here is my setLike is not changing the state even after using setLike(!like). I checked this with console.log() by using it before and after the setlike() but both the console.log() statements were giving me the same value. Here is my Post component.
import React , {useState,useEffect} from 'react'
import './Post.css'
import Avatar from '#material-ui/core/Avatar';
import {Button, IconButton, Input, Typography } from '#material-ui/core';
import {DataBase} from './firebase'
import firebase from 'firebase';
import FavoriteIcon from '#material-ui/icons/Favorite';
import {useStateValue} from '../contexts/StateProvider'
function Post({postId}) {
//get the user from the provider
const [{user}, dispatch] = useStateValue();
//number of likes
const [likeCount,setLikeCount] = useState(likesCount)
//if like=true or not
const [like, setLike] = useState(false);
I am using firebase firestore database for backend. The like document only exists in the database collection when the user interacts with it for the first time (likes it or likes it first and then does undo like). So first check whether user previously liked the document or not. If user previously liked the document, then get the value of the like for that particular user (true/false) and set it using setLike(like). Also get the number of documents in the collection who have like==true and set it equal to setLikeCount(likeCount).
//=======Get likes from the database ===========================
useEffect(() => {
//check if the user already liked the doc or not (first time or not)
DataBase.collection('posts').doc(postId).collection('postLikes')
.doc(user.uid).get().then((doc) => {
if (doc.exists) {
console.log(doc.data().like)
//set like to value of like from database
setLike(doc.data().like)
} else {
// doc.data() will be undefined in this case
console.log("Not liked");
}
}).catch((error) => {
console.log("Error getting document:", error);
});
//grab the docs which have like=true
DataBase.collection('posts').doc(postId).collection('postLikes').where("like", "==",
true).get()
.then((querySnapshot) => {
setLikeCount((querySnapshot.docs.map(doc =>doc.data())).length)
console.log(likeCount +" likes count")
})
.catch((error) => {
console.log("Error getting documents: ", error);
});
}
//when postId changes or page loads fire the code above
},[postId])
Here is my postLike function. The setLike(!like) is supposed to toggle the previous like value the user has for the document from firebase. I guess this is not updating.
//=============Post likes to the database=======
const postLike = (e) => {
//if already liked i.e. doc exists
console.log(like+"like before")
setLike(!like)
console.log(like+"like after")
like?(setLikeCount(likeCount+1)):(setLikeCount(likeCount-1))
console.log("likeCount"+likeCount)
DataBase.collection('posts').doc(postId).collection('postLikes').doc(user.uid).set(
{
like:like,
username:user.displayName,
timestamp:firebase.firestore.FieldValue.serverTimestamp(),
}
).catch((err)=>{console.log("something wrong happened "+err.message)})
}
return (
<div className="post">
<div className="post__likes">
{/*like button*/}
{
like?
(<Button onClick={postLike} ><FavoriteIcon fontsize="small" cursor="pointer" onClick=.
{postLike} style={{color:'red'}}/></Button> ):
(<Button onClick={postLike} ><FavoriteIcon fontsize="small" cursor="pointer" />
</Button>)
}
<Typography style={{color:'aliceblue'}}>Liked by {likeCount}</Typography>
</div>
</div>
)
}
export default Post
Edit:
Yeah, the useEffect might have been a bad idea. It's getting called on initial load and after like is updated from the server request. I've made a snippet that I think does most of what you want. It's still brittle though because a user can click the like button before we get back the likeCount from the server. You might want to disable the button while the request is pending.
const {
useState,
useEffect
} = React;
function Post({
postId
}) {
//number of likes
const [likeCount, setLikeCount] = useState(5)
//if like=true or not
const [like, setLike] = useState(false);
//=======Get likes from the database ===========================
useEffect(() => {
// This is a call to the server to check if the user already liked the doc or not (first time or not)
setTimeout(() => {
if (true) {
setLike(true);
}
}, 500);
// This is a call to the server to grab the docs which have like=true
setTimeout(() => {
setLikeCount(15);
}, 400);
//when postId changes or page loads fire the code above
}, [postId]);
//=============Post likes to the database=======
const postLike = (e) => {
//if already liked i.e. doc exists
const newLikeValue = !like;
const newLikeCount = like ? likeCount - 1 : likeCount + 1;
setLike(!like);
setLikeCount(newLikeCount);
setLike(newLikeValue);
// Update like value on server here
/*
DataBase
.collection('posts').doc(postId)
.collection('postLikes').doc(user.uid).set({
like: newLikeValue, // <-- Use newLikeValue here
username: user.displayName,
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
}).catch((err) => {
console.log("something wrong happened " + err.message)
})
*/
}
return (
<div className = "post" >
<div className = "post__likes" > { /*like button*/ }
{
like
? (<button onClick={postLike}><div style={{color:'red'}}>Icon here</div></button>)
: (<button onClick={postLike}><div>Icon here</div></button>)
}
<p>Liked by {likeCount}</p>
</div>
</div>
);
}
ReactDOM.render(
<Post postId={5}/>,
document.getElementById("react")
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="react"></div>
Initial answer:
Personally, I would use a local copy as Dave suggested in a comment. But you could also set up another useEffect to listen for changes to like and make your server update inside there.
useEffect(() => {
like?(setLikeCount(likeCount+1)):(setLikeCount(likeCount-1));
DataBase
.collection('posts').doc(postId)
.collection('postLikes').doc(user.uid)
.set(
{
like:like,
username:user.displayName,
timestamp:firebase.firestore.FieldValue.serverTimestamp(),
}
).catch((err)=>{console.log("something wrong happened "+err.message)})
}, [like]);
const updateLike = (e) => {
setLike(!like);
}

When does the callback function inside the useCallback() hook runs exactly?

Here is the code for infinite scroll load
Mainly two components MainComponent and a custom hook component
everytime i entered something on search item it sends the request and display the data to screen and inside main component i am using lastELementRef to set observer Api on that to send the request again when i scrolled at the end .
Not able to understand when does function passed inside useCallBack(()=>{}) runs
to check how many times it runs i did console.log at line no 21 inside MainComponent.
It will be very nice of folks on this community if anybody can explain me when does it runs.
I have googled and watched some Youtube videos on useCallback and all I can come up with is that it gives the function object only when the dependency inside its dependency array changes else on it memoizes the function on each re-render if dependency does not change.?
i am sharing the code here
have used axios to send request.
//MainComponent
import React,{useState,useRef,useCallback} from 'react'
import useBookSearch from './useBookSearch';
export default function MainComponent() {
//these 2 stataes are here because
//we want them to be used in this component only
//meaning we dont want them to be part
//of any custom logic
const[query,setQuery] = useState('');
const[pageNumber,setPageNumber] = useState(1);
const observer = useRef();
const {books,loading,hasMore,error} = useBookSearch(query,pageNumber);
const lastElementRef = useCallback(node=>{
console.log("How many times did i run ?");
if(loading) return ;
if(observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries=>{
if(entries[0].isIntersecting && hasMore){
setPageNumber(prevPage => prevPage + 1);
}
})
if(node) observer.current.observe(node);
console.log(node);
},[loading,hasMore])
const handleSearch=(e)=>{
setQuery(e.target.value);
setPageNumber(1);
}
return (
<div>
<input value={query} type="text" onChange={handleSearch} ></input>
{books.map((book,index) =>{
if(books.length === index + 1)
{
return <div ref={lastElementRef}key={book}>{book}</div>
}
else{
return <div key={book}>{book}</div>
}
})}
{loading && 'Loading.....................'}
{error && 'Error........'}
</div>
)
}
//custom hook component-useBookSearch
import { useEffect,useState } from 'react';
import axios from 'axios'
export default function useBookSearch(query,pageNumber) {
const[loading,setLoading] = useState('true');
const[error,setError] = useState('false');
const[books,setBooks] = useState([]);
const[hasMore,setHasMore] = useState(false);
//second useEffect which clear the books first
//and then make an api request
useEffect(()=>{
setBooks([]);
},[query])
useEffect(()=>{
setLoading(true);
setError(false);
let cancel ;
axios({
method:'GET',
url:'http://openlibrary.org/search.json',
params:{q:query,page:pageNumber},
cancelToken:new axios.CancelToken(c=>cancel=c)
}).then(res=>{
setBooks(prevBooks=>{
return [...new Set([...prevBooks,...res.data.docs.map(b=>b.title)])]
})
setHasMore(res.data.docs.length > 0);
setLoading(false);
console.log(res.data);
}).catch(e=>{
if(axios.isCancel(e)) return
setError(true);
})
return ()=> cancel();
},[query,pageNumber])
return {loading,error,books,hasMore};
}
screenshot of how the code looks when i entered the string test to fetch data
Screenshot of the console window when entering test into input box

How do I prevent empty values in my search bar, in React?

So, I am building a book finder in React using Google Books API.
The user types the name of the book he/she wants to search, in the input field, and the value typed in the input is appended to the end of the API url.
Basically, the API is called every single time the user types something in the input field, because I want display some of the results in a dropdown below the search bar. The problem is that, if the user hits spacebar, which is an empty string, I get a HTTP 400 error, specifically this:
Error: Request failed with status code 400
at createError (createError.js:17)
at settle (settle.js:19)
at XMLHttpRequest.handleLoad (xhr.js:60)
If I call .trim() on the input value, then that just prevents a user from typing anything at all. I'm kind of confused what to do right now. Also, is calling the API everytime the input value changes an expensive operation? This is what I've tried so far:
import React, { Component } from 'react';
import axios from 'axios';
export default class BookSearchForm extends Component {
state = {
searchTerm: ''
};
fetchBooks = () => {
let apiURL = `https://www.googleapis.com/books/v1/volumes`;
axios
.get(`${apiURL}?q=${this.state.searchTerm}`)
.then(res => console.log(res))
.catch(err => console.log(err));
};
onChange = e => {
this.fetchBooks();
this.setState({ searchTerm: e.target.value });
};
render() {
return (
<div className="container">
<form autoComplete="off">
<input
className="search-bar"
type="search"
placeholder="Search for books"
onChange={this.onChange}
value={this.state.searchTerm}
/>
</form>
</div>
);
}
}
You can replace whitespaces using the \s regex.
You may also want to fetch after the state is updated (callback of setState) :
onChange = e => {
this.setState(
{ searchTerm: e.target.value.replace(/\s/g, '') },
() => {
this.fetchBooks();
}
);
};
.replace(/\s/g, '') will replace all whitespaces (including tab, newline etc...) from the string by an empty char (''), making whitespaces from user inputs doing nothing.
You could validate the user input and don't allow empty strings.
Also, is calling the API everytime the input value changes an expensive operation?
Likely yes. You might want to debounce or throttle your requests
You can try below code.
onChange = e => {
let value = e.target.value;
this.fetchBooks(value.trim());
this.setState({ searchTerm: e.target.value });
};
fetchBooks = (str) => {
if (str) {
let apiURL = `https://www.googleapis.com/books/v1/volumes`;
axios
.get(`${apiURL}?q=${str}`)
.then(res => console.log(res))
.catch(err => console.log(err));
}
};
Yes, calling API on every input change is expensive. You should use debouncing.
I think the code should be:
onChange = async (e) => {
await this.setState({ searchTerm: e.target.value });
this.fetchBooks();
};

Stripe Payments with React, autofilling address possible?

I'm using Stripe to handle credit cards, but right now I'm running into the issue where a user has to fill out their shipping/billing every time. Is it possible for me to use some predefined values for these? Right now, I have their shipping/billing saved to their account.
Additionally, I'm also trying to pass the charge amount to server side. I've tried wrapping it in a form and using a hidden input, but it always is undefined.
class Payments extends Component {
render() {
return (
<StripeCheckout
name="My Shop"
description="Item"
amount={this.props.payment*100}
email={this.props.email}
shippingAddress={true}
billingAddress={true}
token={token => this.props.handleToken(token)}
stripeKey={process.env.REACT_APP_STRIPE_KEY}
>
<button>Checkout</button>
</StripeCheckout>
)
}
}
export const handleToken = token => async dispatch => {
const res = await axios.post('/api/stripe', token);
dispatch({ type: FETCH_USER, payload: res.data});
};

Categories

Resources