In Main.tsx, I have been using useApi(opts,payload), which is a hook I made that is subscribed to changes in payload. It will make an API call whenever that variable is changed.
When I use usApi() in Main.tsx (as I have commented out), everything works fine.
It is only when I moved this logic to another functional component (Auth.tsx), and try to call it within Main.tsx that I encounter a problem. The problem is that it seems to attempt to make an API call in an infinite loop, but never throws an error.
What is happening here?
Main.tsx
function Main():JSX.Element {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const opts = {
username: username,
password: password,
fail: ()=>null
}
const defaultPayload = {
path: 'notes/validateAuth/',
method: 'GET',
body: null,
callback: ()=>null
}
const [payload, setPayload] = useState<IPayload>(defaultPayload)
//useApi(opts, payload) <---- THIS WORKS
Auth() // <---- THIS DOES NOT
...
Auth.tsx
import React, {useState} from 'react';
import useApi, {IPayload} from './hooks/useApi';
import Modal from 'react-modal'
import './modal.css';
Modal.setAppElement('#root')
function Auth():JSX.Element{
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const opts = {
username: username,
password: password,
fail: ()=>null,
}
const defaultPayload = {
path:'notes/validateAuth/',
method: 'GET',
body: null,
callback: ()=>null
}
//const _payload = (props.payload===null)?defaultPayload:props.payload
// console.log(_payload)
useApi(opts, defaultPayload)
return(
<></>
)
}
export default Auth;
UseApi.tsx
In case this is needed to debug:
import {useState, useEffect, useCallback} from 'react'
import createPersistedState from 'use-persisted-state'
import {apiUrl} from '../config'
console.log("MODE: "+apiUrl)
export interface IProps {
username:string,
password:string,
fail: ()=>void
}
export interface IPayload {
path:string,
method:string,
body:{}|null,
callback: (result:any)=>any,
}
function useApi(props:IProps, payload:IPayload){
const [accessKey, setAccessKey] = useState('')
const useRefreshKeyState = createPersistedState('refreshKey')
const [refreshKey, setRefreshKey] = useRefreshKeyState('')
//const [refreshKey, setRefreshKey] = useState('')
const [refreshKeyIsValid, setRefreshKeyIsValid] = useState<null|boolean>(null)
// const apiUrl = 'http://127.0.0.1:8000/api/'
// const apiUrl = '/api/'
const [accessKeyIsValid, setAccessKeyIsValid] = useState<null|boolean>(null)
const validHttpCodes = [200, 201, 202, 203, 204]
const go = useCallback((accessKey)=>{
const {body, method, path} = payload
console.log('executing GO:'+accessKey)
if(method === 'logout'){
return logout(payload.callback)
}
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer '+accessKey,
},
... (body !== null) && { body: JSON.stringify(body) }
}
fetch(apiUrl+path,options)
.then(response=>{
if(!validHttpCodes.includes(response.status)){
setAccessKeyIsValid(false)
} else {
setAccessKeyIsValid(true)
response.json()
.then(response=>{
payload.callback(response)
})
}
})
},[payload])
function logout(callback:(response:null)=>void){
setRefreshKey('')
setAccessKey('')
setRefreshKeyIsValid(null)
setAccessKeyIsValid(null)
callback(null)
}
useEffect(()=>{
if(accessKeyIsValid===false){
console.log('access key is false')
// We tried to make a request, but our key is invalid.
// We need to use the refresh key
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json', },
body: JSON.stringify( {'refresh': refreshKey} ),
}
fetch(apiUrl+'token/refresh/', options)
.then(response=>{
if(!validHttpCodes.includes(response.status)){
setRefreshKeyIsValid(false)
// this needs to trigger a login event
} else {
response.json()
.then(response=>{
setRefreshKeyIsValid(true)
setAccessKey(response.access)
// setAccessKeyIsValid(true)
})
}
})
}
},[accessKeyIsValid])
useEffect(()=>{
console.log('responding to change in access key:'+accessKey)
go(accessKey)
},[accessKey,payload])
useEffect(()=>{
if(refreshKeyIsValid===false){
// even after trying to login, the RK is invalid
// We must straight up log in.
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: props.username,
password: props.password,
})
}
fetch(apiUrl+'token/', options)
.then(response=>{
console.log(response.status)
if(!validHttpCodes.includes(response.status)){
setAccessKeyIsValid(null)
setRefreshKeyIsValid(null)
props.fail()
console.log('total fail')
}
else {
console.log('login works')
response.json()
.then(response=>{
setAccessKey(response.access)
setRefreshKey(response.refresh)
// setRefreshKeyIsValid(true)
// setAccessKeyIsValid(true) // Commenting this out disables the loop
})
}
})
}
},[refreshKeyIsValid])
};
export default useApi
Note: I have reasons for needing to factor this logic out, which is not evident by my abridged code.
We've spoken a little bit on chat in order to find out a little bit more about what is going on here. I'll edit your question in a moment to take out the irrelevant code so that this is easier to understand for anyone stumbling on the question.
Your original code did the following:
Instantiated an object (defaultProps) within a functional component
Passed that object in as the default for a useState() call
Passed the state value into your hook, which was then used in the dependency array of a useEffect() hook
Your refactor changed it so you were directly passing the defaultProps reference into your hook without going through a useState() hook as before.
An important thing to understand about React components is that although their execution pattern is somewhat predictable, it is something you have no control over as a developer and so you should assume when writing your components that they may be re-rendered at any time.
Within a functional component, each re-render is a new function execution with a new scope. If you instantiate an object within this scope, that's a new reference variable pointing to a different area of memory each time
The problem with then using this within the dependency array of a useEffect() hook is that since this is a new variable on each render, it means that each time the component is re-rendered, the useEffect hook will see a different reference, and cause the effect callback to be executed again. This is not normally the intended behaviour.
const component = () => {
/*
* Each time this component
* renders, this object is created again
* in (probably) a different area of memory
*/
const obj = {
foo: 'bar'
}
/*
* Although 'obj' *appears* to be the same here since it is in fact
* a different reference, this console.log statement is going
* to execute every single time the component re-renders
*/
useEffect(() => {
console.log('running effect!')
}, [obj])
return <div></div>
}
So what is the solution?
The original code first passed the defaultPayload into a useState call. This was the right thing to do. The reason for this is because component state values do maintain the same reference in between renders and can therefore be safely used in a useEffect() dependency array.
const component = () => {
/*
* still a different reference on each render
*/
const obj = {
foo: 'bar'
}
const [objState, setObjState] = React.useState(obj);
/*
* Although `obj` is a difference reference on every render
* objState is *not*, so our `console.log` will now only execute
* when objState changes
*/
useEffect(() => {
console.log('running effect!')
}, [objState])
return <div></div>
}
Alternative (but slightly pointless) Solution
Note that you could also resolve the very specific issue experienced in this question by simply instantiating the object outside of the functional component scope (ie, in the module global scope). However, you will need to put it into a state variable as soon as you want to be able to change it (which the name defaultPayload rather suggests you will)
/*
* Definitions made within the module global scope
* are instantiated only once, when the module is imported
*/
const obj = {
foo: 'bar'
}
const component = () => {
/*
* useEffect hook will now only ever fire once
* since `obj` is never going to change
*/
useEffect(() => {
console.log('running effect!')
}, [obj])
return <div></div>
}
Related
So, I searched for an existing solution, but I could find nothing, or maybe I'm not searching the correct way, thus, sorry if there's an existing thread about it.
In sum, it seems my code is not instantiating an object correctly as a class when it comes from an Axios call to the backend. So, when I call some function, I'm getting the error Uncaught TypeError TypeError: object.method is not a function.
Example:
First, basically, a parent component will call a service that will make a request to the backend. The result is then passed to a child component.
// imports
const Component: React.FC<ComponentProps> = () => {
const { id } = useParams<{ id: string }>();
const [object, setObject] = useState<Class>(new Class());
useEffect(() => {
(async () => {
try {
const object = await Service.getById(id);
setObject(object);
} catch (err) {
//error handling
} finally {
cleanup();
}
})();
return () => {
// cleanup
};
});
return (
<Container title={object.name}>
<Child object={object} />
</Container>
);
};
export default Component;
Then, in child component, let's say I try to call a method that was defined in the Class, there I'm getting the not a function error:
// imports
interface Interface {
object: Class;
}
const Child: React.FC<Interface> = ({ object }) => {
object.callSomeFunction(); // error starts here
return (
<SomeJSXCode />
);
};
export default Child;
Example of the Class code, I tried to write the method as a function, arrow function, and a getter, but none worked. Also, as a workaround, I've been defining a method to instantiate the object and set all properties, but I don't think that's a good long-term solution, and for classes with many properties, it gets huge:
export class Class {
id: string = '';
name: string = '';
callSomeFunction = () => {
// do something;
}
static from(object: Class): Class {
const newInstance = new Class();
newInstance.id = object.id;
newInstance.name = object.name;
// imagine doing this for a class with many attributes
return newInstance;
}
}
Finally, the Service code if necessary to better understand:
// imports
const URL = 'http://localhost:8000';
const baseConfig: AxiosRequestConfig = {
baseURL: URL,
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
};
export const backend = axios.create({
...baseConfig,
baseURL: URL + '/someEndpoint',
});
export const Service = {
async getById(id: string): Promise<Class> {
try {
const { data } = await backend.get<Class>(`/${id}`);
return data;
} catch (err) {
throw new Error(err.response.data.message);
}
},
};
As I can't share the real code due to privacy, please let me know if this is enough or if more information is needed. Thanks in advance.
I thought it was some binding issue as here, but no.
So, I actually fixed this by updating the class validator in the back end, as the parsing was only necessary to parse the strings as number. But, by adding the annotation #Type(() => Number) to my dtos, I won't need to parse the strings anymore.
I might have missed something super obvious when refactoring my implementation of Redux in a React application, but when I'm trying to access the value of one of my slices I get thrown some errors by the Typescript Compiler about not being able to assign a (func) => string to a parameter of type string.
For context, here's my implementation:
Slice:
export const environmentSlice = createSlice({
name: 'environment',
initialState,
reducers: {
updateEnvironment: (state, action:PayloadAction<string>) => {
state.value = action.payload
}
}
});
export const { updateEnvironment } = environmentSlice.actions;
export const selectEnvironment = (state: RootState) => state.environment.value;
How i've defined the interface for my environment:
// Defining type for state
interface EnvironmentState {
value: string,
};
// define the initial state using that type
const initialState: EnvironmentState = {
value: 'live',
}
How RootState is defined in my store:
export const store = configureStore({
reducer: {
loggedIn: loggedInReducer,
environment: environmentReducer,
token: tokenReducer,
},
})
export type RootState = ReturnType<typeof store.getState>;
How I'm trying to get the value into one of my React Components:
let environment = useAppSelector((state: RootState) => {
return state.environment.value
});
I've also tried following the implementation in the redux docs here but had no luck with that: https://react-redux.js.org/tutorials/typescript-quick-start#use-typed-hooks-in-components
When assigning this value, i'm using useAppDispatch() assigned to a variable inside of the response section of a fetch request:
fetch('/api/authenticate', requestOptions)
.then(async response => {
if (response.status === 200) {
let data = await response.json();
dispatch({ type: toggle });
dispatch({ type: updateToken, payload: data.token });
webHelpers.get('/api/user', 'default', 'auth', data.token, (data: any) => {
dispatch({ type: updateUser, payload: data.full_name })
});
//
navigate('../management/staff');
Please note: The environment isn't updated upon sign-in but only once the user selects an option from a drop-down menu in the DOM. It's directly after this sign-in and navigation that the application crashes, however, as it states it cannot read the 'value' on the following:
const token = useAppSelector(state => {
return state.token.value
});
The above is reached after the navigate('../management/staff'); is called.
Edit: Accidently included wrong code snippet when showing useAppSelector in use. Update to fix.
Edit2: Added in section about the dispatches that assigns these values.
Edit3: Managed to resolve the solution but not in the exact way I'd hoped so I'll leave this open. The issue appeared to be that the attempts to dispatch data via the slices I'd added to my store's reducer didn't work, having all of those methods on one sole slice resolved the issue. This isn't ideal as I'd wanted 3 separate slices to manage each of these states separately. There must be some issue in my redux store with setting these up to work independently.
I have an Effect hook in a React component that sets up and initialises a class that I use to communicate with a backend server:
const SignalProvider = ({url, children}) => {
let [sigErr, setSigErr] = useState("")
let [token, setToken] = useContext(TokenContext)
let [signaller, setSignaller] = useState()
useEffect(() => {
if (!signaller) {
const s = new Signaller(url, (err) => setSigErr(err))
setSignaller(s)
}
if (token && signaller) {
signaller.setToken(token)
signaller.setSetTokenCallback(setToken) // Adding this line causes an infinite loop
signaller.connect()
}
}, [url, token, signaller, setToken, authError])
...
}
However, adding the line signaller.setSetTokenCallback(setToken) causes an infinite loop of re-rendering. Without this line it works as expected.
All setSetTokenCallback does is:
setTokenCallback(f) {
this.setTokenCallback = f
}
Which I don't think should matter.
Whats the best way to prevent the loop?
Have you tried using 'useCallback()' hook?
const initializeClass = useCallback(() => {
if (!signaller) {
const s = new Signaller(url, (err) => setSigErr(err))
setSignaller(s)
}
if (token && signaller) {
signaller.setToken(token)
signaller.setSetTokenCallback(setToken)
signaller.connect()
}, [url, token, signaller, setToken, authError])
useEffect(() => {
initializeClass()
}, [bla bla all dependencies needed])
I think your culprit is in these lines
signaller.setToken(token) // first possible culprit
signaller.setSetTokenCallback(setToken)
Since you are setting the token inside the useEffect and your useEffect depends on the token value for a re-evaluation again. If the token value is different than the previous one then it will make the useEffect to re-render.
[url, token, signaller, setToken, authError] // second possible culprit
Also, you should remove object or array type variables from the condition array [url, token, signaller, setToken, authError] or stringify them or find some way to compare, as objects and arrays can't be compared directly. They will always return false if directly compared. Thus, your useEffect will re-run.
This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 years ago.
I'm building a messaging application. I'm retrieving the message log from my backend every 5 seconds with setTimeout and storing it in a state variable. I'm having it also scroll to the bottom of the chat window every timeout, but I am trying to make it only occur when there is a new message in the response object. The issue is that I cannot access the state variable inside my function that retrieves it. Ideally I would compare the response object's length to the state variable's current length to determine if there is an new response.
Here's my code:
import React from "react"
import axios from "axios"
export default function Messaging(props) {
const [messages, setMessages] = React.useState([])
const getMessages = () => {
const config = {
method: "get",
url: "localhost:3001/get-messages",
}
axios(config)
.then((response) => {
console.log(messages.length) // prints 0 in reference to the initial state value
if (response.data.get.records.length !== messages.length) {
// function to scroll to the bottom of the chat window
}
setMessages(response.data.get.records)
setTimeout(getMessages, 5000)
})
}
console.log(messages.length) // prints actual value correctly
// Initial retrieval of chat log
React.useEffect(() => {
getMessages()
}, [])
}
I'm open to better suggestions in handling the logic as well. Thanks in advance!
setMessages will update messages in the next render
With that in mind, you could create an Effect that whenever messages.length changes, scrolls to your element
import React from "react"
import axios from "axios"
export default function Messaging(props) {
const [messages, setMessages] = React.useState([])
React.useEffect(() => {
// store the id to clear it out if required
let timeoutId
const getMessages = () => {
const config = {
method: "get",
url: "localhost:3001/get-messages",
}
axios(config)
.then((response) => {
// store the messages
setMessages(response.data.get.records)
// here we tell to poll every 5 seconds
timeoutId = setTimeout(getMessages, 5000)
})
}
getMessages()
return () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}, [setMessages])
React.useEffect(() => {
// code here to scroll
// this would only trigger if the length of messages changes
}, [messages.length])
}
I also added the cleanup in your first effect, so when your component unmounts you cleanup the timeout and you avoid any extra request
I am using Vuex with axios to fetch data from my backend. But somehow the state property userName is not updating in my Vue Single File Component(SFC).
approot.js
state
const state = {
userName: 'foo'
};
getter
const getters = {
getUserName: (state) => state.userName
};
Single File Component
<template>
<div id="navbar">
//cut for brievity
<span>{{getUserName}}</span>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
name: 'navbar',
computed: mapGetters(['getNumberOfJobMessages','getUserName']),
//cut for brievity
}
</script>
<style scoped>
//cut for brievity
</style>
Action fetching data with axios from the backend
const actions = {
async fetchMenuData({ commit }) {
//fetch data from api controller
const response = await axios.get('../api/Menu/GetMenu');
console.log(response.data.userName); //not undefined
commit('setMenuData', response.data);
}
}
Mutation setting state variables
const mutations = {
setMenuData(state, menuData) {
console.log(menuData.userName); //not undefined
state.userName = menuData.userName;
console.log(state.userName); //not undefined
}
}
Problem
When my single file component calls getUserName it always renders 'foo', the hardcoded value. Im quite baffled by this, since the rest of my state variables are set with the same pattern, and my components have no problems getting them.
Anyone who knows whats going wrong or can see a flaw in my code? It would be highly appreciated.
Use mutations to only set data. and other things do on action. like:
Action:
const actions = {
async fetchMenuData({ commit }) {
const response = await axios.get('../api/Menu/GetMenu');
let userName = response.data.userName;
commit('setUserName', userName);
}
}
And mutations:
const mutations = {
setUserName(state, userName) {
state.userName = userName;
}
}
Dont forget to dispatch the function fetchMenuData
Properly not sure, why this happens. But, I faced this problem and solved by this way.
axios.get('../api/Menu/GetMenu')
.then(({ data }) => {
commit('setUserName', data.userName);
}).catch(error => { })
It is better to make a commit in then()