React Hooks: setState, when does it actually take effect? - javascript

So I'm trying to understand React Hooks and how to use them. I can make a fetch inside a component as follows:
var [pages,setPages] = useState();
var [page,setPage] = useState();
async function fetchData() {
await fetch(`https://myBackend/pages`)
.then(response => response.json())
.then(response => {
setPages(response);
console.log(pages[0].title);
})
.then(()=>{
// AT THIS STAGE, pages IS UNDEFINED
setPage(pages.filter(singlepage=> {
return singlepage.url == props.match.params.url;
}))
}
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
console.log(pages[0].title);
return () => {
console.log('unmounting...');
}
},[]);
I call to my backend to get all the pages, which works fine, as if I harcode pages[0].title it will render. But when I'm trying to access specific values in pages within the useEffect hook, for me to assign page, it gives me the error of pages being undefined. My console logging makes this apparent.
I want page as well as pages to be defined on 'component mounting', prior to the page loading. So my question is when does setPages actually set the page? and is it possible for me to assign both within useEffect, and based off each other?

Asynchronous actions take time. An indeterminate amount of time could theoretically pass. So, there's no way to guarantee that your data fetches before mounting your component.
Instead, you need to act as if the data could be undefined and have a state of the application that handles that.
As for getting access to the pages variable immediately after calling setPages, that will also fail because React actually runs the re-render asynchronously.
Even if it ran at the same time, there's a thing called closures in which javascript pulls in all variables around a function when it is created, so that the function always has access to those. React Hooks work by utilizing these closures to only have access to the variables as they were when the component was rendered/re-rendered. The reason this is the case, is because every re-render, all of the functions in your component are re-created which creates a new closure.
So, this means that you'll need to keep these concepts in mind as you work with React.
As for your code, the results solution that Dlucidione set up is one of your best bets, aside from setting up a separate useEffect to update page when pages changes.
const history = useHistory(); //from react-router-dom
useEffect(() => {
async function fetchData() {
return await fetch(`https://myBackend/pages`)
.then((response) => response.json())
.then((response) => {
setPages(response);
})
.catch((err) => setErrors(err));
}
fetchData();
return () => {
console.log('unmounting...');
};
}, []);
useEffect(() => {
const url = props.match.params.url;
if (!pages || !url) return;
// Using Array#find here because I assume based on the name, you want one page rather than an array
const page = pages.find((singlepage) => {
return singlepage.url === url;
});
if (!page) {
// This will change the route if a page isn't found.
history.push('/404');
}
setPage(page);
}, [pages, props.match.params.url, history]);
Redirect and history.push are different in how they work. Redirect is the declarative navigation method, while history.push is the programmatic.
Example usage of history.push:
if (!page) {
// This will change the route if a page isn't found.
history.push('/404');
}
Important note, this can be used anywhere in the code as long as you can pass the history object to there. I've used it within redux thunks before as well.
Example usages of Redirect without it redirecting on mount:
if(!pages && !page){
return <Redirect to="/404"/>
}
Or within some JSX:
<div>
{!pages && !page && <Redirect to="/404"/>}
</div>
Redirect has to be rendered which means its only usable within the return statement of a component.
The way I'm understanding it: the redirect is based on if the
individual page is found in the mounting process, therefore the
redirecting process has to also be in the mounting process to check
before rendering anything to redirect or not.
This is correct. When the Redirect itself mounts or updates, it will redirect if the conditions are correct. If there's a path or from prop on the Redirect (they are aliases of each other), then that limits the Redirect to only work when that path matches. If the path doesn't match, the redirect will do nothing until the path is changed to match (or it unmounts, of course).
Under the hood, Redirect just calls history.push or history.replace in componentDidMount and componentDidUpdate (via the Lifecycle component), so take that as you will.

useEffect(() => {
const fetchData = async () => {
try {
// Make a first request
const result = await axios.get(`firstUrl`);
setPages(result);
// here you can use result or pages to do other operation
setPage(result.filter(singlepage=> {
return singlepage.url == props.match.params.url;
or
setPage(pages.filter(singlepage=> {
return singlepage.url == props.match.params.url;
}))
} catch (e) {
// Handle error here
}
};
fetchData();
}, []);

Related

how to prevent/lock a function from returning until another separate async call has resolved?

I'm working on route authentication, and am storing the authed status in a context so that other React components can check if a user is logged in. The relevant part of code is:
const [loggedInUser, setLoggedInUser] = useState(null)
const authed = () => !!loggedInUser
useEffect(() => {
async function fetchData() {
const response = await fetch('/loggedInUser')
.then(res => res.json())
setLoggedInUser(response)
}
fetchData()
}, [])
The quick explanation of this code is, I need user data (such as id) in parts of code so I need to store the loggedInUser object. however, for simpler tasks such as checking if a user is logged in, I'm using the function authed to check if the loggedInUser variable contains an object (user is logged in) or is null.
To check if a user is logged in, I'm using passport.js and my '/loggedInUser' route looks like this:
app.get('/loggedInUser', async (req, res) => {
if (!req.isAuthenticated())
return null
const { id, name } = await req.user
.then(res => res.dataValues)
return res.json({ id, name })
})
The problem is, code consuming this context and checking authed() is running before the useEffect() and fetch() are able to hit the GET route and use the API response to setLoggedInUser(response). so use of authed() always returns false, even if the API response later sets loggedInUser to some object value where now authed() is true. obviously this is a race condition, but I'm not sure how to address it.
Is there an elegant solution where I can 'lock' authed() from returning a value until the useEffect() has fully 'set up' the state of loggedInUser?
One (awful) solution I'm envisioning may be something like:
const [loggedInUser, setLoggedInUser] = useState(null)
const isFinishedLoading = false // <--
function authed() {
while (!isFinishedLoading) {} // a crude lock
return !!loggedInUser
}
useEffect(() => {
async function fetchData() {
const response = await fetch('/loggedInUser')
.then(res => res.json())
setLoggedInUser(response)
isFinishedLoading = true // <--
}
fetchData()
}, [])
Is there a better way to 'lock' the function authed() until loading is complete?
Edit:
To clarify my usage of authed() for the comments, here is a trimmed down version of my App.js
export default function App() {
return (
<>
<AuthProvider>
<Router className="Router">
<ProtectedRoute path="/" component={SubmittalTable} />
<Login path="/login" />
</Router>
</AuthProvider>
</>
)
}
function ProtectedRoute({ component: Component, ...rest }){
const { authed } = useContext(AuthContext)
if (!authed()) // due to race condition, authed() is always false
return (<Redirect from="" to="login" noThrow />)
return (<Component {...rest} />)
}
I don't understand why you need authed() to be a function, and don't use isLoggedIn directly. Also I don't see where you are setting the Context value, but anyway ...
general suggestions
Generally: In React, try to think about
"what is the state of my app at any given moment",
and not "what should happen in which order".
In your case:
"which page should be displayed right now, based on the state",
instead of "redirect as soon as something happens".
The user is authorized or is not authorized to use your app at any given moment. That is a state, and you would store this state somewhere. Let's call this state isAuthorized.
different places to store state
You can store isAuthorized in the Context, as long as you know it is available when you need it. If the Context is not available at the moment when you want to know if the user is authorized (which seems to be the case in your app), then you can not use the Context to store isAuthorized (at least not alone).
You can fetch isAuthorized every time when you need it. Then isAuthorized is not available until the fetch responds. What is the state of your app right now ? It is notReady (probably). You can store the state notReady somewhere, e.g. again in the Context. (notReady will be always initially true, so you know the app is ready only if you explicitly say so.) The App might display a Spinner and do nothing else as long as it is notReady.
You can store isAuthorized in e.g. the browser storages (e.g. sessionStorage). They are available across page loads, so you don't have to fetch the state every time. The browser storages are supposed to be synchronous, but indeed I would treat them as being asynchronous, because things I have read about the browser storages are not inspiring confidence.
problem and solution
What you are trying to do is to store isAuthorized in the (1) Context AND (2) fetch it every time, so you have 2 states, which need to be synchronized. Anyway, you do need to fetch isAuthorized at least once in the beginning, without that, the app is not ready to be used. So you do need synchronization and a state (3) notReady (or isReady).
Synchronizing state is done with useEffect in React (or with 'dependencies'), e.g.:
useEffect(() => {
setIsFinishedLoading( false ); // state (3) "app is ready"
fetchData().then( response => {
setLoggedInUser( response ); // state (2) "isAuthorized" from fetch
setIsFinishedLoading( true );
}).catch( error => {
setLoggedInUser( null );
setIsFinishedLoading( true );
});
}, []);
useEffect(() => {
if( isFinishedLoading ){
setIsAuthorized( !!response ); // state (1) "isAuthorized" in local state or Context
}
}, [ isFinishedLoading, response ]);
"blocking"
You probably don't need it anyway, but:
Blocking code execution in that sense is not possible in Javascript. You would instead execute the code at some other time, e.g. using Promises. This again requires thinking in a slightly different way.
You can not do:
function authed(){
blockExecutionBasedOnCondition
return !!loggedInUser
}
But you can do:
function authed(){
return !!loggedInUser;
}
function executeAuthed(){
someConditionWithPromise.then( result => {
authed();
});
}

React useState hook is not working when submitting form

I am new to React and I have some doubt regarding useState hook.I was recently working on an API based recipe react app .The problem I am facing is when I submit something in search form a state change should happen but the state is not changing but if I resubmit the form the state changes.
import React,{useState,useEffect} from "react";
import Form from "./componnents/form";
import RecipeBlock from "./componnents/recipeblock"
import './App.css';
function App() {
const API_id=process.env.REACT_APP_MY_API_ID;
const API_key=process.env.REACT_APP_MY_API_KEY;
const [query,setQuery]=useState("chicken");
const path=`https://api.edamam.com/search?q=${query}&app_id=${API_id}&app_key=${API_key}`
const [recipe,setRecipe]=useState([]);
useEffect(() => {
console.log("use effect is running")
getRecipe(query);
}, []);
function search(queryString){
setQuery(queryString);
getRecipe();
}
async function getRecipe(){
const response=await fetch(path);
const data=await response.json();
setRecipe(data.hits);
console.log(data.hits);
}
queryString in search() function holds the value of form input,Every time I submit the form this value is coming correctly but setQuery(queryString) is not changing the query value or state and if I resubmit the form then it change the state.
The code you provided something doesn't make sense.
useEffect(() => {
console.log("use effect is running")
getRecipe(query);
}, []);
Your getRecipe doesn't take a variable. But from what I am understanding whenever you search you want to set the Query then get the recipe from that Query.
With the useEffect you can pass in a parameters to check if they changed before running a function. So update the setQuery then when the component reloads it will fire the useEffect if query has changed. Here is the code to explain:
useEffect(() => {
console.log("use effect is running")
getRecipe(query); <-- this doesn't make sense on your code
}, [query]);
function search(queryString){
setQuery(queryString);
}
By doing this when the state updates it causes the component to re-render and therefore if query has changed it will call your getRecipe function.
The main issue in your code is that you are running getRecipe() directly after setQuery(queryString). setQuery(queryString) is asynchronous and will queue a state change. When you then run getRecipe() directly after, the state will still hold the old value of query (and path) and therefore does not fetch the new data correctly.
One solution would be to call getRecipe() within a useEffect() dependent on path.
useEffect(() => {
getRecipe();
}, [path]);
function search(queryString){
setQuery(queryString);
// getRecipe() <- removed
}
With [path] given as dependencies for useEffect(), getRecipe() will be called automatically whenever path changes. So we don't have to call it manually from search() and therefore can remove getRecipe() from the function body. This also makes the current useEffect() (without [path] dependency) redundant, so it can be removed.
Another solution would be to provide the new query value through the getRecipe() parameters, removing the dependency upon the state.
function search(queryString){
setQuery(queryString);
getRecipe(queryString);
}
async function getRecipe(query) {
const path = `https://api.edamam.com/search?q=${query}&app_id=${API_id}&app_key=${API_key}`;
const response = await fetch(path); // <- is no longer dependent upon the state
const data = await response.json();
setRecipe(data.hits);
}
This does require moving the path definition inside getRecipe().

Nextjs getInitialProps blocked the page rendering in client side?

Since I like to add SSR to my upcoming project to improve SEO, I would like to try out next. What I want is that only use SSR for the initial page, and the rest of navigations in the site will be client side rendering. I see the getInitialProps fit the most in this case, accordingly the documentations.
As my understanding, getInitialProps is run in server for the initial page rendering, and is run in the browser when navigating using next/link. The issue I found is that the getInitialProps seems to block the page rendering. (i.e. page changed/rendered after getInitialProps is completed)
import axios from 'axios'
function Posts(props) {
return (
<div>
<div>Posts:</div>
<div>
{JSON.stringify(props)}
</div>
</div>
)
}
Posts.getInitialProps = async (context) => {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
// Wait longer to see the effect
// await (new Promise((resolve) => {
// setTimeout(resolve, 5000)
// }))
return {
props: {
posts: response.data
}
}
}
export default Posts;
How can I do it like in pure React, render the jsx first, then fill in the props? (the execution JSON.stringify(props) might be ignored at first)
Also, in next 9.3, the team introduced getServerSideProps, which is recommended over getInitialProps. How can they be comparable when they are not the same that getServerSideProps will on run in server?
Based on your comments, you want to do the fetch on the server, on the initial page load. However, if navigating between pages you don't want rendering to block while waiting for getInitialProps to return.
One solution is to check if you're on the server, and do the fetch in getInitialProps. If on the client, don't do the fetch in getInitialProps and instead fetch using useEffect in your render method.
import {useEffect} from 'react'
import axios from 'axios'
const isServer = () => typeof window === 'undefined'
const getPosts = () => {
return axios.get('https://jsonplaceholder.typicode.com/posts')
.then(response => response.data)
}
function Posts({posts}) {
const [renderPosts, setRenderPosts] = useState(posts)
useEffect(() => {
if(posts === null) {
getPosts()
.then(setRenderPosts)
}
}, [])
return (
<div>
<div>Posts:</div>
<div>
{JSON.stringify(renderPosts)}
</div>
</div>
)
}
Posts.getInitialProps = async (context) => {
if(isServer()) {
return {
posts: await getPosts(),
}
}
else {
return {
posts: null,
}
}
}
export default Posts
By the way, you may be tempted to use getServerSideProps here, since it is only called if rendering on the server. However, when a page using getServerSideProps is rendered, it will actually make a call to the server to get data from getServerSideProps, even if you're navigating using next/link. From the Next.js 9.3 blog post:
When navigating between pages using next/link instead of executing getServerSideProps in the browser Next.js will do a fetch to the server which will return the result of calling getServerSideProps.
This would still cause the blocking issue you're wanting to avoid.
One final note, this might not be an idiomatic solution. There may be a more "standard" solution. I just wasn't able to find one. You could likely also use a wrapper around your page component that could do all of this in a more consistent way. If you use this pattern a lot, I'd recommend that.

React state inside a function is not changing even after calling it with a delay of (5 seconds)

In react I am using functional component and I have two functions (getBooks) and (loadMore)
getBooks get data from an endPoint. But when I call loadMore function on button click inside the getBooks function (loadMoreClicked) is not changed it uses the previous state even after calling it with a delay of (5 seconds). But when I call loadMore again the state changes and everything works fine.
can someone explain why the (loadMoreClicked) on the initial call to (getBooks) didn't update
even calling it after 5 seconds delay.
function component() {
const [loadMoreClicked, setLoadMore] = useState(false);
const getBooks = () => {
const endPoint = `http://localhost/getBooks`; //this is my end point
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
console.log(loadMoreClicked); //the (loadMoreClicked) value is still (false) after (5 sec)
})
.catch(err => {
console.log(err);
});
};
const loadMore = () => {
setLoadMore(true); //here i am changing (loadMoreClicked) value to (true)
setTimeout(() => {
getBooks(); // i am calling (getBooks()) after 5 seconds.
}, 5000);
};
return (
<div>
<button onClick={() => loadMore()}>loadMore</button> //calling (loadMore)
function
</div>
);
}
There's two things going on:
getBooks() is using const values that are defined in the surrounding function. When a function references const or let variables outside of its definition, it creates what's called a closure. Closures take the values from those outer variables, and gives the inner function copies of the values as they were when the function was built. In this case, the function was built right after the state was initially called, with loadMoreClicked set to false.
So why didn't setLoadMore(true) trigger a rerender and rewrite the function? When we set state, a rerender doesn't happen instantaneously. It is added to a queue that React manages. This means that, when loadMore() is executed, setLoadMore(true) says "update the state after I'm done running the rest of the code." The rerender happens after the end of the function, so the copy of getBooks() used is the one built and queued in this cycle, with the original values built in.
For what you're doing, you may want to have different functions called in your timeout, depending on whether or not the button was clicked. Or you can create another, more immediate closure, based on whether you want getBooks() to consider the button clicked or not, like so:
const getBooks = wasClicked => // Now calling getBooks(boolean) returns the following function, with wasClicked frozen
() => {
const endPoint = `http://localhost/getBooks`;
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
console.log(wasClicked); // This references the value copied when the inner function was created by calling getBooks()
})
.catch(err => {
console.log(err);
});
}
...
const loadMore = () => {
setLoadMore(true);
setTimeout(
getBooks(true), // Calling getBooks(true) returns the inner function, with wasClicked frozen to true for this instance of the function
5000
);
};
There is a third option, which is rewriting const [loadMoreClicked, setLoadMore] to var [loadMoreClicked, setLoadMore]. While referencing const variables freezes the value in that moment, var does not. var allows a function to reference the variable dynamically, so that the value is determined when the function executes, not when the function was defined.
This sounds like a quick and easy fix, but it can cause confusion when used in a closure such as the second solution above. In that situation, the value is fixed again, because of how closures work. So your code would have values frozen in closures but not in regular functions, which could cause more confusion down the road.
My personal recommendation is to keep the const definitions. var is being used less frequently by the development community because of the confusion of how it works in closures versus standard functions. Most if not all hooks populate consts in practice. Having this as a lone var reference will confuse future developers, who will likely think it's a mistake and change it to fit the pattern, breaking your code.
If you do want to dynamically reference the state of loadMoreClicked, and you don't necessarily need the component to rerender, I'd actually recommend using useRef() instead of useState().
useRef creats an object with a single property, current, which holds whatever value you put in it. When you change current, you are updating a value on a mutable object. So even though the reference to the object is frozen in time, it refers to an object that is available with the most current value.
This would look like:
function component() {
const loadMoreClicked = useRef(false);
const getBooks = () => {
const endPoint = `http://localhost/getBooks`;
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
console.log(loadMoreClicked.current); // This references the property as it is currently defined
})
.catch(err => {
console.log(err);
});
}
const loadMore = () => {
loadMoreClicked.current = true; // property is uodated immediately
setTimeout(getBooks(), 5000);
};
}
This works because, while loadMoreClicked is defined as a const at the top, it is a constant reference to an object, not a constant value. The object being referenced can be mutated however you like.
This is one of the more confusing things in Javascript, and it's usually glossed over in tutorials, so unless you're coming in with some back-end experience with pointers such as in C or C++, it will be weird.
So, for what you are doing, I'd recommend using useRef() instead of useState(). If you really do want to rerender the component, say, if you want to disable a button while loading the content, then reenable it when the content is loaded, I'd probably use both, and rename them to be clearer as to their purpose:
function component() {
const isLoadPending = useRef(false);
const [isLoadButtonDisabled, setLoadButtonDisabled] = useState(false);
const getBooks = () => {
const endPoint = `http://localhost/getBooks`;
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
if (isLoadPending.current) {
isLoadPending.current = false:
setLoadButtonDisabled(false);
}
})
.catch(err => {
console.log(err);
});
};
const loadMore = () => {
isLoadPending.current = true;
setLoadButtonDisabled(true);
setTimeout(getBooks(), 5000);
};
}
It's a little more verbose, but it works, and it separates your concerns. The ref is your flag to tell your component what it's doing right now. The state is indicating how the component should render to reflect the button.
Setting state is a fire-and-forget operation. You won't actually see a change in it until your component's entire function has executed. Keep in mind that you get your value before you can use the setter function. So when you set state, you aren't changing anything in this cycle, you're telling React to run another cycle. It's smart enough not to render anything before that second cycle completes, so it's fast, but it still runs two complete cycles, top to bottom.
you can use the useEffect method to watch for loadMoreClicked updates like componentDidUpdate lifecycle method and call the setTimeout inside that,
useEffect(() => {
if(loadMoreClicked){
setTimeout(() => {
getBooks();
}, 5000);
}
}, [loadMoreClicked])
this way only after the loadMoreClicked is changed to true we are calling the setTimeout.
This boils down to how closures work in JavaScript. The function given to setTimeout will get the loadMoreClicked variable from the initial render, since loadMoreClicked is not mutated.

ReactJS warning "Can't perform a react state update on an unmounted component" on return of Firebase promise in .then handler

I want to save a user to Firebase's Realtime Database upon user creation in a sign-up form. If I return the Firebase function (value), for saving users, in a .then handler instead of just calling it (without return statement), I get an error message from React, saying "Can't perform a react state update on an unmounted component".
My code for the submit handler of the sign-up form looks something like the following:
const SignUpFormBase = (props) => {
const [credentials, setCredentials] = useState(INITIAL_STATE);
const [error, setError] = useState(null);
[some other code]
const handleSubmit = (e) => {
firebase
.doCreateUserWithEmailAndPassword(email, passwordOne)
.then(authUser => {
// create a user in the Firebase realtime database
return firebase.database().ref(`users/${authUser.user.uid}`)
.set({ username, email });
})
.then(() => {
setCredentials(INITIAL_STATE);
props.history.push(ROUTES.DASHBOARD);
})
.catch(error => {
setError(error);
});
e.preventDefault();
};
return (
[some jsx]
);
};
const SignUpForm = withRouter(SignUpFormBase);
export default SignUpForm;
The code actually works, whether you include or leave off the return statement. However, if you don't use a return statement, the warning won't show.
I just don't understand why I get the above-mentioned warning from firebase since I (seemingly) don't perform any state updates on the component after it has been unmounted.
Update:
The component actually unmounts before the setCredentials hook has the chance to update the state. This is not because of the push to history in the code above, but a Public Route I've implemented to show users only pages they are allowed to see. It uses the new useContext hook which triggers a re-render when the context value changes (the value is derived from subscribing to the firebase onAuthStateChanged method). I solved the issue by putting the setCredentials call into a useEffect hook:
useEffect(() => {
// set credentials to INITIAL_STATE before ComponentDiDUnmount Lifecycle Event
return () => {
setCredentials(INITIAL_STATE);
};
}, []);
However, I still don't get why a missing return statement (in the previous setup) lead to a vanishing react warning.
Telling from given code, I'm not sure what part lead to the warning. However, I'd like to provide some advices to help pin point it (Hard to write it clear in comment, so I just post as an answer).
firebasePromise
.then(() => {
// [1] async code
setCredentials(INITIAL_STATE);
// [2] sync code
// UNMOUNT happens after route change
props.history.push(ROUTES.DASHBOARD);
})
.catch(error => {
// [3] async code
setError(error);
});
I suspect the problem comes from the execution order of sync/async code. Two things you can try out to gain more info.
Check if [3] is invoked, use console.log maybe?
Wrap [2] in a setTimeout(() => {}) to make it async too.
My bet is on [3]. somehow some error is thrown after [2] is invoked.
Your problem is here:
setCredentials(INITIAL_STATE);
props.history.push(ROUTES.DASHBOARD);
most likely setCredentials is async function
and when it is trying to change state you already have different route because of props.history.push(ROUTES.DASHBOARD)
as the result you don't have component and you can't update the state
try:
setCredentials(INITIAL_STATE).then(
props.history.push(ROUTES.DASHBOARD);
)

Categories

Resources