Updating state based on changing API React - reactjs

Is it possible to have an api constantly being called (in a loop until asked to stop) or have react automatically change state if the api changes?
I currently have a backend (/logs) that will send out logs in an array ex: [{log:"test"}, {log:"test1"}].
I have react able to pick that up and update state to display those logs on the front end when a button is clicked
axios.get("/logs").then(response =>{
let templog = response.data
let newLogs = [...logs]
for (let i = 0; i < templog.length; i++) {
if (templog[i].log !== "") {
newLogs.push({log: templog[i].log, id: uuidv4()})
}
}
setLogs(newLogs)
})
Right now, if I update the backend, I would have to reclick the button for the state to update rather than the state automatically updating based on the api

try setInterval in useEffect, also return clearInterval at end of useEffect
import React from 'react'
const ScheduleUpdate = (props) => {
const [logs, setLogs] = React.useState([])
const [run, setRun] = React.useState(true)
React.useEffect(() => {
const getLogs = () => {
fetch("/logs")
.then(r => r.json())
.then(d => { setLogs(d) })
}
if (run){
const handle = setInterval(getLogs, 1000);
return () => clearInterval(handle);
}
}, [run])
return (
<div>
<h1>ScheduleUpdate</h1>
{
logs.map(l => <div>{l}</div>)
}
<button onClick={() => setRun(false)}>Stop</button>
</div>
)
}
export default ScheduleUpdate

Related

Cannot setstate in nested axios post request in react

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>
)

useEffect function inside context unaware of state changes inside itself

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

React hooks : how to watch changes in a JS class object?

I'm quite new to React and I don't always understand when I have to use hooks and when I don't need them.
What I understand is that you can get/set a state by using
const [myState, setMyState] = React.useState(myStateValue);
So. My component runs some functions based on the url prop :
const playlist = new PlaylistObj();
React.useEffect(() => {
playlist.loadUrl(props.url).then(function(){
console.log("LOADED!");
})
}, [props.url]);
Inside my PlaylistObj class, I have an async function loadUrl(url) that
sets the apiLoading property of the playlist to true
gets content
sets the apiLoading property of the playlist to false
Now, I want to use that value in my React component, so I can set its classes (i'm using classnames) :
<div
className={classNames({
'api-loading': playlist.apiLoading
})}
>
But it doesn't work; the class is not updated, even if i DO get the "LOADED!" message in the console.
It seems that the playlist object is not "watched" by React. Maybe I should use react state here, but how ?
I tested
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
//refresh playlist if its URL is updated
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
})
}, [props.playlistUrl]);
And this, but it seems more and more unlogical to me, and, well, does not work.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setPlaylist(playlist); //added this
})
}, [props.playlistUrl]);
I just want my component be up-to-date with the playlist object. How should I handle this ?
I feel like I'm missing something.
Thanks a lot!
I think you are close, but basically this issue is you are not actually updating a state reference to trigger another rerender with the correct loading value.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
setPlaylist(playlist); // <-- this playlist reference doesn't change
})
}, [props.playlistUrl]);
I think you should introduce a second isLoading state to your component. When the effect is triggered whtn the URL updates, start by setting loading true, and when the Promise resolves update it back to false.
const [playlist] = React.useState(new PlaylistObj());
const [isloading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setIsLoading(false);
});
}, [props.playlistUrl]);
Use the isLoading state in the render
<div
className={classNames({
'api-loading': isLoading,
})}
>
I also suggest using the finally block of a Promise chain to end the loading in the case that the Promise is rejected your UI doesn't get stuck in the loading "state".
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl)
.then(function() {
console.log("LOADED!");
})
.finally(() => setIsLoading(false));
}, [props.playlistUrl]);
Here you go:
import React from "react";
class PlaylistAPI {
constructor(data = []) {
this.data = data;
this.listeners = [];
}
addListener(fn) {
this.listeners.push(fn);
}
removeEventListener(fn) {
this.listeners = this.listeners.filter(prevFn => prevFn !== fn)
}
setPlayList(data) {
this.data = data;
this.notif();
}
loadUrl(url) {
console.log("called loadUrl", url, this.data)
}
notif() {
this.listeners.forEach(fn => fn());
}
}
export default function App() {
const API = React.useMemo(() => new PlaylistAPI(), []);
React.useEffect(() => {
API.addListener(loadPlaylist);
/**
* Update your playlist and when user job has done, listerners will be called
*/
setTimeout(() => {
API.setPlayList([1,2,3])
}, 3000)
return () => {
API.removeEventListener(loadPlaylist);
}
}, [API])
function loadPlaylist() {
API.loadUrl("my url");
}
return (
<div className="App">
<h1>Watching an object by React Hooks</h1>
</div>
);
}
Demo in Codesandbox

react navigation: deep linking with state persistence not working on the web

I'm using react navigation v5 on the web and when I manually change the URL and refresh I am kept on the same screen instead of navigating to the new screen. I persisted my navigation state so that behavior is expected and every time the navigation state changes, it is set into local storage. When I manually change the url the navigation container does not know that I am trying to navigate thus returns me the previous navigation state. Below, the handlePushToScreen function was experimental and did not work. Does anyone know how to push a route on top when I manually change the url? Also, I've tried using the useLinkTo hook but I cannot use it since I'm outside of the navigation.
App.tsx
useEffect(() => {
const handlePushToScreen = () => {
const route = navigationRef.current?.getCurrentRoute();
const pushAction = StackActions.push(route.name);
const prevRoute = localStorage.getItem('prevRoute');
if (prevRoute !== null && prevRoute !== window.location.pathname) {
navigationRef?.current.dispatch(StackActions.push(route.name));
} else {
localStorage.setItem('prevRoute', window.location.pathname);
}
};
if (isNavMounted) {
handlePushToScreen();
}
}, [window.location.pathname, isNavMounted]);
useEffect(() => {
const restoreState = async () => {
try {
const initialUrl = await Linking.getInitialURL();
const savedStateString = await AsyncStorage.getItem(PERSISTENCE_KEY);
const state = savedStateString
? JSON.parse(savedStateString)
: undefined;
if (state !== undefined) {
setInitialState(state);
}
} finally {
setIsReady(true);
}
};
if (!isReady) {
restoreState();
}
}, [isReady]);
const handleNavigationChange = (state) => {
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state));
};
if (!isReady) {
return <Text>loading stuff...</Text>;
}
return (
<NavigationContainer
linking={linking}
initialState={initialState}
onReady={() => setIsNavMounted(true)}
onStateChange={(state) => handleNavigationChange(state)}
ref={navigationRef}
fallback={<Text>Loading…</Text>}>
<MainStackNavigator />
</NavigationContainer>
);

Right way to update state with react socket hooks

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);
});

Resources