Infinite loop because of misusing ReactJS Router, useEffect hook - reactjs

In the child component, I'm calling an API then pushing to a different route, this causes App component to re-render because its location prop key (location.key) keeps changing causing the child to keep re-mounting ( I know re-rendering the parent should only re-render the child not re-mount it, but this is what's happening when I tried passing empty array to the second arg in useEffect, useEffect(() => {}, []);
I can stop App from re-rendering through shouldComponenUpdate, and update it only when location.pathname changes not when the key changes, but is there a better way?
Parent.js
constructor(props) {
super(props);
const {
....,
....,
location,
}= props;
if (props.location.pathname.includes('/x');
push('/x');
}
shouldComponentUpdate(nextProps, nextState) {
if (
this.props.location.pathname !== nextProps.location.pathname ||
this.state.match.path !== nextState.match.path
) {
return true;
}
return false;
}
render() {
<Child/>
}
Child.js
useEffect(() => {
if (Y.ID) {
fetchYDetails();
}
}, [fetchYDetails, Y.ID]);
Model.js
effects: dispatch => ({
fetchYDetails() {
const response = await request({ ...});
if (!response.error) {
dispatch(push('/z'));
};
}
});
Thank you!

Related

Pass parameter to page, data not updating

I have a navigation component where I'm passing a parameter to another page, the parameter is getting passed, however, the data in the dropdown is not updating for the passed ID:
nav:
<Link to='/service/ServiceAppointment/${car.Make}'> { serviceAppointment } </Link>
appointment page:
const ScheduleAppointment = () => {
const { id } = useParams();
console.log (id); //I can see the ID passed to the page in the console
useEffect(() => {
console.log(id); //the ID is not there
scheduleAppointment(id);
});
const Appointment= () => {
//call to API for open dates
//the ID never gets here
}
}
Router:
<Route exact path='/service/appointment/:id' component={ ScheduleAppointment } />
how can I get the appointment page to change when a new ID is passed to it?
Dependencies argument of useEffect is useEffect(callback, dependencies)
Let's explore side effects and runs:
Not provided: the side-effect runs after every rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs after EVERY rendering
});
}
An empty array []: the side-effect runs once after the initial rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs ONCE after initial rendering
}, []);
}
Has props or state values [prop1, prop2, ..., state1, state2]: the side-effect runs only when any dependency value changes.
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState('');
useEffect(() => {
// Runs ONCE after initial rendering
// and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state]);
}
in your case try this way
useEffect(() => {
console.log(id); //the ID is not there
scheduleAppointment(id);
},[id]);
Please update the below link instead of your code
<Link to=`/service/ServiceAppointment/${car.Make}`> { serviceAppointment } </Link>
I hope it will work for you! Thanks.

Update React context from child component

I am not able to update my context object from child component. Here is my provider file for creating react context and that component is wrapped over whole application in root app.tsx file.
I can fetch context in child components but I am not sure how to update it.
const CorporateContext = React.createContext(undefined);
export const useCorporateContext = () => {
return useContext(CorporateContext);
};
export const CorporateProvider = ({ children }) => {
const [corporateUserData, setCorporateUserData] = useState(undefined);
useEffect(() => {
const localStorageSet = !!localStorage.getItem('corporateDetailsSet');
if (localStorageSet) {
setCorporateUserData({corporateId: localStorage.getItem('corporateId'), corporateRole: localStorage.getItem('corporateRole'), corporateAdmin: localStorage.getItem('corporateAdmin'), corporateSuperAdmin: localStorage.getItem('corporateSuperAdmin')});
} else {
(async () => {
try {
const json = await
fetchCorporateUserDetails(getClientSideJwtTokenCookie());
if (json.success !== true) {
console.log('json invalid');
return;
}
setCorporateUserData({corporateId: json.data.corporate_id, corporateRole: json.data.corporate_role, corporateAdmin: json.data.corporate_role == 'Admin', corporateSuperAdmin: json.data.corporate_super_admin});
addCorporateDetailsToLocalStorage(corporateUserData);
} catch (e) {
console.log(e.message);
}
})();
}
}, []);
return (
<CorporateContext.Provider value={corporateUserData}>
{children}
</CorporateContext.Provider>
);
};
export default CorporateProvider;
Update: Here is what I changed and context seems to be updated (at least it looks updated when I check in chrome devtools) but child components are not re-rendered and everything still stays the same:
I changed react context to:
const CorporateContext = React.createContext({
corporateContext: undefined,
setCorporateContext: (corporateContext) => {}
});
Changed useState hook in a component:
const [corporateContext, setCorporateContext] = useState(undefined);
Passed setState as property to context:
<CorporateContext.Provider value={{corporateContext , setCorporateContext}}>
{children}
</CorporateContext.Provider>
You can also pass in setCorporateUserData as a second value to the provider component as such:
//...
return (
<CorporateContext.Provider value={{corporateUserData, setCorporateUserData}}>
{children}
</CorporateContext.Provider>
);
You can then destructure it off anytime you want using the useContext hook.
How I resolved this issue:
Problem was that I have cloned corporateContext object and changed property on that cloned object and then I passed it to setCorporateContext() method. As a result of that, context was updated but react didnt noticed that change and child components were not updated.
let newCorporateContext = corporateContext;
newCorporateContext.property = newValue;
setCorporateContext(newCorporateContext);
After that I used javascript spread operator to change the property of corporateContext inside setCorporateContext() method.
setCorporateContext({...corporateContext, property: newValue);
After that, react noticed change in hook and child components are rerendered!

Lifecycle - fetch data only after loading complete

I have a component that needs to wait for the user's token to be validated first before fetching data. Right now, my component only renders when authLoading is false but I'm fetching my data in componentDidMount so sometimes the token is not validated yet and I get an unauthorized error. How should I refactor this so everything should wait for authLoading to finish?
componentDidMount() {
// if (!authLoading) { <----- Sometimes the component mounts and the fetchData function doesn't get called
this.fetchData();
// }
}
render() {
const { authLoading } = this.props;
if (!authLoading) {
return (
<Component1 />
);
}
else {
return null
}
}
You want to fetch your data only once, but also only after this.props.authLoading becomes false.
Check out componentDidUpdate(), which will allow you to update any time your props change.
Your implementation might look something like this:
componentDidUpdate(prevProps) {
if (!this.props.authLoading && prevProps.authLoading) { // tokens just loaded!
this.fetchData();
}
}
If you can convert your component to be a functional component then using hooks would me this simple.
const MyComponent = ({ authLoading }) => {
const [authLoaded, setAuthLoaded] = useState();
useEffect(
() => {
fetchData();
// process data
setAuthLoaded(/* some value based on fetched data I assume */);
},
[authLoading], // triggers effect anytime value of `authLoading` changes
);
return authLoaded ? <Component1 /> : null;
};

How can I check if the component is unmounted in a functional component?

A callback function sets the component state. But sometimes subscription which supplies the data need to end. Because callback is executed asynchronously, it's not aware if the subscription ends just after making the service call (which executes the callback function).
Then I see following error in the console:
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.
Is there a way to access the component state, even if I am in the callback function?
This would be the steps:
subscribe with parameters
unsubscribe
component is unmounted
subscribed service executes the callback function
callback functio sets state in an unmounted component and it gives error above
You can use a ref like this:
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => { mounted.current = false; };
}, []);
Then in your callback you can check if mounted.current === false and avoid setting the state
Here is some pseudo code how you can use useEffect to see if a component is mounted.
It uses useEffect to listen to someService when it receives a message it checks if the component is mounted (cleanup function is also called when component unmounts) and if it is it uses setServiceMessage that was created by useState to set messages received by the service:
import { useState, useEffect } from 'react';
import someService from 'some-service';
export default props => {
const userId = props.userId;
const [serviceMessage, setServiceMessage] = useState([]);
useEffect(
() => {
const mounted = { current: true };
someService.listen(
//listen to messages for this user
userId,
//callback when message is received
message => {
//only set message when component is mounted
if (mounted.current) {
setServiceMessage(serviceMessage.concat(message));
}
});
//returning cleanup function
return () => {
//do not listen to the service anymore
someService.stopListen(userId);
//set mounted to false if userId changed then mounted
// will immediately be set to true again and someService
// will listen to another user's messages but if the
// component is unmounted then mounted.current will
// continue to be false
mounted.current = false;
};
},//<-- the function passed to useEffects
//the function passed to useEffect will be called
//every time props.userId changes, you can pass multiple
//values here like [userId,otherValue] and then the function
//will be called whenever one of the values changes
//note that when this is an object then {hi:1} is not {hi:1}
//referential equality is checked so create this with memoization
//if this is an object created by mapStateToProps or a function
[userId]
);
};
This hook (insired from Mohamed answer) solves the problem in a more elegant maner:
function useMounted() {
const mounted = useMemo(() => ({ current: true }), []);
useEffect(() => {
return () => { mounted.current = false}
}, [mounted]);
return mounted;
}
(Updated to use useMemo instead of useRef for readability).
You can return a function from useEffect, which will be fired when a functional component unmount.
Please read this
import React, { useEffect } from 'react';
const ComponentExample = () => {
useEffect(() => {
// Anything in here is fired on component mount.
return () => {
// Anything in here is fired on component unmount.
}
}, [])
}
I found the accepted answer to this question hard to read, and React provides their own documentation on just this question. Their example is:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
I've created a component I call <Fade> that will fade in/out any children its given. Note that it relies on bootstrap's .fade and .show classes, though these could easily be implemented without bootstrap.
const Fade: React.FC<{}> = ({ children }) => {
const [ className, setClassName ] = useState('fade')
const [ newChildren, setNewChildren ] = useState(children)
useEffect(() => {
setClassName('fade')
const timerId = setTimeout(() => {
setClassName('fade show')
setNewChildren(children)
}, TIMEOUT_DURATION)
return () => {
clearTimeout(timerId)
}
}, [children])
return <Container fluid className={className + ' p-0'}>{newChildren}</Container>
}
It all boils down to one rule: unsubscribe from your asynchronous tasks in the cleanup function returned from useEffect.

infinite loop when dispatching in componentWillReceiveProps

I have a Profile component that is loaded by react-router (path="profile/:username") and the component itself looks like this:
...
import { fetchUser } from '../actions/user';
class Profile extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const { username } = this.props;
this.fetchUser(username);
}
componentWillReceiveProps(nextProps) {
const { username } = nextProps.params;
this.fetchUser(username);
}
fetchUser(username) {
const { dispatch } = this.props;
dispatch(fetchUser(username));
}
render() {...}
}
export default connect((state, ownProps) => {
return {
username: ownProps.params.username,
isAuthenticated: state.auth.isAuthenticated
};
})(Profile);
And the fetchUser action looks like this (redux-api-middleware):
function fetchUser(id) {
let token = localStorage.getItem('jwt');
return {
[CALL_API]: {
endpoint: `http://localhost:3000/api/users/${id}`,
method: 'GET',
headers: { 'x-access-token': token },
types: [FETCH_USER_REQUEST, FETCH_USER_SUCCESS, FETCH_USER_FAILURE]
}
}
}
The reason I added componentWillReceiveProps function is to react when the URL changes to another :username and to load that users profile info. At a first glance everything seems to work but then I noticed while debugging that componentWillReceiveProps function is called in a infinite loop and I don't know why. If I remove componentWillReceiveProps then the profile doesn't get updated with the new username but then I have no loops problem. Any ideas?
Try adding a condition to compare the props. If your component needs it.
componentWillRecieveProps(nextProps){
if(nextProps.value !== this.props.value)
dispatch(action()) //do dispatch here
}
Your componentWillReceiveProps is in an infinite loop because calling fetchUser will dispatch an action that will update the Props.
Add a comparison to check if the specific prop changes before dispatching the action.
EDIT:
In React 16.3+ componentWillReceiveProps will be slowly deprecated.
It is recommended to use componentDidUpdate in place of componentWillReceiveProps
componentDidUpdate(prevProps) {
if (this.props.params.username !== prevProps.params.username) {
dispatch(fetchUser(username));
}
}
See https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data-when-props-change
If you have react routes with some path params like profile/:username,
You can simply compare the props.location.pathname
componentWillReceiveProps(nextProps){
if(nextProps.location.pathname !== this.props.location.pathname){
dispatch()
}
}

Resources