I have written the following code in react development that runs without an error. But it appears buggy in a peculiar way. A race condition when running useEffect looks possible:
// Variables declared top-level outside the component
let initialized = false;
let componentIsMounted = true;
const PrivateRouteGuard = () => {
const token = useSelector((state) => state?.auth?.token);
const userIsAuthenticated = Boolean(token) || sessionStorage.getItem("isAuthenticated");
const [displayPage, setDisplayPage] = useState(false);
const dispatch = useDispatch();
useEffect(() => {
componentIsMounted = true;
if (!initialized) {
initialized = true;
console.log("✅ Only runs once per app load");
if (!token && userIsAuthenticated) {
refreshAccessToken()
.then((response) => {
// Add the new Access token to redux store
dispatch(addAuthToken({ token: response?.data?.accessToken }));
return getUserProfile(); // Get authenticated user using token in redux store
})
.then((response) => {
const user = response.data?.user;
// Add authenticated user to redux store
dispatch(addAuthUser({ user }));
})
.finally(() => {
componentIsMounted && setDisplayPage(true);
});
} else {
setDisplayPage(true);
}
}
return () => componentIsMounted = false;
}, []);
if (!displayPage) {
return "LOADING..."; // Display loading indicator here
}
if (!userIsAuthenticated) {
return (
<Navigate to="/login" />
);
}
return <Outlet />;
};
export default PrivateRouteGuard;
With the code above, a user with an authentication token is considered authenticated. I have backed up authentication status in browser local storage since a page refresh, wipes out redux store.
Actually this is a react component that protects a route from an unauthenticated user.
With how react strict mode works in development, useEffect is called twice. However calling twice to get a new Access token (refreshAccessToke()), will cause a token to be invalidated(how the backend works). So I tried to mitigate useEffect from calling refreshAccessToken() twice by using a top-level initialized variable.
While this works, the code makes me cringe abit.
Why?
On page refresh:
useEffect runs and refreshAccessToken() is fired on the first app load.
React cleans Up useEffect by running clean up function.
React sets up useEffect again; this time not calling refreshAccessToken().
The problem
However I am doubtful of a race condition occuring at step 2. Say, while React is running clean-up function, refreshAccessToken() has resolved and is ready with its data. But when clean up function is run,the variable componentIsMounted is false. Therefore this piece of code:
.finally(() => {
componentIsMounted && setDisplayPage(true);
})
Means the state will never be set.
React then heads to step 3, where everything is setup again. But refreshAccessToken() finished running(while at step 2) and it already decided not to set displayPage state i.e: componentIsMounted && setDisplayPage(true);. So there is a possibility to get stuck seeing a loading indicator as with this piece:
if (!displayPage) {
return "LOADING..."; // Display loading indicator here
}
I might be missing a concept about the internals of react. Nevertheless code shared above looks like one that sometimes work and sometimes fail. I hope to get my doubts cleared.
Related
I'm trying to save State twice, so I can reset it later on, but no matter what method I try, the 'setFullTrials' won't update with the saved data. The "console.log(savedData)" shows that all the data is there, so that's definitely not the problem. Not sure where I'm going wrong.
function AllTrials({Trialsprop}) {
let [savedData, setSavedData] = useState([]);
let [fullTrials, setFullTrials] = useState([]);
useEffect(() => {
//Call the Database (GET)
fetch("/trials")
.then(res => res.json())
.then(json => {
// upon success, update trials
console.log(json);
setFullTrials(json);
setSavedData(json);
})
.catch(error => {
// upon failure, show error message
});
}, []);
const resetState = () => {
setFullTrials(savedData);
//setFullTrials((state) => ({
...state,
savedData
}), console.log(fullTrials));
// setFullTrials(savedData.map(e => e));
console.log("savedData", savedData)
}
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, it will likely run 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(fullTrials)
// Whatever else we want to do after the state has been updated.
}, [fullTrials])
This console.log will run only after the state has finished changing and a render has occurred.
Note: "fullTrials" in the example is interchangeable with whatever other state piece you're dealing with.
Check the documentation for more info.
P.S: the correct syntax for useState is with const, not let.
i.e. - const [state, setState] = useState()
I have a function that needs to run when my app starts. It basically makes several API calls to fetch the user's information, count its messages and subscribe to a socket channel.
It is made with React-query. But it doesn't perform any API call. The function _getUserInformations is never triggered and the network tab of my inspector remains void.
The server has no issues and the routes work. Here is the code:
export default function App() {
useEffect(() => {
initializeUser();
}, []);
const user = userStore();
return routes + app
);
}
export const initializeUser = () => {
try {
const res = await getUserInformations();
const user = res.data.user;
updateUser(user);
const { unreadConversations } = await hasUnreadConversations(user._id);
updateunreadConversations(unreadConversations);
getNotifications(user._id);
}
catch (err) {
return null;
}
};
const _getUserInformations = async () => {
try {
const userToken = await api.get("/user-informations", {
withCredentials: true,
});
return userToken;
} catch (err) {
throw new Error(err.message || "error.unknown");
}
};
const getUserInformations: UserService["getUserInformations"] = () => {
const { data } = useQuery("getUserInfos", () => _getUserInformations(), {
cacheTime: 1000 * 60 * 60 * 24, // 24 hours
});
return data.data.user;
};
What is wrong here?
Here is also a sandbox that reproduces the same issue with PokeApi: https://codesandbox.io/s/gifted-hill-tei88?file=/src/App.js
_getPokemon (that imitates _getUserInformations) is never called as well...
Hooks should only be used in a functional component.
For example in your codesandbox codes, if you use the following, it will work.
export default function App() {
const { data, isError } = useQuery("getpokemon", () => _getPokemon());
console.log('data', data) //you can see the data printed.
React.useEffect(() => initialize(), []);
if (!data && !isError) return <Loading /> // Bonus Tip - data is fetching if both data and isError is undefined.
return <div>hello</div>;
}
https://reactjs.org/docs/hooks-rules.html
Don’t call Hooks from regular JavaScript functions. Instead, you can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom
Hooks (we’ll learn about them on the next page).
Also...
Only Call Hooks at the Top Level Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top
level of your React function, before any early returns. By following
this rule, you ensure that Hooks are called in the same order each
time a component renders. That’s what allows React to correctly
preserve the state of Hooks between multiple useState and useEffect
calls. (If you’re curious, we’ll explain this in depth below.)
For my app's signup form, I am maintaining the form values with local component state, but the currentUser state, error state, and the API calls are in Redux.
What I'd like to happen is when the form is submitted, the button has a loading spinner and the form values remain until a response is returned from the server. If the server responds with an authorized user, redirect to app. If there's an error, the form's values should not be cleared.
The problem seems to be that Redux is clearing my form values when it dispatches any state updating function (whether removing an error or making the API call to authorize the user). Is there any way to avoid this happening?
From my AuthForm.js
const submitData = () => {
setLoading(true);
if (formType === 'reset') {
updatePassword(resetToken, values.password, history)
.then(result => setLoading(false));
} else if (formType === 'forgot') {
forgotPassword(values.email, history);
} else {
console.log(values); // form values still populated
onAuth(formType, values, history)
.then(result => {
console.log('result received'); // values empty
setLoading(false);
if (formType === 'signup') {
history.push('/questionnaire')
} else {
history.push('/app')
}
})
.catch(err => setLoading(false));
}
};
From my redux actions.js file:
export function authUser(type, userData, history) {
return dispatch => {
dispatch(removeError());
console.log('dispatch') // by this time the form values are empty
// unless I comment out the dispatch(removeError()) above,
// in which case we still have values until 'token recevied' below
return apiCall('post', `/users/${type}`, userData)
.then(({ jwt, refresh_token, ...user }) => {
console.log('token received')
localStorage.setItem('jwtToken', jwt);
localStorage.setItem('jwtTokenRefresh', refresh_token);
dispatch(getUser(user.id));
})
.catch(err => {
handleError(dispatch, err);
});
};
}
I'm also logging the values in my AuthForm component. The result is this:
EDIT: It definitely looks like my components are unmounting but it's still not clear why to me, or how to prevent it.
I am trying to memoize the dispatch function but it seems to have no effect.
const dispatch = useDispatch();
const onAuth = useCallback(
(formType, values, history) => {
dispatch(authUser(formType, values, history))
},
[dispatch]
);
Your component is a subscriber to a redux store, thats why it re-renders,
component's state gets initialized to initial states,
Now you need, a way to persist these component states within rendering cycles.
On class component you would tie a value to a instance i.e using this.value == 'value'
so that it remains the same between renders,
On functional component to achieve this you make use of a hook called useRef
which doesnt change its value on re-render unless you manually change it,
Hopely it will help
Edit: It just occurred to me that there's likely no need to reset the variable within the useEffect hook. In fact, stateTheCausesUseEffectToBeInvoked's actual value is likely inconsequential. It is, for all intents and purposes, simply a way of triggering useEffect.
Let's say I have a functional React component whose state I initialize using the useEffect hook. I make a call to a service. I retrieve some data. I commit that data to state. Cool. Now, let's say I, at a later time, interact with the same service, except that this time, rather than simply retrieving a list of results, I CREATE or DELETE a single result item, thus modifying the entire result set. I now wish to retrieve an updated copy of the list of data I retrieved earlier. At this point, I'd like to again trigger the useEffect hook I used to initialize my component's state, because I want to re-render the list, this time accounting for the newly-created result item.
const myComponent = () => {
const [items, setItems] = ([])
useEffect(() => {
const getSomeData = async () => {
try {
const response = await callToSomeService()
setItems(response.data)
setStateThatCausesUseEffectToBeInvoked(false)
} catch (error) {
// Handle error
console.log(error)
}
}
}, [stateThatCausesUseEffectToBeInvoked])
const createNewItem = async () => {
try {
const response = await callToSomeService()
setStateThatCausesUseEffectToBeInvoked(true)
} catch (error) {
// Handle error
console.log(error)
}
}
}
I hope the above makes sense.
The thing is that I want to reset stateThatCausesUseEffectToBeInvoked to false WITHOUT forcing a re-render. (Currently, I end up calling the service twice--once for win stateThatCausesUseEffectToBeInvoked is set to true then again when it is reset to false within the context of the useEffect hook. This variable exists solely for the purpose of triggering useEffect and sparing me the need to elsewhere make the selfsame service request that I make within useEffect.
Does anyone know how this might be accomplished?
There are a few things you could do to achieve a behavior similar to what you described:
Change stateThatCausesUseEffectToBeInvoked to a number
If you change stateThatCausesUseEffectToBeInvoked to a number, you don't need to reset it after use and can just keep incrementing it to trigger the effect.
useEffect(() => {
// ...
}, [stateThatCausesUseEffectToBeInvoked]);
setStateThatCausesUseEffectToBeInvoked(n => n+1); // Trigger useEffect
Add a condition to the useEffect
Instead of actually changing any logic outside, you could just adjust your useEffect-body to only run if stateThatCausesUseEffectToBeInvoked is true.
This will still trigger the useEffect but jump right out and not cause any unnecessary requests or rerenders.
useEffect(() => {
if (stateThatCausesUseEffectToBeInvoked === true) {
// ...
}
}, [stateThatCausesUseEffectToBeInvoked]);
Assuming that 1) by const [items, setItems] = ([]) you mean const [items, setItems] = useState([]), and 2) that you simply want to reflect the latest data after a call to the API:
When the state of the component is updated, it re-renders on it's own. No need for stateThatCausesUseEffectToBeInvoked:
const myComponent = () => {
const [ items, setItems ] = useState( [] )
const getSomeData = async () => {
try {
const response = await callToSomeService1()
// When response (data) is received, state is updated (setItems)
// When state is updated, the component re-renders on its own
setItems( response.data )
} catch ( error ) {
console.log( error )
}
}
useEffect( () => {
// Call the GET function once ititially, to populate the state (items)
getSomeData()
// use [] to run this only on component mount (initially)
}, [] )
const createNewItem = async () => {
try {
const response = await callToSomeService2()
// Call the POST function to create the item
// When response is received (e.g. is OK), call the GET function
// to ask for all items again.
getSomeData()
} catch ( error ) {
console.log( error )
}
} }
However, instead of getting all items after every action, you could change your array locally, so if the create (POST) response.data is the newly created item, you can add it to items (create a new array that includes it).
I have an issue where I am trying to use the Redux state to halt the execution of some polling by using the state in an if conditional. I have gone through posts of SO and blogs but none deal with my issue, unfortunately. I have checked that I am using mapStateToProps correctly, I update state immutably, and I am using Redux-Thunk for async actions. Some posts I have looked at are:
Component not receiving new props
React componentDidUpdate not receiving latest props
Redux store updates successfully, but component's mapStateToProps receiving old state
I was kindly helped with the polling methodology in this post:Incorporating async actions, promise.then() and recursive setTimeout whilst avoiding "deferred antipattern" but I wanted to use the redux-state as a single source of truth, but perhaps this is not possible in my use-case.
I have trimmed down the code for readability of the actual issue to only include relevant aspects as I have a large amount of code. I am happy to post it all but wanted to keep the question as lean as possible.
Loader.js
import { connect } from 'react-redux';
import { delay } from '../../shared/utility'
import * as actions from '../../store/actions/index';
const Loader = (props) => {
const pollDatabase = (jobId, pollFunction) => {
return delay(5000)
.then(pollFunction(jobId))
.catch(err => console.log("Failed in pollDatabase function. Error: ", err))
};
const pollUntilComplete = (jobId, pollFunction) => {
return pollDatabase(jobId, pollFunction)
.then(res => {
console.log(props.loadJobCompletionStatus) // <- always null
if (!props.loadJobCompletionStatus) { <-- This is always null which is the initial state in reducer
return pollUntilComplete(jobId, pollFunction);
}
})
.catch(err=>console.log("Failed in pollUntilComplete. Error: ", err));
};
const uploadHandler = () => {
...
const transferPromise = apiCall1() // Names changed to reduce code
.then(res=> {
return axios.post(api2url, res.data.id);
})
.then(postResponse=> {
return axios.put(api3url, file)
.then(()=>{
return instance.post(api3url, postResponse.data)
})
})
transferDataPromise.then((res) => {
return pollUntilComplete(res.data.job_id,
props.checkLoadTaskStatus)
})
.then(res => console.log("Task complete: ", res))
.catch(err => console.log("An error occurred: ", err))
}
return ( ...); //
const mapStateToProps = state => {
return {
datasets: state.datasets,
loadJobCompletionStatus: state.loadJobCompletionStatus,
loadJobErrorStatus: state.loadJobErrorStatus,
loadJobIsPolling: state.loadJobPollingFirestore
}
}
const mapDispatchToProps = dispatch => {
return {
checkLoadTaskStatus: (jobId) =>
dispatch(actions.loadTaskStatusInit(jobId))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(DataLoader);
delay.js
export const delay = (millis) => {
return new Promise((resolve) => setTimeout(resolve, millis));
}
actions.js
...
export const loadTaskStatusInit = (jobId) => {
return dispatch => {
dispatch(loadTaskStatusStart()); //
const docRef = firestore.collection('coll').doc(jobId)
return docRef.get()
.then(jobData=>{
const completionStatus = jobData.data().complete;
const errorStatus = jobData.data().error;
dispatch(loadTaskStatusSuccess(completionStatus, errorStatus))
},
error => {
dispatch(loadTaskStatusFail(error));
})
};
}
It seems that when I console log the value of props.loadJobCompletionStatus is always null, which is the initial state of in my reducer. Using Redux-dev tools I see that the state does indeed update and all actions take place as I expected.
I initially had placed the props.loadJobCompletionStatus as an argument to pollDatabase and thought I had perhaps created a closure, and so I removed the arguments in the function definition so that the function would fetch the results from the "upper" levels of scope, hoping it would fetch the latest Redux state. I am unsure as to why I am left with a stale version of the state. This causes my if statement to always execute and thus I have infinite polling of the database.
Can anybody point out what might be causing this?
Thanks
I'm pretty sure this is because you are defining a closure in a function component, and thus the closure is capturing a reference to the existing props at the time the closure was defined. See Dan Abramov's extensive post "The Complete Guide to useEffect" to better understand how closures and function components relate to each other.
As alternatives, you could move the polling logic out of the component and execute it in a thunk (where it has access to getState()), or use the useRef() hook to have a mutable value that could be accessed over time (and potentially use a useEffect() to store the latest props value in that ref after each re-render). There are probably existing hooks available that would do something similar to that useRef() approach as well.