Loading Data with useEffect - reactjs

So I have a Route that loads a dashboard component, and a sidebar with different links to this dashboard component I'm attempting to use useEffect to load the appropriate data from the backend when the component is loaded
const Dashboard = ({match}) => {
const dispatch = useDispatch()
const [loading, setLoading] = useState(true)
const thing = useSelector(state => state.things)[match.params.id]
const fetchData = async () => {
setLoading(true)
await dispatch(loadStuff(match.params.id))
setLoading(false)
}
useEffect(() => {
fetchData()
}, [match]);
return loading
? <div>Loading</div>
: <div>{thing.name}</div>
}
This works well enough for the first load. However when I click the NavLink on the sidebar to change { match }, thing.name blows up. I would expect, since match is a dependency on useEffect, that it would restart the load cycle and everything would pause until the load is complete, instead it appears to try to render immediately and the API call is not made at all. If I remove thing.name, I see the api call is made and everything works.
I keep running into this, so I appear to have a fundamental misunderstanding of how to predictably load data with redux when a component is mounted. What am I doing wrong here?

Have you wrapped your component with withRouter of react router dom?
import React, { useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
const Dashboard = ({ match }) => {
const dispatch = useDispatch()
const [loading, setLoading] = useState(true)
const thing = useSelector(state => state.things)[match.params.id]
const fetchData = async () => {
setLoading(true)
await dispatch(loadStuff(match.params.id))
setLoading(false)
}
useEffect(() => {
fetchData()
}, [match.params.id]);
return loading
? <div>Loading</div>
: <div>{thing.name}</div>
}
export default withRouter(Dashboard);
Use match.params.id in the useEffect as comparison of object(match) should not be done.
{} === {} /* will always be false and useEffect will be called every time
irrespective of match.params.id change. */

Related

How to run functional component in useEffect used for updating context

Trying to get the API functional component that updates Context values in useEffect to execute within another App functional component (on import) when mounted to allow render when values exist/update.
The API function at API.js appears to be working correctly as a stand alone component. The problem (due to lack of understanding) is upon the import to App.js where the API function should execute.
API.js
import { useState, useContext, useEffect } from 'react'
import { AppContext } from './contexts/AppContext';
export const API = () => {
const {updateOrder, updateCustomer} = useContext(AppContext);
useEffect(() => {
const fetchData = async () => {
const orderDetails = await fetch('url.com/order');
const customerDetails = await fetch('url.com/customer');
updateOrder(await orderDetails.json())
updateCustomer(await customerDetails .json())
}
fetchData();
}, [])
}
App.js
import React, { useContext, useEffect } from 'react';
import { AppContext } from './contexts/AppContext';
import { API } from './components/API';
const App = ({ isLoading }) => {
const {order, customer} = useContext(AppContext);
useEffect(() => {
isLoading && <API />
}, []) // eslint-disable-line react-hooks/exhaustive-deps
if (order && customer) {
return <SomeComponent/>
}
}
The expected outcome is to be able to use API within the initial mount (as API dependency) and conditionally return/render content in App.
I've tried changing the API component into function and exporting with default, however context is not supported outside of component.
A component is a function that renders something in the UI. You API component is not really a component - it doesn't have any branch that returns any JSX - although that's not always required.
You should look into building a custom hook instead. Try something like this:
App.js
import React, { useContext, useEffect } from "react";
import { AppContext } from "./contexts/AppContext";
// Hooks should start with 'use'
export const useAPI = () => {
const { updateOrder, updateCustomer } = useContext(AppContext);
useEffect(() => {
const fetchData = async () => {
// Fetching data from both endpoints in parallel
const res = await Promise.all([fetch("url.com/order"), fetch("url.com/customer")]);
// Converting both payloads to JSON in parallel
const data = await Promise.all([res[0].json(), res[1].json()]);
updateOrder(data[0]);
updateCustomer(data[1]);
};
fetchData();
}, []);
};
const App = ({ isLoading }) => {
// Calling the custom hook
useAPI();
const { order, customer } = useContext(AppContext);
if (order && customer) {
return <SomeComponent />;
}
};

How to fetch request in regular intervals using the useEffect hook?

What I'm trying to do is fetch a single random quote from a random quote API every 5 seconds, and set it's contents to a React component.
I was able to fetch the request successfully and display it's contents, however after running setInterval method with the fetching method fetchQuote, and a 5 seconds interval, the contents are updated multiple times in that interval.
import { Badge, Box, Text, VStack, Container} from '#chakra-ui/react';
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const RandomQuotes = () => {
const [quote, setQuote] = useState<Quote>(quoteObject);
const [error, setError]: [string, (error: string) => void] = React.useState("");
const [loading, setLoading] = useState(true);
const fetchQuote = () => {
axios.get<Quote>(randomQuoteURL)
.then(response => {
setLoading(false);
setQuote(response.data);
})
.catch(ex => {
setError(ex);
console.log(ex)
});
}
setInterval(() => setLoading(true), 5000);
useEffect(fetchQuote, [loading, error]);
const { id, content, author } = quote;
return (
<>
<RandomQuote
quoteID={id}
quoteContent={content}
quoteAuthor={author}
/>
</>
);
}
When any state or prop value gets updated, your function body will re-run, which is called a re-render.
And you've put setInterval call in the main function(!!!), so each time the component re-renders, it will create another interval again and again. Your browser will get stuck after a few minutes.
You need this interval definition once, which is what useEffect with an empty second parameter is for.
Also, using loading flag as a trigger for an API call works, but semantically makes no sense, plus the watcher is expensive and not needed.
Here's a rough correct example:
useEffect(() => {
const myInterval = setInterval(fetchQuote, 5000);
return () => {
// should clear the interval when the component unmounts
clearInterval(myInterval);
};
}, []);
const fetchQuote = () => {
setLoading(true);
// your current code
};

Why React detect the function reference as new?

I have theorical question about custom hooks and use effect when redux is involved.
Let`s assume I have this code:
//MyComponent.ts
import * as React from 'react';
import { connect } from 'react-redux';
const MyComponentBase = ({fetchData, data}) => {
React.useEffect(() => {
fetchData();
}, [fetchData]);
return <div>{data?.name}</data>
}
const mapStateToProps= state => {
return {
data: dataSelectors.data(state)
}
}
const mapDispatchToProps= {
fetchData: dataActions.fetchData
}
export const MyComponent = connect(mapStateToProps, mapDispatchToProps)(MyComponentBase);
This works as expected, when the component renders it does an async request to the server to fetch the data (using redux-thunk). It initializes the state in the reduces, and rerender the component.
However we are in the middle of a migration to move this code to hooks. Se we refactor this code a little bit:
//MyHook.ts
import { useDispatch, useSelector } from 'react-redux';
import {fetchDataAction} from './actions.ts';
const dataState = (state) => state.data;
export const useDataSelectors = () => {
return useSelector(dataState);
}
export const useDataActions = () => {
const dispatch = useDispatch();
return {
fetchData: () => dispatch(fetchDataAction)
};
};
//MyComponent.ts
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, [fetchData]);
return <div>{data?.name}</data>
}
With this change the component enters in an infite loop. When it renders for the first time, it fetches data. When the data arrives, it updates the store and rerender the component. However in this rerender, the useEffect says that the reference for fetchData has changed, and does the fetch again, causing an infinite loop.
But I don't understand why the reference it's different, that hooks are defined outside the scope of the component, they are not removed from the dom or whateverm so their references should keep the same on each render cycle. Any ideas?
useDataActions is a hook, but it is returning a new object instance all the time
return {
fetchData: () => dispatch(fetchDataAction)
};
Even though fetchData is most likely the same object, you are wrapping it in a new object.
You could useState or useMemo to handle this.
export const useDataActions = () => {
const dispatch = useDispatch();
const [dataActions, setDataActions] = useState({})
useEffect(() => {
setDataActions({
fetchData: () => dispatch(fetchDataAction)
})
}, [dispatch]);
return dataActions;
};
first of all if you want the problem goes away you have a few options:
make your fetchData function memoized using useCallback hook
don't use fetchData in your useEffect dependencies because you don't want it. you only need to call fetchData when the component mounts.
so here is the above changes:
1
export const useDataActions = () => {
const dispatch = useDispatch();
const fetchData = useCallback(() => dispatch(fetchDataAction), []);
return {
fetchData
};
};
the 2nd approach is:
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, []);
return <div>{data?.name}</data>
}

Can't perform a React state update on an unmounted component when using custom hook for fetching data

I am having this warning in my console whenever I try to run this custom hook I made for fetching data, here is how it looks like:
import { useState, useEffect } from "react";
import axios from "axios";
const useFetch = (url) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
axios
.get(url)
.then((response) => {
setData(response.data);
})
.catch((err) => {
setError(err);
});
}, [url]);
return { data, error };
};
export default useFetch;
And this is the full warning message:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Does anyone know how to fix this?
You need to make sure the component is still mounted before trying to update the state:
import { useState, useEffect } from "react";
import axios from "axios";
const useFetch = (url) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const isMounted=useRef(null)
useEffect(()=>{ isMounted.current=true; return()=>{isMounted.current=false}},[])
useEffect(() => {
axios
.get(url)
.then((response) => {
if (isMounted.current)
{setData(response.data);}
})
.catch((err) => {
if (isMounted.current)
{setError(err);}
});
}, [url]);
return { data, error };
};
export default useFetch;
Set a mutable value to true right after the component mounts, and set it to false when it is going to unmount.
Before every setState check if it is still mounted.

asynchronous context with useEffect in React

im trying to create an api request with the header value, that is received from a context component. However, as soon as the page component is loaded, it throws an Cannot read property '_id' of null exception. Is there a way to run the useEffect function, as soon as the context is loaded?
main component:
import React, { useState, useEffect, useContext } from "react";
import "./overview.scss";
/* COMPONENTS */;
import axios from 'axios';
import { GlobalContext } from '../../components/context/global';
const Overview = () => {
const [bookings, setBookings] = useState([]);
const [loaded, setLoaded] = useState(false);
const [user, setUser] = useContext(GlobalContext);
useEffect(() => {
axios
.get(`/api/v1/bookings/user/${user._id}`)
.then(res => setBookings(res.data))
.catch(err => console.log(err))
.finally(() => setLoaded(true));
}, [user]);
context component:
import React, {useState, useEffect, createContext} from 'react';
import jwt from 'jsonwebtoken';
/* GLOBAL VARIABLES (CLIENT) */
export const GlobalContext = createContext();
export const GlobalProvider = props => {
/* ENVIRONMENT API URL */
const [user, setUser] = useState([]);
useEffect(() => {
const getSession = async () => {
const user = await sessionStorage.getItem('authorization');
setUser(jwt.decode(user));
}
getSession();
}, [])
return (
<GlobalContext.Provider value={[user, setUser]}>
{props.children}
</GlobalContext.Provider>
);
};
The issue here is useEffect is running on mount, and you don't have a user yet. You just need to protect against this scenario
useEffect(() => {
if (!user) return;
// use user._id
},[user])
Naturally, when the Context fetches the user it should force a re-render of your component, and naturally useEffect should re-run as the dependency has changed.
put a condition before rendering you GlobalProvider, for example:
return (
{user.length&&<GlobalContext.Provider value={[user, setUser]}>
{props.children}
</GlobalContext.Provider>}
);
If user is not an array just use this
return (
{user&&<GlobalContext.Provider value={[user, setUser]}>
{props.children}
</GlobalContext.Provider>}
);

Resources