So this is going to be a bit long, but i will try to produce my issue quite simply. The full code of my component is at the end btw.
I am learning to use firebase authentication and adding and retrival of data from the firebase Firestone in react. I have the authentication set up and this gives me a unique ID that I can use to fetch the content I have added in real-time. The function that listen and fetches the data in the code below is listenForMessages() that is inside my useEffect hook. This function fetches the data updates the state in the content hook which then displays the data in my render. The issue is that right now to fetch the data I have to insert the unique ID (jYAhV0xCtmOEJtOPXHirYXkQtju1) directly into doc() inside this listenForMessages(). This is not ideal as I cannot be hardcoding the unique ID. So I switched things a little bit and passed the unique id I got from my app.js as a prop into this component. Thanks to the below bit of code the unique ID is now passed into the below usestate hook, which should then give me the freedom to use id in doc() in this manner= doc(id).
const [ id, setId ] = useState(props);
useEffect(
() => {
setId(props.uid);
},
[ props ]
);
The issue is that it simply doesn't work :/
When i add id into doc() I am met with this messege- FirebaseError: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: a custom Object object
can anyone help? :/
const [ id, setId ] = useState(props);
useEffect(
() => {
setId(props.uid);
},
[ props ]
);
import React, { useState, useEffect } from 'react';
import fire, { db } from '../Config/firebase';
export default function Home(props) {
function logout() {
fire.auth().signOut();
}
const [ name, setName ] = useState('');
const [ email, setEmail ] = useState('');
const [ Content, setContent ] = useState([]);
const [ id, setId ] = useState(props);
useEffect(
() => {
setId(props.uid);
},
[ props ]
);
const sendData = () => {
db
.collection('users')
.doc(props.uid)
.set({
name,
email,
id
})
.then(function() {
console.log('Document successfully written!');
})
.catch(function(error) {
console.error('Error writing document: ', error);
});
};
const listenForMessages = () => {
db.collection('users').doc('jYAhV0xCtmOEJtOPXHirYXkQtju1').onSnapshot(function(doc) {
const allMessages = [];
allMessages.push(doc.data());
setContent(allMessages);
});
};
useEffect(() => {
listenForMessages();
}, []);
return (
<div>
<h1>I am home</h1>
<button onClick={logout}>Log out</button>
<h1>Enter information</h1>
<form>
<label>Name</label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<label>Email</label>
<input type="text" value={email} onChange={(e) => setEmail(e.target.value)} />
</form>
<button onClick={sendData}>Send data</button>
{Content.map((n) => (
<p key={Math.floor(Math.random() * 10)}>
{n.name} {n.email}
</p>
))}
</div>
);
}
Related
I'm currently converting the logic in my mern (with typescript) project to use React/Tanstack query to learn this tool better.
I want to use useMutation to handle the post request logic from the details inputted in the form, in this login component but can't figure out how to do this. Any tips would be appreciated thanks. Below is the code from my login component
const Login = () => {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const [state, setState] = useContext(UserContext);
const handleSubmit = async (e: { preventDefault: () => void }) => {
e.preventDefault();
let response;
const { data: loginData } = await axios.post("http://localhost:5001/auth/login", {
email,
password,
});
response = loginData;
if (response.errors.length) {
return setErrorMsg(response.errors[0].msg);
}
setState({
data: {
id: response.data.user.id,
email: response.data.user.email,
stripeCustomerId: response.data.user.stripeCustomerId,
},
loading: false,
error: null,
});
localStorage.setItem("token", response.data.token);
axios.defaults.headers.common["authorization"] = `Bearer ${response.data.token}`;
navigate("/dashboard");
};
return (
<div className="login-card">
<div>
<h3>Login</h3>
</div>
<form onSubmit={handleSubmit}>
<div className="login-card-mb">
<label>Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div className="login-card-mb">
<label>Password</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
{errorMsg && <p>{errorMsg}</p>}
<button type="submit">Submit</button>
</form>
</div>
);
};
After setting up your project to use React Query ( Check the docs if you have not). You want to extract your api call to a separate function that takes an object. This object will hold the values you would like to post.
const Login = (dataToPost) => {
let res = await axios.post('url', dataToPost)
return res.data
}
Now that you have that, you can import useMutation from React Query. Once imported you can now use the hook. UseQuery, useMutation both contain a data variable so no need to create state for the data returned from your endpoint. In this example, I'm deconstructing the data and loading state. But most importantly the mutate function. Which allows you to fire off your api call. We add our api call to the hook. I'm renaming the mutate function to doLogin. It's a habit
const {data,isLoading,mutate:doLogin} = useMutation(Login)
Finally we can just call mutate(objectWithValues) wherever you want in your code. The data will initially be null and isLoading will be true once called. To tie it all together. Your handleSubmit could look as follows
const handleSubmit = () => {
e.preventDefault();
doLogin({email,password})
}
You also have the option of running functions on a success or error of the mutation
const {data,isLoading,mutate: doLogin} =
useMutation(Login, {
onError: (err) => console.log("The error",err),
onSuccess:(someStuff)=>console.log("The data being returned",someStuff)
})
I encountered a problem in my chat app.
I works when I post message doc to the messages col but then I'm trying do getDocs back and render them I get an empty array.
I looked through FB docs, and I didn't notice any mistakes on my part. I also read an article where I was advised to use the react-firebase library with useCollectionData with which I had the same result.
const [messages, loading] = useCollectionData(
firestore.collection('messages').orderBy('createdAt')
)
I tried different approaches but nothing seems to work.
import React, { useState, useEffect } from 'react'
import { auth, db, app } from '../../firebase.config'
import { useAuthState } from 'react-firebase-hooks/auth'
import { useCollectionData } from 'react-firebase-hooks/firestore'
import { docs, onSnapshot, query, where, addDoc, collection, serverTimestamp, orderBy, getDocs } from 'firebase/firestore'
import Message from '../Message/Message'
import Spinner from '../Spinner/Spinner'
import './chat.css'
const Chat = () => {
const [user, loading, error] = useAuthState(auth)
const [value, setValue] = useState('')
const [msgs, setMsgs] = useState([])
console.log('msgs>>>>', msgs)
useEffect(() => {
const fetchMsg = async () => {
const messagesRef = collection(db, 'messages')
const q = query(
messagesRef,
orderBy('timestamp', 'desc')
)
const querySnap = await getDocs(q)
let listings = []
querySnap.forEach((doc) => {
return listings.push({
id: doc.id,
data: doc.data(),
})
})
setMsgs(listings)
}
fetchMsg()
}, [])
const sendMessage = async (e) => {
e.preventDefault();
const docRef = await addDoc(collection(db, 'messages'), {
uid: user.uid,
displayName: user.displayName,
photoURL: user.photoURL,
text: value,
createdAt: serverTimestamp()
})
console.log(docRef)
setValue('')
}
if (loading) {
return <Spinner />
}
return (
<>
<div className='ch-wind'>
{msgs.map((msg) => (
<Message key={msg.id} msg={msg} style={{ backgroundColor: user.uid === msg.uid ? '#A32cc4' : '#a1045a' }} />
))}
</div>
<form className="ch-form" onSubmit={sendMessage}>
<textarea
value={value}
className='ch-form-text'
onChange={e => setValue(e.target.value)}
placeholder='Enter your message here'
/>
<button
className='ch-form-btn'
>
Send
</button>
</form>
</>
)
}
export default Chat
By using useEffect() hook, I would assume that you want to get the data realtime. Firestore has a realtime listeners that you can use. You can listen to a document with the onSnapshot() method. An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document. Then, each time the contents change, another call updates the document snapshot. See code below:
useEffect(() => {
const messagesRef = query(collection(db, 'messages'), orderBy('timestamp', 'desc'));
onSnapshot(messagesRef, (snapshot) => {
// Maps the documents and sets them to the `msgs` state.
setMsgs(snapshot.docs.map(doc => ({
id: doc.id,
data: doc.data()
})))
})
}, [])
Also, as pointed out by #CDoe, you should use the same Fieldname which you set from the addDoc method as you can see on the above code.
Then on the rendering, something like this:
{msgs.map((msg) => (
// By setting the `doc.data()` to the object `data`, you should access it by `msg.data.<object_key>`
<Message key={msg.id} msg={msg.data.text} style={{ backgroundColor: user.uid === msg.data.uid ? '#A32cc4' : '#a1045a' }} />
))}
I leave some comments on the code to better understand it.
For more information on realtime updates, you may check out this documentation.
In the query, you're trying to orderBy timestamp. That's not a field you're creating in sendMessage.
When a value you're ordering by doesn't exist on the document, it won't return.
Maybe you meant to orderyBy the createdAt value.
const q = query(
messagesRef,
orderBy('createdAt', 'desc')
)
I am trying to make an API call in useEffect() and want useEffect() to be called everytime a new data is added in the backend.
I made a custom Button(AddUserButton.js) which adds a new user in backend. I am importing this button in the file (ManageUsers.js) where I am trying to display all the users. I just wanted to make an useState to keep track everytime an add button is clicked and make useEffect refresh according to it. For Example:
const [counter, setCounter] = useState(0);
...
const handleAdd = () => {
setCounter(state => (state+1));
};
...
useEffect(() => {
// fetch data here
...
}, [counter]);
...
return(
<Button onClick = {handleAdd}> Add User </Button>
);
But currently because I have two .js files, I am not sure how to make my logic stated above
work in this case
ManageUsers.js
import AddUserButton from "./AddUserButton";
...
export default function ManageShades() {
...
useEffect(() => {
axios
.get("/api/v1/users")
.then(function (response) {
// After a successful add, store the returned devices
setUsers(response.data);
setGetUserFailed(false);
})
.catch(function (error) {
// After a failed add
console.log(error);
setGetUserFailed(true);
});
console.log("Load User useeffect call")
},[]);
return (
<div>
...
<Grid item xs={1}>
<AddUserButton title = "Add User" />
</Grid>
...
</div>
);
}
AddUserButton.js
export default function AddDeviceButton() {
...
return (
<div>
<Button variant="contained" onClick={handleClickOpen}>
Add a device
</Button>
...
</div>
);
}
A common approach is to pass a callback function to your button component that updates the state of the parent component.
import { useState, useEffect } from "react";
const AddUserButton = ({ onClick }) => {
return <button onClick={onClick} />;
};
export default function Test() {
const [updateCount, setUpdateCount] = useState(false);
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count++);
}, [updateCount]);
return (
<div>
<AddUserButton
onClick={() => {
// do something, e.g. send data to your API
// finally, trigger update
setUpdateCount(!updateCount);
}}
/>
</div>
);
}
So it seems like you are trying to let a child update it's parent's state, an easy way to do this is to let the parent provide the child a callback, which will update the parent's state when called.
const parent = ()=>{
const [count, setCount] = useState(0);
const increCallback = ()=>{setCount(count + 1)};
return (<div>
<child callback={increCallback}/>
</div>);
}
const child = (callback)=>{
return (<button onClick={callback}/>);
}
If you were to tell the ManageUsers component to fetch from the back-end right after the AddUser event is fired, you will almost certainly not see the latest user in the response.
Why? It will take some time for the new user request to be received by the back-end, a little longer for proper security rules to be passed, a little longer for it to be formatted, sanitized, and placed in the DB, and a little longer for that update to be available for the API to pull from.
What can we do? If you manage the users in state - which it looks like you do, based on the setUsers(response.data) - then you can add the new user directly to the state variable, which will then have the user appear immediately in the UI. Then the new user data is asynchronously added to the back-end in the background.
How can we do it? It's a really simple flow that looks something like this (based roughly on the component structure you have right now)
function ManageUsers() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('https://api.com')
.then(res => res.json())
.then(res => setUsers(res));
.catch(err => console.error(err));
}, [setUsers]);
const handleAdd = ({ name, phone, dob }) => {
const newUser = {
name,
phone,
dob
};
setUsers([...users, newUser]);
};
return (
<div>
<UserList data={users} />
<AddUser add={handleAdd} />
</div>
);
}
// ...
function AddUser({ add }) {
const [userForm, setUserForm] = useState({ name: "", phone: "", dob: "" });
return (
// controlled form fields
<button onClick={() => add(userForm)}>Submit</button>
);
}
// ...
function UserList({ data }) {
return (
<>
{data.map(user =>
<p>{user.name></p>
}
</>
);
}
Once the user adds a new user with the "Submit" button, it passes the new user to the "add" function which has been passed down as a prop. Then the user is appended to the users array of the ManageUsers component, instantly populating the latest user data in the UserList component. If we wait for a new fetch request to come through, this will add a delay, and the newest user we just added will not likely come back with the response.
This is my Json file which I created in my app.
export const Data = [
{
id: 1,
title: "Tilte 1",
description: "Decription 1 Data",
},
{
id: 2,
title: "Tilte 2",
description: "Decription 2 Data",
}
];
This is my main file from where I navigate it. I use json file to display all the records on page. When I click on selected item it will get its id and navigate to another page, where i can get the data of selected item coming from json.
import React from "react";
import { Data } from "./JSON"
import { useNavigate } from 'react-router-dom'
const Home = () => {
let naviagte = useNavigate();
return (
<>
{Data.map((data, key) => {
return (
<div class="card" >
<div class="card-body">
<h5 class="card-title" key={key.id}>{data.title}</h5>
<p class="card-text">{data.description}</p>
<button onClick={() => naviagte(`/service/${data.id}`)}>{data.title} </button>
</div>
</div>
);
})}
</>
)
}
export default Home;
When I navigate to another page where I want to display all data regarding the selected id. It shows only id not all data.
import React, {useState, useEffect} from "react";
import { Data } from "../home/JSON"
import { useParams } from "react-router-dom";
const Service = () => {
const { id } = useParams();
const [data, setData] =useState('');
console.log("check", data);
useEffect(() => {
setData (Data.map((_data) => _data.id === id ))
}, [id])
return(
<>
{id}
{data.title}
{data.description}
</>
)
}
export default Service;
Please guide me what I miss here. Thanks in Advance
Since you are importing the data in both places you just need to find the data by the id property instead of mapping it to booleans. Keep in mind that your id property is a number but the id route param will be a string, so you will need to convert them to a compatible type for the strict equality (===) check.
Example:
useEffect(() => {
setData(Data.find((_data) => String(_data.id) === id));
}, [id]);
Since data is treated as an object in the render return you'll want to insure you maintain a valid state invariant. Update the initial data state to be an object, and check that Array.prototype.find returned a defined object from the Data array before updating state.
const Service = () => {
const { id } = useParams();
const [data, setData] = useState({});
console.log("check", data);
useEffect(() => {
const data = Data.find((_data) => String(_data.id) === id);
if (data) {
setData(data);
}
}, [id]);
return (
<>
{id}
{data.title}
{data.description}
</>
);
};
I am having a strange issue whilst trying to implement an auto save feature to capture my controlled inputs and save them to sessionStorage().
I have an auto-save function that runs every 30 seconds. I create an object from my input values, and then save those into sessionStorage(). I run a check to see if my created object of input values matches the currently stored object. If the new object is different, I replace the current object in sessionStorage with this new object. This seems pretty straight forward to me.
What is happening, is that I am watching the sessionStorage update one character at a time, much like how the controlled inputs I am using work when setting their values from the onChange() function. Once the object is updated fully with what I typed, it resets back to being blank.
I will show an example of the described issue with the sessionStorage below the code examples.
Here is my AddPost component, that contains the 'add post' form and the auto-save function for now:
import React, { useState, useEffect } from 'react';
//Styles
import {
AddPostContainer,
AddPostInfoInput,
AddPostInfoLabel,
AddPostTextArea,
PostOptionWrapper,
PostOptionGroup,
AddPostBtn
} from './styles';
//Components
import LivePreview from './LivePreview/LivePreview';
import Icon from '../../../Icons/Icon';
const AddPost = props => {
const [htmlString, setHtmlString] = useState('');
const [title, setTitle] = useState('');
const [postBody, setPostBody] = useState('');
const [author, setAuthor] = useState('');
const [tags, setTags] = useState('');
const [featuredImage, setFeaturedImage] = useState('');
const autoSave = async () => {
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
try {
await window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(autoSaveObject)
);
} catch (e) {
return;
}
};
setInterval(() => {
const currentSave = window.sessionStorage.getItem('add_post_auto_save');
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
if (currentSave === JSON.stringify(autoSaveObject)) {
return;
} else {
autoSave();
}
}, 10000);
return (
<AddPostContainer>
<AddPostInfoLabel htmlFor="title">Title</AddPostInfoLabel>
<AddPostInfoInput
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="enter post title"
id="title"
/>
<AddPostTextArea
inputwidth="100%"
height="400px"
value={postBody}
onChange={e => {
setHtmlString(e.target.value);
setPostBody(e.target.value);
}}
/>
<AddPostInfoLabel htmlFor="postbody">Live Preview:</AddPostInfoLabel>
<LivePreview id="postbody" htmlstring={htmlString} />
<PostOptionWrapper>
<PostOptionGroup width="33%">
<AddPostInfoLabel htmlFor="author">Author:</AddPostInfoLabel>
<AddPostInfoInput
type="text"
value={author}
onChange={e => setAuthor(e.target.value)}
placeholder="enter author's name"
id="author"
inputwidth="60%"
/>
</PostOptionGroup>
<PostOptionGroup width="33%">
<AddPostInfoLabel htmlFor="tags">Tags:</AddPostInfoLabel>
<AddPostInfoInput
type="text"
placeholder="enter tags separated by ,"
value={tags}
onChange={e => setTags(e.target.value)}
id="tags"
inputwidth="60%"
/>
</PostOptionGroup>
<PostOptionGroup width="33%">
<AddPostInfoLabel htmlFor="featuredImage">
Feat. Image:
</AddPostInfoLabel>
<AddPostInfoInput
type="text"
placeholder="enter image url"
value={featuredImage}
onChange={e => setFeaturedImage(e.target.value)}
id="featuredImage"
inputwidth="60%"
/>
</PostOptionGroup>
</PostOptionWrapper>
<AddPostBtn type="button">
<Icon icon={['far', 'plus-square']} size="lg" pSize="1em">
<p>Add Post</p>
</Icon>
</AddPostBtn>
</AddPostContainer>
);
};
export default AddPost;
Here is the auto-save function on it's own:
const autoSave = async () => {
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
try {
await window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(autoSaveObject)
);
} catch (e) {
return;
}
};
setInterval(() => {
const currentSave = window.sessionStorage.getItem('add_post_auto_save');
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
if (currentSave === JSON.stringify(autoSaveObject)) {
return;
} else {
autoSave();
}
}, 30000);
This auto-save function runs once every 30 seconds, and then replaces what is in sessionStorage with what the current values for the input fields are. I use JSON.stringify() on the objects to compare them. (note: the obj from sessionStorage is already stringified.). If that match returns true, nothing is saved as the current input values are also what is saved. Else, it saves the new object into sessionStorage.
My thought was that I needed to make autoSave() async, as updating both session and local storage is asynchronous and doesn't happen immediately (although pretty close to it). That didn't work.
Here is what the sessionStorage object is when it tries to save:
It may be a lower quality, but you can see how it is updating the 'title' property. It behaves like a controlled input, character by character being added to the value.
Can someone point out what is going on here? I am at a loss on this one. Thanks in advance!
The main issue you have is that setInterval is being called on every render and the created intervals are never being cleared.
That means that if you type 10 characters into a text input, then you'll have 10 intervals firing every 10 seconds.
To avoid this using hooks you need to wrap your setInterval call with useEffect and return a deregistration function that will clear the interval when re-rendering (or on unmount). See the Effects with Cleanup documentation.
Here is the minimal updated version using useEffect:
const autoSave = (postData) => {
try {
window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(postData)
);
} catch (e) {
}
};
useEffect(() => {
const intervalId = setInterval(() => {
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
const currentSave = window.sessionStorage.getItem('add_post_auto_save');
if (currentSave === JSON.stringify(autoSaveObject)) {
return;
} else {
autoSave(autoSaveObject);
}
}, 10000);
return () => {clearInterval(intervalId)};
});
If you don't want to clear and recreate the interval on every render you can conditionally control when the effect is triggered. This is covered in the useEffect Conditionally firing an effect documentation.
The main thing is that you'll need to pass in every dependency of the useEffect, which in your case is all of your state variables.
That would look like this - and you would need to make sure that you include every state variable that is used inside the useEffect hook. If you forget to list any of the variables then you would be setting stale data.
useEffect(() => {
//... your behaviour here
}, [title, author, tags, featuredImage, postBody]);
Further reading:
Here's a blog post from Dan Abramov that delves more into hooks, using setInterval as an example: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Also, you don't need to have five or six separate useState calls if it makes sense for the post data to always be "bundled" together.
You can store the post data as an object in useState instead of managing them all separately:
const [postData, setPostData] = useState({
htmlString: '',
title: '',
author: '',
tags: '',
featuredImage: '',
postBody: '',
});
function updateData(value, key) {
setPostData((prevData) => {
return {
...prevData,
[key]: value
};
});
}
const autoSave = (postData) => {
try {
window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(postData)
);
} catch (e) {}
};
useEffect(() => {
const intervalId = setInterval(() => {
const currentSave = window.sessionStorage.getItem('add_post_auto_save');
if (currentSave === JSON.stringify(postData)) {
return;
} else {
autoSave(postData);
}
}, 10000);
return () => {
clearInterval(intervalId)
};
}, [postData]);
// jsx:
<AddPostInfoInput
type="text"
value={postData.title}
onChange={e => updateData(e.target.value, 'title')}
placeholder="enter post title"
id="title"
/>
Good question and nice formatting too. Your problem is happening because you are creating a new interval each time your component updates, that is a lot since you are using controlled inputs. I guess you can get what you want changing your component to a class component and create the setInterval on the componentDidMount method. And don't forget to clean the interval on the component unmounting, here is an example:
import ReactDOM from "react-dom";
import React from "react";
class Todo extends React.Component {
state = {
text1: "",
text2: "",
interval: null
};
componentDidMount() {
this.setState({
interval: setInterval(() => {
const { text1, text2 } = this.state;
const autoSaveObject = {
text1,
text2
};
console.log(JSON.stringify(autoSaveObject));
}, 3000)
});
}
componentWillUnmount() {
clearInterval(this.state.interval);
}
render() {
return (
<div>
<h1>TODO LIST</h1>
<form>
<input
value={this.state.text1}
onChange={e => this.setState({ text1: e.target.value })}
/>
<input
value={this.state.text2}
onChange={e => this.setState({ text2: e.target.value })}
/>
</form>
</div>
);
}
}
ReactDOM.render(<Todo />, document.getElementById("root"));
try to useEffect rather the setInterval
as this shape
this code will make update setItem in session when the state element is change
you can get the getItem when any time you need
const [state,setState]=useState({
htmlString:"",
title:"",
postBody:"",
author:"",
tags:"",
featuredImage:""
})
useEffect(async () => {
const autoSaveObject = { ...state };
try {
await window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(autoSaveObject));
} catch (e) {
return console.log(e)
}
}, [state])