I am currently working on a chat application and for some reason every time I pass in my array of messages as a prop to another component it passes in a number to the component instead of the message object. I have tried a lot of different methods of passing it in regarding using multiple components etc but it seems to still be passing in the number of elements for some reason. Any help is appreciated... code is below
Component receiving the props
import React, { useEffect } from 'react'
import Message from '../../Message/Message'
function Messages({ messages }) {
useEffect(() => {
console.log(messages)
}, [messages])
return (
<div>
test
</div>
)
}
export default Messages
// Import React dependencies.
import React, { useEffect, useState, } from "react";
// Import React dependencies.
import io from 'socket.io-client'
import axios from 'axios'
import Messages from './Messages/Messages'
import uuid from 'react-uuid'
import { Redirect } from 'react-router-dom'
// Import the Slate components and React plugin.
const ENDPOINT = 'http://localhost:5000/'
export const socket = io.connect(ENDPOINT)
const LiveChatFunction = ({ group_id }) => {
// Add the initial value when setting up our state.
const [message, setValue] = useState("")
const [user, setUser] = useState("")
const [groupId, setGroup] = useState('')
const [messages, setMessages] = useState([])
const [toLogin, userAuth] = useState(false)
useEffect(() => {
setGroup(group_id)
axios.post('http://localhost:5000/api/users/refresh_token', null, { withCredentials: true }).then(data => {
if (!data.data.accessToken) {
userAuth(true)
}
})
axios.get('http://localhost:5000/api/users/userInfo', { withCredentials: true }).then(data => {
setUser(data.data.user)
})
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
axios.get(`http://localhost:5000/live/${group_id}`).then(x => {
console.log(x.data)
})
}, [group_id, messages])
function setClick() {
const data = {
messageId: uuid(),
user,
groupId,
message
}
socket.emit('message', data)
}
if (toLogin) {
return (
<Redirect to="/login" />
)
}
return (
<div>
<input placeholder="message" type="text" onChange={value => {
setValue(value.target.value)
socket.emit('typing-message', { username: user, time: new Date() })
}} />
<button onClick={setClick}>Submit</button>
<Messages messages={messages} />
</div>
)
}
export default LiveChatFunction;
I have added some comments of what I think you can change:
useEffect(() => {
const recieveFunction = (data) => {
//using callback so no dependency on messages
setMessages((messages) => messages.push(data));
};
async function init() {
//next line is pointless, this runs when group_id
// has changed so something must have set it
// setGroup(group_id);
await axios //not sure if this should be done before listening to socket
.post(
'http://localhost:5000/api/users/refresh_token',
null,
{ withCredentials: true }
)
.then((data) => {
if (!data.data.accessToken) {
userAuth(true);
}
});
await axios
.get('http://localhost:5000/api/users/userInfo', {
withCredentials: true,
})
.then((data) => {
setUser(data.data.user);
});
//start listening to socket after user info is set
socket.on(`message-${group_id}`, recieveFunction);
axios
.get(`http://localhost:5000/live/${group_id}`)
.then((x) => {
console.log(x.data);
});
}
init();
//returning cleanup function, guessing socket.off exists
return () =>
socket.off(`message-${group_id}`, recieveFunction);
}, [group_id]); //no messages dependencies
console.log('messages are now:',messages);
If messages is still not set correctly then can you log it
So I think I found your problem:
In your useEffect hook, you're setting messages to the wrong thing.
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
An example:
const m = [].push();
console.log(m);
// m === 0
const n = [].push({});
console.log(n);
// n === 1
As you can see this is the index.
So what you need is:
socket.on(`message-${group_id}`, data => {
messages.push(data);
setMessages(messages);
});
This will set messages to the array of messages.
Related
Introducing The Problem
I am beginner ReactJS learner developing a simple weather app using OpenWeather API. The app is designed to fetch data from two components: one that returns the current weather of the user input and another one that returns the weather forecast for the next 5 days.
When the city name is typed down into the input field, the following message appears on the console:
GET https://api.openweathermap.org/data/2.5/weather?q=undefined&units=metric&appid=${Api.key} 400 (Bad Request)
I do not know how to pass the data from Search Component into App Component. Seriously, I have tried a lot of alternatives but they have been unsuccessful. There are commented lines of code to show my last try so far.
(ignore ForecastWeather because this component is empty)
I know that all of you are quite busy folks, but I would appreciate the help in a respectful way. Even suggestions about what I have to study (e.g. callBack) are welcome. I've tried this already:
https://stackoverflow.com/questions/56943427/whether-to-save-form-input-to-state-in-onchange-or-onsubmit-in-react
https://sebhastian.com/react-onchange/
The code is forward below:
App.js
import React, { useState } from "react";
import { Api } from "./Api";
import {
Search,
CurrentWeather,
ForecastWeather,
Footer,
} from "./components/index";
import "./App.css";
function App() {
const [getCity, setGetCity] = useState();
const [weatherData, setWeatherData] = useState(null);
const [forecastData, setForecastData] = useState(null);
const handleSearchLocation = (dataSearch) => {
const weatherDataFetch = fetch(
`${Api.url}/weather?q=${getCity}&units=metric&appid=${Api.key}`
);
const forecastDataFetch = fetch(
`${Api.url}/forecast?q=${getCity}&units=metric&appid=${Api.key}`
);
Promise.all([weatherDataFetch, forecastDataFetch])
.then(async (response) => {
const weatherResponse = await response[0].json();
const forecastResponse = await response[1].json();
setGetCity(dataSearch);
setWeatherData(weatherResponse);
setForecastData(forecastResponse);
})
.catch(console.log);
};
return (
<div className="App">
<Search
searchResultData={handleSearchLocation}
textPlaceholder="Search for a place..."
/>
{weatherData && <CurrentWeather resultData={weatherData} />}
<ForecastWeather resultData={forecastData} />
<Footer />
</div>
);
}
export default App;
Search.jsx
import React, { useState } from "react";
function Search({ textPlaceholder, searchResultData }) {
const [searchCity, setSearchCity] = useState("");
//const handlerOnChange = ( event, dataSearch ) => {
//setSearchCity(event.target.value);
//setSearchCity(dataSearch);
//searchResultData(dataSearch);
//};
return (
<div className="componentsBoxLayout">
<input
value={searchCity}
//onChange={handlerOnChange}
onChange={(event) => setSearchCity(event.target.value)}
onKeyDown={(event) => event.key === "Enter" && searchResultData(event)}
placeholder={textPlaceholder}
/>
</div>
);
}
export default Search;
CurrentWeather.jsx
import React from "react";
function CurrentWeather({ resultData }) {
return (
<div className="componentsBoxLayout">
<p>{resultData.name}</p>
</div>
);
}
export default CurrentWeather;
ForecastWeather.jsx (empty)
import React from 'react';
function ForecastWeather() {
return (
<div className="componentsBoxLayout">ForecastWeather</div>
)
}
export default ForecastWeather;
Api.js
const Api = {
url: "https://api.openweathermap.org/data/2.5",
key: "etcetc",
img: "https://openweathermap.org/img/wn",
};
export { Api };
Yippee-ki-yay
You can not use getCity in this function:
const handleSearchLocation = (dataSearch) => {
const weatherDataFetch = fetch(
`${Api.url}/weather?q=${getCity}&units=metric&appid=${Api.key}`
);
const forecastDataFetch = fetch(
`${Api.url}/forecast?q=${getCity}&units=metric&appid=${Api.key}`
);
Promise.all([weatherDataFetch, forecastDataFetch])
.then(async (response) => {
const weatherResponse = await response[0].json();
const forecastResponse = await response[1].json();
setGetCity(dataSearch);
setWeatherData(weatherResponse);
setForecastData(forecastResponse);
})
.catch(console.log);
};
getCity is defined on that function so it does not exist when you try to use it, unless you need getCity later for another component I would delete it becuase is redundant and do this:
const handleSearchLocation = (dataSearch) => {
const weatherDataFetch = fetch(
`${Api.url}/weather?q=${dataSearch}&units=metric&appid=${Api.key}`
);
const forecastDataFetch = fetch(
`${Api.url}/forecast?q=${dataSearch}&units=metric&appid=${Api.key}`
);
Promise.all([weatherDataFetch, forecastDataFetch])
.then(async (response) => {
const weatherResponse = await response[0].json();
const forecastResponse = await response[1].json();
setWeatherData(weatherResponse);
setForecastData(forecastResponse);
})
.catch(console.log);
};
When you run searchResultData on the search component you send the city you are looking for. Remember that useState will trigger a re-render but a function that is already running before that will never get the new value of the state if the state changes
I'm trying to add functionality to fetch the next 4 comments but when I click on the Load more comments button, it throws this error message.
Uncaught FirebaseError: Invalid query. You must not call startAt() or startAfter() before calling orderBy().
Anyone, please help me with this.
SolutionComments.js
import React, { useState } from "react"
import { useParams } from "react-router-dom"
import { useCollection } from "../../hooks/useCollection"
import Comment from "./Comment"
import CommentForm from "./CommentForm"
const SolutionComments = () => {
const [activeComment, setActiveComment] = useState(null)
const { id } = useParams()
const { documents, fetchMore } = useCollection(`solutions/${id}/comments`, null, 4, [
"createdAt",
"desc",
])
const fetchMoreComments = () => {
fetchMore(documents[documents.length - 1])
}
return (
<div className="mt-10">
<CommentForm docID={id} />
<div>
{documents &&
documents.map((comment) => (
<Comment
key={comment.id}
comment={comment}
replies={comment.replies}
activeComment={activeComment}
setActiveComment={setActiveComment}
/>
))}
</div>
<button onClick={fetchMoreComments} className="text-white bg-purple-500">
Load More Comments!
</button>
</div>
)
}
export default SolutionComments
useCollection hook:
import { useEffect, useRef, useState } from "react"
// firebase import
import {
collection,
limit,
onSnapshot,
orderBy,
query,
startAfter,
where,
} from "firebase/firestore"
import { db } from "../firebase/config"
export const useCollection = (c, _q, _l, _o) => {
const [documents, setDocuments] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
// if we don't use a ref --> infinite loop in useEffect
// _query is an array and is "different" on every function call
const q = useRef(_q).current
const o = useRef(_o).current
let ref = collection(db, c)
useEffect(() => {
if (q) {
ref = query(ref, where(...q))
}
if (o) {
ref = query(ref, orderBy(...o))
}
if (_l) {
ref = query(ref, limit(_l))
}
const unsubscribe = onSnapshot(ref, (snapshot) => {
const results = []
snapshot.docs.forEach(
(doc) => {
results.push({ ...doc.data(), id: doc.id })
},
(error) => {
console.log(error)
setError("could not fetch the data")
}
)
// update state
setDocuments(results)
setIsLoading(false)
setError(null)
})
// unsubscribe on unmount
return unsubscribe
}, [c, q, _l, o, isLoading])
// I'm using this function to fetch next 4 comments including the previous ones.
const fetchMore = (doc) => {
ref = query(ref, orderBy(...o), startAfter(doc), limit(_l))
onSnapshot(ref, (snapshot) => {
const results = []
snapshot.docs.forEach(
(doc) => {
results.push({ ...doc.data(), id: doc.id })
},
(error) => {
console.log(error)
setError("could not fetch the data")
}
)
// update state
console.log(results)
setDocuments([...documents, results])
setIsLoading(false)
setError(null)
})
}
return { documents, fetchMore, error, isLoading }
}
You are using onSnapshot() to start your query. Since you are doing paging, it is likely you want to use getDocs() instead.
At the very least, you want to be sure to shut down your previous listener for onSnapshot() before starting a new one. But it is rarely all that useful/correct to use an onSnapshot() with paging. Things get hairy-complicated under that approach.
I want my React.js page to reload after hitting the submit button. This is because I put new entries into my database and once submitting was successful, I want to request new data from the database and display them.
import React, {useEffect, useState} from 'react';
import axios from "axios";
const Questionnaire = () => {
const [questions, setQuestions] = useState({questions: []});
const [updated, setUpdated] = useState(false); // approach 1
// let updated = false // approach 2
const onSubmit = async (answeredQuestions) => {
let data = Object.values(answeredQuestions)
axios.put('http://localhost:8000/questionnaire/', data)
.then(response => {
// setUpdated(false); // approach 1
// updated = !updated; // approach 2
}
);
};
useEffect( () => {
axios.get('http://localhost:8000/questionnaire/', {})
.then((response) => {
setQuestions({"questions": response.data});
setUpdated(true); // approach 1
});
}, [updated]);
return (
<>
<Questions questions={questions} onSubmit={onSubmit} />
</>
);
}
export default Questionnaire;
I want the useEffect() to be executed immediately after getting the response from axios.put() so that the new questions can be requested and displayed to the user.
I tried out two approaches, but either axios.get() was executed twice or the re-render didn't work properly.
I appreciate your support!
Use location.reload(); after put/post request is finished as below
import React, {useEffect, useState} from 'react';
import axios from "axios";
const Questionnaire = () => {
const [questions, setQuestions] = useState({questions: []});
const [updated, setUpdated] = useState(false); // approach 1
// let updated = false // approach 2
const onSubmit = async (answeredQuestions) => {
let data = Object.values(answeredQuestions)
axios.put('http://localhost:8000/questionnaire/', data)
.then(response => {
//The below line will force browser refresh
location.reload();
}
);
};
....
//the rest of the codes
Updated answer.
It seems that your logic for approach 1 is not completely correct. Try this instead
import React, {useEffect, useState} from 'react';
import axios from "axios";
const Questionnaire = () => {
const [questions, setQuestions] = useState({questions: []});
const [updated, setUpdated] = useState(true); // Set to true to trigger get on first render
const onSubmit = async (answeredQuestions) => {
let data = Object.values(answeredQuestions)
axios.put('http://localhost:8000/questionnaire/', data)
.then(response => {
setUpdated(true); // approach 1
}
);
};
useEffect( () => {
if (updated) {
axios.get('http://localhost:8000/questionnaire/', {})
.then((response) => {
setQuestions({"questions": response.data});
setUpdated(false); // approach 1
});
}
}, [updated]);
return (
<>
<Questions questions={questions} onSubmit={onSubmit} />
</>
);
}
export default Questionnaire;
Old answer:
Is there any reason why you need to get data in a useEffect? Or could you simply do the get-request whenever the put is resolved?
import React, {useEffect, useState} from 'react';
import axios from "axios";
const Questionnaire = () => {
const [questions, setQuestions] = useState({questions: []});
const onSubmit = async (answeredQuestions) => {
let data = Object.values(answeredQuestions)
axios.put('http://localhost:8000/questionnaire/', data)
.then(response => {
axios.get('http://localhost:8000/questionnaire/', {})
.then((response) => {
setQuestions({"questions": response.data});
});
}
);
};
return (
<>
<Questions questions={questions} onSubmit={onSubmit} />
</>
);
}
export default Questionnaire;
(Or do it with async/await since you are declaring onSubmit to be an async function)
I'm learning ReactTs and I'm coding a simple two components app to Upload a file and with another request obtain upload progress, but I can't receive the progress data from request. I think useEffect never execute the axios get request. How Can execute a lot of times the get request to obtain 0-100 progress? Thanks for your help!
Principal Component:
import axios from 'axios';
import React, { Fragment, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { transactionFileAction, uploadFileAction } from '../../actions/uploadFileAction';
import { RootStore } from '../../store';
import ProgressBar from '../progressBar/ProgressBar';
const UploadFile: React.FC = () => {
const dispatch = useDispatch();
const uploadFileStore = useSelector((state: RootStore) => state.uploadFile);
const [file, setFile] = useState<FormData>();
const [uploadProgress, setUploadProgress] = useState<number | undefined>(0);
const [uploadStatus, setUploadStatus] = useState<boolean>(false);
useEffect(() => {
if (uploadStatus) setUploadStatus(false);
}, [uploadStatus, uploadFileStore]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const formData = new FormData();
formData.append("file", e.target.files[0]);
setFile(formData);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
dispatch(uploadFileAction(file));
setUploadStatus(true);
};
return (
<Fragment>
<form onSubmit={(e) => handleSubmit(e)}>
<label>Notes</label>
<input
type="file"
name="file"
accept=".pdf,.doc,.docx,.xls,.xlsx"
onChange={(e) => handleChange(e)}
/>
<button type="submit">Agregar</button>
{uploadFileStore.UploadFileResponse?.message ? (
<p>{uploadFileStore.UploadFileResponse?.message}</p>
) : null}
</form>
{uploadFileStore.UploadFileResponse?.transactionId ? (
<ProgressBar
transactionId={uploadFileStore.UploadFileResponse?.transactionId}
/>
) : null}
</Fragment>
);
};
export default UploadFile;
Upload Progress Component:
import axios from 'axios';
import React, { useEffect, useState } from 'react';
interface Props {
transactionId: string | undefined;
}
const ProgressBar: React.FC<Props> = ({ transactionId }) => {
const [uploadProgress, setUploadProgress] = useState<number | undefined>(0);
console.log("entra");
useEffect(() => {
axios
.get(`http://localhost:3000/file_parser/${transactionId}`)
.then((response) => {
setUploadProgress(response.data.progress);
});
}, [transactionId, uploadProgress]);
return (
<div>
<p>{transactionId}</p>
<p>Progreso: {uploadProgress && uploadProgress}</p>
</div>
);
};
export default ProgressBar;
Since you need to make a request and this will not trigger re-render else one time since transaction will only call one time, then you can do that via use time interval for request to progress every 1 second for example...
ex:
let us has an useInterval custom hook:
import React, { useEffect, useRef } from 'react';
const useInterval = (callback, delay) => {
const savedCallback = useRef();
// Remember the latest callback.
savedCallback.current = callback;
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
export default useInterval;
Then in my component we can call it:
useInterval(() => {
if(!transactionId) return;
axios
.get(`http://localhost:3000/file_parser/${transactionId}`)
.then((response) => {
setUploadProgress(response.data.progress);
});
}, 1000);
Another Option:
Base on result from api, we keep check if the progress response is 100 or not, if not then trigger it again...
for example:
const [forceUpdate, setForceUpdate] = useState(null);
useEffect(() => {
axios
.get(`http://localhost:3000/file_parser/${transactionId}`)
.then((response) => {
setUploadProgress(response.data.progress);
if(response.data.progress !== 100){
setForceUpdate(new Date());
}
});
}, [transactionId, forceUpdate]);
Another option
its work base on events nested of api request or Socket or any option...
I have a react application for which I'm trying to show the average salary for each employee that I am getting from my data.json file. I am able to output all the salaries but unsure of how to get the average of all the salaries on the screen. I'm pretty new to React so any help would be greatly appreciated.
import React,{useState,useEffect} from 'react';
import './App.css';
function App() {
const [data,setData]=useState([]);
const getData=()=>{
fetch('data.json'
,{
headers : {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}
)
.then(function(response){
// console.log(response)
return response.json();
})
.then(function(myJson) {
setData(myJson)
});
}
useEffect(()=>{
getData()
},[])
const avg = () => {
return console.log(data);
}
return (
<div className="App">
{data && data.length>0 && data.map((item)=><p>{item.Salary}</p>)}
</div>
);
}
export default App;
You can use the useMemo hook for this. It could look sth like this:
const avg = useMemo(() => {
if (data.length === 0) {
return 0;
}
return data.reduce((sum, item) => sum + item.Salary, 0) / data.length;
}, [data])
The memo hook receives a dependency array. In this case, it is [data]. Whenever a value in the dependency array changes, the useMemo hook is recomputed and the value is accessible via the constant.
You don't want to duplicate all of that code every time you need a bit of JSON in your components. You can begin by writing a generic useAsync custom hook -
// hooks.js
import { useState, useEffect } from React
const useAsync = (runAsync, deps = []) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
export { useAsync }
You're excited so you start using it right away -
// MyComponent.js
import { useAsync } from "./hooks.js"
const MyComponent = () => {
const { loading, error, result:items } =
useAsync(_ => {
axios.get("path/to/json")
.then(res => res.json())
}, ...)
// ...
}
export default MyComponent
But stop there. Write more useful hooks when you need them -
// hooks.js (continued)
const useAysnc = //...
const fetchJson = (url) =>
fetch(url, {
headers : {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then(r => r.json())
const useJson = (url) =>
useAsync(fetchJson, [url])
export { useAsync, useJson }
Now it's dead-simple to fetch JSON in any component. Reusable generic functions reduces the chance you introduce bugs to your own code. And you get all the possible states: loading, error, or result -
// MyComponent.js
import { useJson } from "./hooks.js"
const MyComponent = () => {
const { loading, error, result:items } =
useJson("path/to/json")
if (loading)
return <p>Loading...</p>
if (error)
return <p>Error: {error.message}</p>
return <div><Items items={items} /></div> // example use of `items`
}
export default MyComponent
Caclculating average is a matter of using another generic function, sum -
// stats.js
const average = a =>
sum(a) / a.length
const sum = a =>
a.reduce((s, v) => s + v, 0)
export { average, sum }
Noticing a pattern? Isolate behaviour and write reusable functions. Import them where needed. Here's how we use average in your component -
// MyComponent.js
import { average } from "./stats.js"
import { useJson } from "./hooks.js"
const MyComponent = () => {
// useJson...
<div>Average: {average(items.map(v => v.Salary))}</div>
}
export default MyComponent
You need to find the sum of all salaries and then find the average of it.
let salaries=[2,3,8,1];
let sum=0;
for(let i=0;i<salaries.length;i++){
sum=sum+salaries[i];
}
average=sum/salaries.length;
console.log(average);