Problem with dynamically updating a React Component within a chat project - reactjs

In my chat application, I am having trouble displaying user messages after send button is pressed.
I receive all the message data from an API and store it inside a state. Here is the relevant part.
I simplified the code because I think the problem is in the logic of my code and there are no syntax errors.
const Chat = (props) => {
const [selectedRoom, setSelectedRoom] = useState({})
const [roomMessages, setRoomMessages] = useState([])
let roomMessagesData = []
function getUserRoomMessages() {
fetch("url").then((response) => {
return response.json();
}).then((requestData) => {
setRoomMessages(requestData.message_list)
});
};
function sendMessage(message) {
fetch("url").then((response) => {
return response.json();
}).then((requestData) => {
getUserRoomMessages()
});
}
function handleSendMessageButton() {
let currentMsg = document.querySelector("#textBox").value;
if (currentMsg != "") {
sendMessage(currentMsg)
roomMessagesData.push(
<div className="message receiver">{currentMsg}</div>
)
}
document.querySelector("#textBox").value = "";
}
useEffect(() => {
getUserRoomMessages()
}, [selectedRoom])
// Note that:
// typeof roomMessages !== 'undefined' will always be true,
// since initially, roomMessages = [], it is defined.
// Instead just check if roomMessages is not empty, to avoid this operation.
if (roomMessages.length > 0) {
const sortedMessages = roomMessages.sort(
(a,b)=> a.message_id > b.message_id ? 1 : -1
);
for (let i = 0; i<sortedMessages.length; i++) {
if (sortedMessages[i].creator_id == user_id) {
roomMessagesData.push(
<div className="message receiver">
{sortedMessages[i].message_text}
</div>
);
} else {
roomMessagesData.push(
<div className="message sender">
{sortedMessages[i].message_text}
</div>
);
}
};
}
return (
<div>
//some code
<div className="chatBox">{roomMessagesData}</div>
<div><input type="text" id="textBox"/></div>
<div>
<input
id="btn"
type="button"
value="Send message"
onClick={handleSendMessageButton}
/>
</div>
</div>
)
}
In this current logic, when I press send message button, the message is sent to the API successfully, but not displayed. Only after I refresh the page new message appear.
All of the calls to API are successful.
My idea is that when a user sends a new message I retrieve a newly updated list of messages from API which then should be re-rendered by reactjs. Or I want push a new element to roomMessagesData so that after it changes reactjs should reload its elements.
The question is what is the problem in my code and what is it that I have do to achieve what I described above.

Quick review of the steps:
So handleSendMessageButton triggers the sendMessage function which does a POST.
And then sendMessage triggers a GET message using getUserRoomMessages.
Within getUserRoomMessages, you trigger state update using setRoomMessages.
And the rendered child {roomMessagesData} is dependant on the changes within roomMessages state.
Based on the above, the issue to be fixed is here:
// ONLY re-renders if selectedRoom changes
useEffect(() => { getUserRoomMessages() }, [selectedRoom]);
Change the above to:
// re-render if roomMessages changes,
// and the message should display automatically
useEffect(() => { getUserRoomMessages() }, [roomMessages]);
Remember, that useEffect() will only run again if certain "dependant" states change.
UPDATE
Despite suggesting the above, notice that it will create infinite loop because - getUserRoomMessages makes a API call and then updates roomMessages state which the useEffect depends on to re-render the component - thus leading to neverending re-rendering and numerous API calls.
Suggestion:
Since getUserRoomMessages is already called when the onClick is triggered, thus updating date... you can instead utilize the useEffect to run only when state is updated e.g.
let loaded = React.useRef(false);
// Cleanup useEffect
useEffect(() => {
// set to true on mount...
isLoaded.current = true;
// ... and to false on unmount
return () => { isLoaded.current = false; };
}, [isLoaded, roomMessages]);

Right now you have defined your list using a variable let roomMessagesData = [],whereas you need to use a state to define it. later on when you change that state , your component will be updated since a state has changed.
another way to go is by deploying a useEffect hook in which you will pass roomMessagesData as the second dependancy. This way , when that array changes, your component will rerender.
useEffect(() => {
const copyRoomMessagesData = [...roomMessagesData]
}, [roomMessagesData])
<div className="chatBox">
{copyRoomMessagesData}
</div>

Related

Can't Update state after fetch request

I'm trying to make an app where I fetch data from a Graphql API, and update state after fetching, but after the successful request I'm getting the data but I can't update state with that data. when I console log the response data it's showing the array with required data but when I update state and console log the state it's showing empty array.
I need to update the state to use it in the component, when i'm doing this it's throwing error that currList is undefined.
here are pictures of code and console.
export default function App() {
const [search, setSeach] = useState("");
const [currList, setCurrList] = useState([]);
const fetchShips = async () => {
const response = await request(gql`
{
ships {
name
home_port
image
}
}
`);
console.log("request response", response.data);
setCurrList(response.data);
console.log("currlist:", currList);
};
useEffect(() => {
fetchShips();
}, [currList]);
const Searchitems = (event) => {
setSeach(event.target.value);
setCurrList(
currList.filter((item) => {
return item.name.includes(search) || item.home_port.includes(search);
})
);
};
return (
<div className="App">
<div className="container">
<header></header>
<div className="body">
<input
type="text"
id="search"
value={search}
onChange={Searchitems}
className="input"
placeholder="Search Ships"
/>
<p>Total Count: {currList.length}</p>
<div className="ships">
{currList.map((item, index) => {
return (
<Ship
key={index}
name={item.name}
port={item.port}
img={item.image}
/>
);
})}
</div>
</div>
</div>
</div>
);
}
the State is Updated just not when the code is running that's why it logs that the state is an empty array, try to console.log it once again and you will see that there is something in the List.
That's normal, everything is happening asynchronously. React will trigger a re-render once your currList is updated and you will get its new value. If you want to listen to this change, you have to use useEffect with currList as a dependency.
useEffect(() => {
console.log("List is updated", currList);
}, [currList]);
The function fetchShips has closure over the currList state variable that blongs to specific render call, so when you log it after calling setCurrList it shows you the previous state not the updated state, you can log it in the useEffect to see the changes.
I had the same problem once. I couldn't find efficient solution but my solution saved me anyway.
I used useRef instead of useState.
Try this:
const currList = useRef([]);
useEffect=(()=>{
const fetchShips = async () => {
const response = await request(gql`
{
ships {
name
home_port
image
}
}
`);
console.log("request response", response.data);
// setCurrList(response.data);
if(response)
currlist.current = response.data
console.log("currlist:", currList);
};
fetchShips()
// Also as far as I know ,you should use useEffect like this
},[])
//... codes
return(
//... codes
currList.current.map(...
)
//... codes
Before using useRef, try to define your fetchShips function inside useEffect so maybe you don't need my solution.
Why is not efficient my solution for your case?
When you want to update your currList data, useRef does not trigger re-render. Even if your data updated, you cannot see it on your screen.
So setCurrList(currList.current) can save you but as I said earlier it may not efficient way.

ReactJS: How to detect value changed into another componet and compare with previous?

I am doing a simple exercise to clear my hook concept.
In my current scenario, I need to call the API only if i click on the Call API button and that time the page number should be the current value of Click Me button value.
But currently, on every Click Me button click the API is called.
I have tried with this solution, but can not get the result.
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
console.log(ref.current);
return ref.current;
};
export default function GetAPIData(props) {
const [chars, setChars] = useState([]);
const prevCount = usePrevious(props.id);
console.log(
"Previous value is: " + prevCount,
"Current value is: " + props.id
);
useEffect(() => {
axios
.get(`https://rickandmortyapi.com/api/character/?page=${props.id}`)
.then((res) => {
setChars(res.data.results);
})
.catch((err) => {
alert(err.message);
});
}, [prevCount, props.id]);
return (
<div className="container">
<h3>
<span>Achtung,</span> ein Trend geht um
</h3>
</div>
);
}
Here is the working Sandbox URL for reference.
You are updating the state every time when you click on the button by that the page was re-render and useEffect() is calling the API.
Try to create a new state which only changes when you click on the Call API. By that, your component does not re-render on the change of count.
Might it help You

exported array becomes a pending promise? [duplicate]

This question already has answers here:
How to async await in react render function?
(2 answers)
Which react hook to use with firestore onsnapshot?
(4 answers)
React native call function in text element
(1 answer)
FireStore - Update the UI with new data without reloading the browser
(2 answers)
Closed 1 year ago.
I have this code that gets all players in a specific lobby. The console.log() before the return works as expected:
const getPlayersByLobby = async (lobbyId) => {
const playersRef = firebase.firestore().collection("GameLobbies").doc(lobbyId).collection("players");
const playerNames = [];
const players = await playersRef.get();
players.forEach((groupSnapshot) => {
playerNames.push(groupSnapshot.data().name)
});
console.log(playerNames) // ["player1Name", "player2Name"];
return playerNames
}
Now when rendering the component I do a console.log(getPlayersByLobby(lobby.id)) to see what I get. I see I have a pending promise instead of my array.
Can anyone help my why this is? And how to get my array back?
return(
<div className="App">
<h1>Current game lobbies:</h1>
{gameLobbies.map((lobby) => { //gameLobbies comes from a useState() hook that does work
return (<div key={lobby.id}>
<h2>naam: {lobby.name}</h2>
<p>Code: {lobby.gameId}</p>
<h3>Spelers in deze lobby:</h3>
<ul>
{console.log(getPlayersByLobby(lobby.id))}
</ul>
</div>
)
})}
</div>
);
This is something I struggled with when I came across promises.
With promises it is important to remember that async/await is just a syntactic sugar and it is doesn't make the code synchronous as one might think. It lets you write your code in a way in which it looks synchronous.
Any async function by default returns a Promise and the only way to get the returned value (which comes AFTER the promise has resolved) is to await it everywhere you call that async function.
It does not matter that you already awaited it within the function, that doesn't mean that the function will wait for the promise to get the value/resolve the value before it returns.
The function simply returns the Promise right away with pending status and once the data is there the Promise is updated with the data. In this case by not adding await you are saying "I don't want to wait for the data, just give me the promise" and now you can pass this promise around but you will have to await it if you want the value anywhere you pass it.
Since in your example in the component {console.log(getPlayersByLobby(lobby.id))} there is no await before you call getPlayersByLobby it will simply return a pending Promise without awaiting it
Another way to think about it is using .then(()=> {}).catch((error) => {}) syntax. If there were no async/await keywords or Promises you would have to chain all thens inside of each other.
Using async/await you STILL have to chain the promise but it just looks more readable/synchronous
Hope that makes sense.
try to store players in the state when you get them from the database, also its better to use useEffect when you retrieve data from your database
const [players, setPlayers] = useState([])
const [lobbyId, setLobbyId] = useState([initalId])
useEffect(async () => {
const playersRef =
firebase.firestore().collection("GameLobbies").doc(lobbyId).collection("players");
const playerNames = [];
const players = await playersRef.get();
players.forEach((groupSnapshot) => {
playerNames.push(groupSnapshot.data().name)
});
console.log(playerNames) // ["player1Name", "player2Name"];
//return playerNames
setPlayers(playerNames)
}, [lobbyId]) // it called again whenever you changed your looby id
now it should work
<ul>
// now you have access to them
{console.log(players)}
</ul>
First, as you are only using the name field stored in your document, consider switching out groupSnapshot.data().name for groupSnapshot.get("name") for efficiency.
As this function is stateless, you should place this function outside of your component's render function:
const getPlayersByLobby = async (lobbyId) => {
const playersRef = firebase.firestore()
.collection("GameLobbies")
.doc(lobbyId)
.collection("players");
const playerNames = [];
const players = await playersRef.get();
players.forEach((groupSnapshot) => {
playerNames.push(groupSnapshot.get("name"))
});
console.log(playerNames) // ["player1Name", "player2Name"];
return playerNames
}
Same with this "render lobby" function:
const renderLobby = (lobby, playerList = undefined) {
let players;
if (playerList === undefined) {
players = (<p key="loading">Loading player list...</p>);
} else if (playerList === null) {
players = (<p key="failed">Failed to get player list</p>);
} else if (playerList.length === 0) {
players = (<p key="empty">Player list empty</p>);
} else {
players = (<ul key="players">{
playerList.map((p) => (
<li key={p.id}>
{p.name}
</li>
))
}</ul>);
}
return (
<div key={lobby.id}>
<h2>naam: {lobby.name}</h2>
<p>Code: {lobby.gameId}</p>
<h3>Spelers in deze lobby:</h3>
{ players }
</div>
);
}
Next, in your component, you make use of useEffect() to fetch data from the database. Here, I've used a state object that is a map of lobby IDs to lists of players. On each render, a call is sent off to the database to fetch the current player list.
Because Promises (in this case, database calls) can take longer than render cycles to finish, we need to make sure to not call any setState functions after the component has been unmounted. This is done using the disposed boolean value to discard the results of the Promises if the useEffect's unsubscribe function has been called.
const [gameLobbies, setGameLobbies] = useState(/* init value */);
const [playersByLobby, setPlayersByLobby] = useState({});
useEffect(() => {
let disposed = false;
const newPlayersByLobby = {};
Promise.all(gameLobbies.map((lobby) =>
getPlayersByLobby(lobby.id)
.then((playerList) => newPlayersByLobby[lobbyId] = playerList)
.catch((err) => {
console.error(`Failed to get Lobby #${lobbyId}'s player list: `, err);
newPlayersByLobby[lobbyId] = null;
})
))
.then(() => {
if (disposed) return; // component disposed/gameLobbies was changed
setPlayersByLobby(newPlayersByLobby);
})
return () => disposed = true;
}, [gameLobbies]); // rerun when gameLobbies updates
return (
<div className="App">
<h1>Current game lobbies:</h1>
{
gameLobbies.map(
(lobby) => renderLobby(lobby, playersByLobby[lobby.id])
)
}
</div>
);
Note: Consider implementing a <Lobby> component along with a <PlayerList> component. By doing this, you allow yourself the ability to make full use of the Firestore's Realtime updates.
Try doing it like this
{
getPlayeesByLobby(lobby.id).then(val => {
console.log(val);
}
}
This should give an error in React so what's next
Make an iife or more specifically useEffect in React
so make a useState and after getting data from backend setPlayers to that array. And then map the array and render <li> in the <ul>.
This is what you need to do

Page renders faster than fetching the data and displaying it

How do i display data that i am grabbing using a fetch call and then setting it into an array and displaying in the Dom if it meets the conditions. Currently i am rendering the page faster than i am fetching the data and doing stuff with it.
thanks in advance
function Profile() {
let myReceipe = [];
const [isPopUp, setPopUp] = useState(false);
ReceipeService.getReceipes().then(data => {
myReceipe = data;
})
const popUpHandler = () => {
setPopUp(!isPopUp);
}
return (
<>
<div className='profile'>
<ProfileHeader/>
<div className='createdposts'>
{myReceipe.length !== 0 ?
<Post popUpHandler={popUpHandler} myReceipe={myReceipe}/>
:
null
}
</div>
</div>
{isPopUp === true ?
<Magnify popUpHandler={popUpHandler}/>
:
null
}
</>
)
}
Couple of problems in your code:
You are not using the useEffect hook to make the HTTP request
myReceipe should be in the state of your component
Data will always be loaded after your component has rendered.
The way you are fetching the data is not the correct way to do it. React has useEffect hook that is built exactly for this purpose.
Fetching data from the server is a side-effect and all the side effects belong inside the useEffect hook. So, move the code that makes the HTTP request inside the useEffect hook.
Also make sure that myReceipe is the local state of your component
const [myReceipe, setMyReceipe] = useState([]);
and when the data from the server is available, update the state to trigger a re-render so that you can show the data to the user.
useEffect(() => {
ReceipeService.getReceipes()
.then(data => {
setMyReceipe(data);
});
}, []);
While the data is not available, show some kind of loading spinner to the user to indicate to the user that data is loading.
just use a state variable myReceipe then when myReceipe is set the Component will re render nthen call ReceipeService.getReceipes() in useEffect :
let myReceipe = [];
const [isPopUp, setPopUp] = useState(false);
const [myReceipe , setmyReceipe ] = useState([]);
useEffect(
()=>{
let isMounted=true;
ReceipeService.getReceipes().then(data => {
// isMounted && insures the component is still mounted
// or else this might through an error if the component has unmounted but the api call responded because you cant just update staet of un unmounted Component
isMounted && setmyReceipe(data);
})
return ()=>{isMounted = false }
},[])
const popUpHandler = () => {
setPopUp(!isPopUp);
}
return (
<>
<div className='profile'>
<ProfileHeader/>
<div className='createdposts'>
{myReceipe.length !== 0 ?
<Post popUpHandler={popUpHandler} myReceipe={myReceipe}/>
:
null
}
</div>
</div>
{isPopUp === true ?
<Magnify popUpHandler={popUpHandler}/>
:
null
}
</>
)
}

Fetching w/ custom React hook based on router parameters

I'm trying to fetch data with a custom React hook, based on the current router parameters.
https://stackblitz.com/edit/react-fetch-router
What it should do:
On first load, check if URL contains an ID...
If it does, fetch a todo with that ID
If it does not, fetch a todo with a random ID & add ID to url
On fetch button clicks...
Fetch a todo with a random ID & add ID to url
What is wrong:
Watching the console or inspector network tab, you can see that it's firing several fetch requests on each click - why is this and how should this be done correctly?
Since you used history.push on handleClick, you will see multiple requests being sent as you are using history.push on click handler which will cause a re-render and make use use random generated url as well as also trigger the fetchTodo function.
Now a re-render will occur which is cause a randomised id to be generated. and passed onto useTodo hook which will lead to the fetchTodo function being called again.
The correct solution for you is to set the random todo id param on handleClick and also avoid unnecessary url updates
const Todos = () => {
const history = useHistory();
const { id: todoId } = useParams();
const { fetchTodo, todo, isFetching, error } = useTodo(todoId);
const isInitialRender = useRef(true);
const handleClick = () => {
const todoId = randomMax(100);
history.push(`/${todoId}`);
};
useEffect(() => {
history.push(`/${todoId}`);
}, []);
useEffect(() => {
if(!isInitialRender.current) {
fetchTodo();
} else {
isInitialRender.current = false
}
}, [todoId])
return (
<>
<button onClick={handleClick} style={{marginBottom: "10px"}}>Fetch a todo</button>
{isFetching ? (
<p>Fetching...</p>
) : (
<Todo todo={todo} color={todo.color} isFetching={isFetching} />
)
}
</>
);
};
export default Todos;
Working demo

Resources