1

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?

yahms23
  • 326
  • 2
  • 11
  • 1
    Does this answer your question? [useState set method not reflecting change immediately](https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately) – Emile Bergeron May 25 '20 at 14:30

2 Answers2

2

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.

Zachary Haber
  • 10,376
  • 1
  • 17
  • 31
  • Thanks, this clarified things up a bit, guess I'll just have to be aware of these things going forward. – yahms23 May 25 '20 at 15:16
  • If I can pick your brain for a minute. Bearing in mind what you said:--- An indeterminate amount of time could theoretically pass. So, there's no way to guarantee that your data fetches before mounting your component. ---- I'm trying to set up a conditional to use react router Redirect if the single page URL is not found (a 404 page essentially). But by the time the component mounts the page has already redirected to my page and not loaded the component, even if it exists. Any thoughts on how to bypass this? – yahms23 May 25 '20 at 16:21
  • 1
    That's correct. Imagine you are on a really slow connection, it might take an hour to download a tiny bit of data. In actuality, `useEffect` actually triggers after the initial render, so no matter how fast the effect is, you won't have the data yet. – Zachary Haber May 25 '20 at 16:24
  • Apologies, edited the q. I guess I have to structure it differently to account for it rendering or not – yahms23 May 25 '20 at 16:25
  • 1
    Render your redirect conditionally based on if pages exists. So, `{pages && !page && }` – Zachary Haber May 25 '20 at 16:26
  • 1
    @yahms23 I've added that with `useHistory` and the page effect I set up on this answer, although you could set up a second effect instead, if you'd like. – Zachary Haber May 25 '20 at 16:41
  • Thanks @Zachary, `useHistory` worked brilliantly. 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. Cheers for the help – yahms23 May 26 '20 at 08:35
  • 1
    @yahms23, I've added more details about that. Your understanding was pretty spot on :) – Zachary Haber May 26 '20 at 13:39
0
    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();
}, []);
Dlucidone
  • 1,091
  • 9
  • 23