I am building a messaging feature using socket.io and react context;
I created a context to hold the conversations that are initially loaded from the server as the user passes authentication.
export const ConversationsContext = createContext();
export const ConversationsContextProvider = ({ children }) => {
const { user } = useUser();
const [conversations, setConversations] = useState([]);
const { socket } = useContext(MessagesSocketContext);
useEffect(() => {
console.log(conversations);
}, [conversations]);
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
});
}, [socket]);
return (
<ConversationsContext.Provider
value={{
conversations,
setConversations,
}}
>
{children}
</ConversationsContext.Provider>
);
};
The conversations state is updated with the values that come from the server, and I have confirmed that on the first render, the values are indeed there.
Whenever i am geting a message, when the socket.on("receive-message", ...) function is called, the conversations state always return as []. When checking devTools if that is the case I see the values present, meaning the the socket.on is not updated with the conversations state.
I would appreciate any advice on this as I`m dealing with this for the past 3 days.
Thanks.
You can take "receive-message" function outside of the useEffect hook and use thr reference as so:
const onReceiveMessageRef = useRef();
onReceiveMessageRef.current = (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
};
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (...r) => onReceiveMessageRef.current(...r));
}, [socket]);
let me know if this solves your problem
Related
I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned. Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(activeId);
}, [activeId);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
Edit:
Based on the discussion in the comments.
const handleSaveSections = () => {
// ... Your logic with the `setState` at the end.
}
useEffect(() => {
if (activeId === null) {
return;
}
save2(); // ( or any other function / logic you need )
}, [activeId]);
return (
<button onClick={handleSaveSections}>Click me!</button>
)
As the setState is a async task, you will not see the changes directly.
If you want to see the changes after the axios call, you can use the following code :
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
setTimeout(()=>console.log(activeId),0);
})
useEffect(() => {
}, [activeId]);
const [activeId, setActiveId] = useState(null);
const save1 = () => {
const handleSaveSections = async () => {
activeMetric &&
axios.get(api1, getDefaultHeaders()).then(res => {
if (res.data.length > 0) {
Swal.fire({
text: 'Record already exists',
icon: 'error',
});
return false;
}
else {
const data = {
item1: item1,
item2: item2
}
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
})
}
});
}
handleSaveSections()
}
const save2 = () => {
console.log(activeId); //correct result would be shown here
}
const handleSaveAll = () => {
save1();
save2();
}
return (
<button type="submit" onClick={handleSaveAll}>Save</button>
)
I have an array of country codes and I need to have the name.
I am trying to access the countries data from the state (axios call) and from there filter by country code, and from that new array, extract the common name of the country.
(I am using the restcountries.com api).
-If I create a new state to map from, I get the too many re-renders.
-Right now, Although the border countries info is there, I can't access it, I get the "Cannot read properties of undefined" error, that usually is tied to a lifecycle issue, therefore I am using a condition on when to access the information.
Still I am not able to get it stable and return the name that I need.
Can someone please take a look and tell me what am I doing wrong?
Thanks in advance
import axios from "axios";
const BorderCountries = (props) => {
const [countriesList, setCountriesList] = useState([]);
useEffect(() => {
axios
.get(`https://restcountries.com/v3.1/all`)
.then((countries) => setCountriesList(countries.data))
.catch((error) => console.log(`${error}`));
}, []);
const getCountryName = () => {
const codes = props.data;
const borderCountries = [];
codes.map((code) => {
const borderCountry = countriesList.filter((country) =>
country.cca3.includes(code)
);
borderCountries.push(borderCountry);
});
// console.log(borderCountries);
if (props.data.length === borderCountries.length) {
const borderName = borderCountries.map((border) =>
console.log(border[0].name.common)
);
return borderName
}
};
return (
<div>
<h3>Border Countries:</h3>
{getCountryName()}
</div>
);
};
export default BorderCountries;
const getCountryName = () => {
const codes = props.data;
if(countriesList.length === 0) return <></>;
const borderCountries = [];
codes.map((code) => {
const borderCountry = countriesList.filter((country) =>
country.cca3.includes(code)
);
borderCountries.push(borderCountry);
});
// console.log(borderCountries);
if (props.data.length === borderCountries.length) {
const borderName = borderCountries.map((border) =>
console.log(border[0].name.common)
);
return borderName
}
};
Try this, you forgot to wait for the call to finish.
I'm building a component to test the performance of different algorithms. The algorithms return the ms they took to run and this is want I want to display. The "fastAlgorithm" takes about half a second, and the "slowAlgorithm" takes around 5 seconds.
My problem is that the UI is not re-rendered with the result until both algorithms have finished. I would like to display the result for the fast algorithm as soon as it finishes, and the slow algorithm when that one finishes.
I've read about how React batches updates before re-rendering, but is there someway to change this behavior? Or is there a better way to organize my component/s to achieve what I want?
I'm using react 16.13.1
Here is my component:
import { useState } from 'react'
import { fastAlgorithm, slowAlgorithm } from '../utils/algorithms'
const PerformanceTest = () => {
const [slowResult, setSlowResult] = useState(false)
const [fastResult, setFastResult] = useState(false)
const testPerformance = async () => {
fastAlgorithm().then(result => {
setFastResult(result)
})
slowAlgorithm().then(result => {
setSlowResult(result)
})
}
return (
<div>
<button onClick={testPerformance}>Run test!</button>
<div>{fastResult}</div>
<div>{slowResult}</div>
</div>
)
}
export default PerformanceTest
I read somewhere that ReactDOM.flushSync() would trigger the re-rendering on each state change, but it did not make any difference. This is what I tried:
const testPerformance = async () => {
ReactDOM.flushSync(() =>
fastAlgorithm().then(result => {
setFastResult(result)
})
)
ReactDOM.flushSync(() =>
slowAlgorithm().then(result => {
setSlowResult(result)
})
)
}
And also this:
const testPerformance = async () => {
fastAlgorithm().then(result => {
ReactDOM.flushSync(() =>
setFastResult(result)
)
})
slowAlgorithm().then(result => {
ReactDOM.flushSync(() =>
setSlowResult(result)
)
})
}
I also tried restructuring the algorithms so they didn't use Promises and tried this, with no luck:
const testPerformance = () => {
setFastResult(fastAlgorithm())
setSlowResult(slowAlgorithm())
}
Edit
As Sujoy Saha suggested in a comment below, I replaced my algorithms with simple ones using setTimeout(), and everything works as expected. "Fast" is displayed first and then two seconds later "Slow" is displayed.
However, if I do something like the code below it doesn't work. Both "Fast" and "Slow" shows up when the slower function finishes... Does anyone know exactly when/how the batch rendering in React happens, and how to avoid it?
export const slowAlgorithm = () => {
return new Promise((resolve, reject) => {
const array = []
for(let i = 0; i < 9000; i++) {
for(let y = 0; y < 9000; y++) {
array.push(y);
}
}
resolve('slow')
})
}
Your initial PerfomanceTest component is correct. The component will re-render for the each state change. I think issue is in your algorithm. Please let us know how did you returned promise there.
Follow below code snippet for your reference.
export const fastAlgorithm = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('fast')
}, 1000)
})
}
export const slowAlgorithm = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('slow')
}, 3000)
})
}
Are you running your algorithms synchronously on the main thread? If so, that's probably what's blocking React from re-rendering. You may need to move them to worker threads.
The below is loosely based on this answer, minus all the compatibility stuff (assuming you don't need IE support):
// `args` must contain all dependencies for the function.
const asyncify = (fn) => {
return (...args) => {
const workerStr =
`const fn = ${fn.toString()}
self.onmessage = ({ data: args }) => {
self.postMessage(fn(...args))
}`
const blob = new Blob([workerStr], { type: 'application/javascript' })
const worker = new Worker(URL.createObjectURL(blob))
let abort = () => {}
const promise = new Promise((resolve, reject) => {
worker.onmessage = (result) => {
resolve(result.data)
worker.terminate()
}
worker.onerror = (err) => {
reject(err)
worker.terminate()
}
// In case we need it for cleanup later.
// Provide either a default value to resolve to
// or an Error object to throw
abort = (value) => {
if (value instanceof Error) reject(value)
else resolve(value)
worker.terminate()
}
})
worker.postMessage(args)
return Object.assign(promise, { abort })
}
}
const multiplySlowly = (x, y) => {
const start = Date.now()
const arr = [...new Array(x)].fill([...new Array(y)])
return {
x,
y,
result: arr.flat().length,
timeElapsed: Date.now() - start,
}
}
const multiplySlowlyAsync = asyncify(multiplySlowly)
// rendering not blocked - just pretend this is React
const render = (x) => document.write(`<pre>${JSON.stringify(x, null, 4)}</pre>`)
multiplySlowlyAsync(999, 9999).then(render)
multiplySlowlyAsync(15, 25).then(render)
Note that fn is effectively being evaled in the context of the worker thread here, so you need to make sure the code is trusted. Presumably it is, given that you're already happy to run it on the main thread.
For completeness, here's a TypeScript version:
type AbortFn<T> = (value: T | Error) => void
export type AbortablePromise<T> = Promise<T> & {
abort: AbortFn<T>
}
// `args` must contain all dependencies for the function.
export const asyncify = <T extends (...args: any[]) => any>(fn: T) => {
return (...args: Parameters<T>) => {
const workerStr =
`const fn = ${fn.toString()}
self.onmessage = ({ data: args }) => {
self.postMessage(fn(...args))
}`
const blob = new Blob([workerStr], { type: 'application/javascript' })
const worker = new Worker(URL.createObjectURL(blob))
let abort = (() => {}) as AbortFn<ReturnType<T>>
const promise = new Promise<ReturnType<T>>((resolve, reject) => {
worker.onmessage = (result) => {
resolve(result.data)
worker.terminate()
}
worker.onerror = (err) => {
reject(err)
worker.terminate()
}
// In case we need it for cleanup later.
// Provide either a default value to resolve to
// or an Error object to throw
abort = (value: ReturnType<T> | Error) => {
if (value instanceof Error) reject(value)
else resolve(value)
worker.terminate()
}
})
worker.postMessage(args)
return Object.assign(promise, { abort }) as AbortablePromise<
ReturnType<T>
>
}
}
I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.
I am a beginner to react and I feel this question has been asked several times in the past but I can't see to understand what I am missing to get this working.
I am writing a simple browser based game. I am using socket-io to connect to the server and I am using a custom hook for obtaining state sent by the server
export const useSocketState = (serverUrl: string) => {
const [isConnected, setConnected] = useState(false)
const [socketId, setSocketId] = useState('')
const [gameState, setGameState] = useState(DEFAULT_INITIAL_STATE)
useEffect(() => {
const client = socket.connect(serverUrl)
client.on("connect", () => {
console.log("connected with id " + client.id)
setConnected(true)
setSocketId(client.id)
});
client.on("disconnect", () => {
setConnected(false)
});
client.on("gameStateUpdate", (data: ClientGameState) => {
console.dir("Recieved new state from server" + data);
console.dir("GameState is " + gameState);
setGameState(prevState => {
const newState = data
console.log(" new state is " + newState)
return newState
})
});
}, gameState);
return {gameState, isConnected, socketId}
}
I am using this hook in a component where I am referrring to the gameState returned by this hook.
I am expecting that when server sends a new state my gameState is actually set with a new state. I do see the log statement where a newState is printed when the server returns a new state but I am not seeing it being reflected in the component I am using.
Game component
import React from "react";
import {useSocketState} from "./useSocket";
import {ClientGameState} from "../game/GameState";
export function GameComponent(props: any) {
const {gameState, isConnected, socketId} = useSocketState("http://127.0.0.1:8080");
if (gameState.state === 'created') {
return (
<div>
<p> Game id for this game: {props.location.state.gameId} and playerId: {props.location.state.playerId} </p>
<p>{JSON.stringify(gameState)}</p>
</div>
);
} else {
return (
<div>
<p> Game state is not created </p>
</div>
)
}
}
export const useSocketState = (serverUrl: string) => {
const [isConnected, setConnected] = useState(false)
const [socketId, setSocketId] = useState('')
const [gameState, setGameState] = useState(DEFAULT_INITIAL_STATE);
function socketConnection(){
const client = socket.connect(serverUrl)
client.on("connect", () => {
console.log("connected with id " + client.id)
setConnected(true)
setSocketId(client.id)
});
client.on("disconnect", () => {
setConnected(false)
});
client.on("gameStateUpdate", (data: ClientGameState) => {
console.dir("Recieved new state from server" + data);
console.dir("GameState is " + gameState);
setGameState(prevState => {
const newState = data
console.log(" new state is " + newState)
return newState
})
});
}
useEffect(() => {
socketConnection();
}, []);
return {gameState, isConnected, socketId}
}
When you connect to the socket, you don't need to use useEffect() anymore.
Socket connection needed be made when component is mounted. So, you'll need second parameter of useEffect:
useEffect(() => {
// ... connection
}, []) // run on component mount
You may also use useEffect when some variable changes:
useEffect(() => { // additional useEffect hook
// ... changes being detected
}, [gameState])
How about just passing the data that is returned from the socket to your setGameState hook.
client.on("gameStateUpdate", (data: ClientGameState) => {
setGameState(data);
});