Too many re-render problems with React - javascript

I have some cards in my application that can lead to another pages through clicks. So I have a main component that contains a button like this:
function MainComponent(props) {
.
.
.
const handleClick = (key) => {
history.push("/exampleurl/" + key);
};
Then according to the key passed, I have to make a request that gives me some information required to display it. As default I have my initial state as null, and when it completes the request, it changes to the object I got. But as soon as I click on the card, I get the re-render error.
function MyComponent(props) {
let { key } = useParams();
const [myObject, setMyObject] = React.useState(null)
useEffect(() => {
axios.get('/myendpoint/' + key).then( response => {
let myObject = response.data
setMyObject(myObject)
})
}, [key])
I suppose that the solution is avoiding the key value to update when it changes the state. But i am not finding the solution to this trouble.
Edit: The route that leads to the components:
<Route path="/inbox">
<MainComponent />
</Route>
<Route path="/exampleurl/:key">
<NewComponent />
</Route>

I think the problem is related to the handleClick function.
Every time this method is called, you push a new entry to the history stack. Which analyze your defined routes and render the linked component. In your case, it is the same component, but I am not sure if the router is capable to determine it, therefore I would expect a re-render.
Maybe a solution would be to include another state which is responsible to inform the component of the current obj being displayed on the screen. So key will be responsible only for the route parameter and this new state will be responsible for the internal navigation.
function MyComponent(props) {
let { key } = useParams();
const [myObject, setMyObject] = React.useState(null)
const [displayedObj, setDisplayedObj] = React.useState('');
useEffect(() => {
axios.get('/myendpoint/' + key).then( response => {
let myObject = response.data
setMyObject(myObject)
setDisplayedObj(key)
})
}, [key, displayedObj]) // we listen for displayedObj too
and then in the handleClick we update this new state. This will trigger the useEffect and therefore update the myObject state to the new value:
const handleClick = (key) => {
setDisplayedObj(key);
// This will trigger the useEffect and refresh
// the data displayed without reloading the page
};

Related

Reactjs closure when passing state to component

I got a react functional component:
const DataGrid = (props) =>
{
const [containerName, setContainerName] = useState("");
const [frameworkComponents, setFrameworkComponents] = useState(
{customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
useEffect(async () =>
{
if(props.containerName && props.containerName !== "")
{
setContainerName(props.containerName);
}
},[props.containerName]);
.
.
.
const onDeleteSetting = async (settingKey) =>
{
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName); //HERE THE CONTAINER NAME IS EMPTY
...
}
return (
<UxDataGrid
frameworkComponents={frameworkComponents}/>
);
The container name inside useEffect exists and is not empty. As you can see in the comment in onDeleteSetting, the containerName is empty when this callback is invoked. I tried adding this to the useEffect after setContainerName:
setFrameworkComponents({customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
That didn't work.
How can I get the name inside the callback? There is no special need to leave that frameworkComponents struct in the state.. it can also be moved to somewhere else if you think its better
Try this in your useEffect, update the onDeleteSetting function with the new containerName when it's updated
.....
useEffect(async() => {
if (props.containerName && props.containerName !== "") {
setContainerName(props.containerName);
// move this function here
const onDeleteSetting = async(settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
// use props.containerName since the state update is async
console.log(props.containerName);
...
}
// update your components with the updated functions
setFrameworkComponents(prevComponents => ({
...prevComponents,
editButton: params =>
<ViewAndDeleteSetting
{...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}
/>,
}));
}
}, [props.containerName]);
.....
This should provide the updated state with the updated function, if it works, I can add more details.
You almost certainly shouldn't be storing it in state. Props are essentially state controlled by the parent. Just use it from props. Copying props to state is usually not best practice.
If you're looking at one of the very rare situations where it makes sense to set derived state based on props, this page in the documentation tells you how to do that with hooks. Basically, you don't use useEffect, you do your state update right away.
Here's a full quote from the linked documentation:
How do I implement getDerivedStateFromProps?
While you probably don’t need it, in rare cases that you do (such as implementing a <Transition> component), you can update the state right during rendering. React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.
Here, we store the previous value of the row prop in a state variable so that we can compare:
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
This might look strange at first, but an update during rendering is exactly what getDerivedStateFromProps has always been like conceptually.
If you did it the same way they did in that example, your component would still render with containerName set to the default state (""), it's just that it will then almost immediately re-render with the updated containerName. That makes sense for their example of a transition, but you could avoid that by making the prop's initial value the state's initial value, like this:
const DataGrid = (props) => {
const [containerName, setContainerName] = useState(props.containerName); // *** ONLY USES THE INITIAL PROP VALUE
const [frameworkComponents, setFrameworkComponents] = useState(
// ...
});
// *** Updates the state value (on the next render) if the prop changes
if (containerName !== props.containerName) {
setContainerName(props.containerName);
}
// ...
};
Every time the containerName prop changes, though, your component will render twice, which brings us back full circle to: Don't store it in state, just use it from props. :-)
Stepping back and looking at the component as a whole, I don't think you need any state information at all, but if your goal is to avoid having the frameworkComponents you pass UxDataGrid change unnecessarily, you probably want useMemo or React.memo rather than state.
For instance, with useMemo (but keep reading):
const DataGrid = ({containerName}) => {
const frameworkComponents = useMemo(() => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return {
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
};
}, [containerName]);
return (
<UxDataGrid frameworkComponents={frameworkComponents} />
);
};
But if componentName is your only prop, it may well be even simpler with React.memo:
const DataGrid = React.memo(({containerName}) => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return (
<UxDataGrid frameworkComponents={{
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
}} />
);
});
React.memo memoizes your component, so that your component function is only ever called again when the props change. Since everything in the component needs to update based on the componentName prop changing, that looks like a good match (but I don't know what UxDataGrid is).
The problem was with how I tried passing props to ViewAndDeleteSetting. If you want to pass prop to a cell rendered component, you shouldn't be doing it in frameworkComponents, but rather you need to do it in the column definition like this:
useEffect(() =>
{
let columns = [{headerName: '', cellRenderer: 'editButton', width: 90, editable: false,
cellRendererParams: {
openAddConfigurationsWindow: openAddConfigurationsWindow,
onDeleteSetting: onDeleteSetting
}},
.. other columns
]
setColumnDefinition(columns);
},[props.containerName]);
The columns with the cellRendererParams do gets recreated in the useEffect when the name changes, and then the component can access this params regularly via its props

How to trigger useEffects before render in React?

I have a prop being passed from a parent component to a child component which changes based on the user's input.
I want to trigger a data fetch in the child component when that prop changes before the child component is rendered. How can I do it?
I tried in the following manner by using useEffects(()=>{},[props.a, props.b]) but that is always called after the render. Please help!
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function parentComponent() {
const [inputs, setInputs] = useState({ a: "", b: "" });
return (
<>
<input
value={inputs.a}
onChange={(event) => {
const value = event.target.value;
setInputs((prevState) => {
return { ...prevState, a: value };
});
}}
/>
<input
value={inputs.b}
onChange={(event) => {
const value = event.target.value;
setInputs((prevState) => {
return { ...prevState, b: value };
});
}}
/>
<ChildComponent a={inputs.a} b={inputs.b} />
</>
);
}
function ChildComponent(props) {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState({});
useEffect(() => {
console.log("updating new data based on props.a: " + props.a);
setData({ name: "john " + props.a });
return () => {};
}, [props.a, props.b]);
useEffect(() => {
console.log("data successfully changed");
console.log(data);
if (Object.keys(data).length !== 0) {
setIsLoading(false);
}
return () => {};
}, [data]);
function renderPartOfComponent() {
console.log("rendering POC with props.a: " + props.a);
return <div>data is: {data.name}</div>;
}
return (
<div className="App">{isLoading ? null : renderPartOfComponent()}</div>
);
}
In the console what I get is:
rendering POC with props.a: fe
rendering POC with props.a: fe
updating new data based on props.a: fe
rendering POC with props.a: fe
rendering POC with props.a: fe
data successfully changed
Object {name: "john fe"}
rendering POC with props.a: fe
rendering POC with props.a: fe
If you know how I can make the code more efficient, that would be a great help as well!
Here's the codesandbox link for the code: https://codesandbox.io/s/determined-northcutt-6z9f8?file=/src/App.js:0-1466
Solution
You can use useMemo, which doesn't wait for a re-render. It will execute as long as the dependencies are changed.
useMemo(()=>{
doSomething() //Doesn't want until render is completed
}, [dep1, dep2])
You can use function below:
// utils.js
const useBeforeRender = (callback, deps) => {
const [isRun, setIsRun] = useState(false);
if (!isRun) {
callback();
setIsRun(true);
}
useEffect(() => () => setIsRun(false), deps);
};
// yourComponent.js
useBeforeRender(() => someFunc(), []);
useEffect is always called after the render phase of the component. This is to avoid any side-effects from happening during the render commit phase (as it'd cause the component to become highly inconsistent and keep trying to render itself).
Your ParentComponent consists of Input, Input & ChildComponent.
As you type in textbox, ParentComponent: inputs state is modified.
This state change causes ChildComponent to re-render, hence renderPartOfComponent is called (as isLoading remains false from previous render).
After re-render, useEffect will be invoked (Parent's state propagates to Child).
Since isLoading state is modified from the effects, another rendering happens.
I found the solution by creating and maintaining state within the ChildComponent
So, the order of processes was this:
props modified -> render takes place -> useEffect block is executed.
I found the workaround by simply instantiating a state within the childComponent and making sure that the props state is the same as the one in the child component before rendering, else it would just show loading... This works perfectly.
Nowadays you can use useLayoutEffect which is a version of useEffect that fires before the browser repaints the screen.
Docs: https://beta.reactjs.org/reference/react/useLayoutEffect

In React how can I append values to functional state when it is a dependency of useEffect without triggering another API call?

I have a todo list app which users can read and save items to. Here, Todo is a functional component that queries an API for the users current items on their list using the useEffect() hook. When a successful response is received the data is added to the component's state using useState() and rendered as part of the ItemList component.
When a user submits the form within the AddItemForm component a call back is fired that updates the state of newItem, a dependency of useEffect, which triggers another call to the API and a re-render of the component.
Logically, everything above works. However, it seems wrong to make an extra request to the API simply to receive the data that is already available but I can't find the correct pattern that would allow me to push the item available in useCallback to the items array without causing useEffect to loop infinitely yet still update the ItemList component.
Is there away for my app to push new date from the form submission to items array whilst updating the view and only calling the API once when the page loads?
function Todo() {
const [items, setItems] = useState();
const [newItem, setNewItem] = useState();
useEffect(() => {
fetch('https://example.com/items').then(
(response) => {
setItems(response.data.items);
}, (error) => {
console.log(error);
},
);
}, [newItem]);
const updateItemList = useCallback((item) => {
setNewItem(item);
});
return (
<>
<AddItemForm callback={updateItemList} />
<ItemList items={items} />
</>
);
}
function ItemList(props) {
const { items } = props;
return (
<div>
{ items
&& items.map((item) => <p>{item.description}</p>)}
</div>
);
}
Call API only on start by removing newItem from useEffect(...,[]).
Then add item to the items by destructuring in setItems:
const updateItemList = (item) => {
setItems([...items, item]);
}

React: how can I force state to update in a functional component?

This function component has a template method that calls onChangeHandler, which accepts a select value and updates state. The problem is, state does not update until after the render method is called a second time, which means the value of selected option is one step ahead of the state value of selectedRouteName.
I know there are lifecycle methods in class components that I could use to force a state update, but I would like to keep this a function component, if possible.
As noted in the code, the logged state of selectedRouteDirection is one value behind the selected option. How can I force the state to update to the correct value in a functional component?
This question is not the same as similarly named question because my question asks about the actual implementation in my use case, not whether it is possible.
import React, { useState, Fragment, useEffect } from 'react';
const parser = require('xml-js');
const RouteSelect = props => {
const { routes } = props;
const [selectedRouteName, setRouteName] = useState('');
const [selectedRouteDirection, setRouteDirection] = useState('');
//console.log(routes);
const onChangeHandler = event => {
setRouteName({ name: event.target.value });
if(selectedRouteName.name) {
getRouteDirection();
}
}
/*
useEffect(() => {
if(selectedRouteName) {
getRouteDirection();
}
}); */
const getRouteDirection = () => {
const filteredRoute = routes.filter(route => route.Description._text === selectedRouteName.name);
const num = filteredRoute[0].Route._text;
let directions = [];
fetch(`https://svc.metrotransit.org/NexTrip/Directions/${num}`)
.then(response => {
return response.text();
}).then(response => {
return JSON.parse(parser.xml2json(response, {compact: true, spaces: 4}));
}).then(response => {
directions = response.ArrayOfTextValuePair.TextValuePair;
// console.log(directions);
setRouteDirection(directions);
})
.catch(error => {
console.log(error);
});
console.log(selectedRouteDirection); // This logged state is one value behind the selected option
}
const routeOptions = routes.map(route => <option key={route.Route._text}>{route.Description._text}</option>);
return (
<Fragment>
<select onChange={onChangeHandler}>
{routeOptions}
</select>
</Fragment>
);
};
export default RouteSelect;
Well, actually.. even though I still think effects are the right way to go.. your console.log is in the wrong place.. fetch is asynchronous and your console.log is right after the fetch instruction.
As #Bernardo states.. setState is also asynchronous
so at the time when your calling getRouteDirection();, selectedRouteName might still have the previous state.
So to make getRouteDirection(); trigger after the state was set.
You can use the effect and pass selectedRouteName as second parameter (Which is actually an optimization, so the effect only triggers if selectedRouteName has changed)
So this should do the trick:
useEffect(() => {
getRouteDirection();
}, [selectedRouteName]);
But tbh.. if you can provide a Stackblitz or similar, where you can reproduce the problem. We can definitely help you better.
setState is asynchronous! Many times React will look like it changes the state of your component in a synchronous way, but is not that way.

How to call a function in componentDidMount dynamically?

Consider this
componentDidMount() {
const { currentUserId, userList } = this.props;
//I get these from redux' mapStateToProps function;
//these are not passed down as regular props.
Mousetrap.bind(['shift+right'], () =>
nextUser(currentUserId, userList)
);
}
Say I have 10 users in my list, and we start with user 1. When I start the app, it will go from user 1 to user 2; However, it won't go any further since the value of currentUserId will eternally be user 1.
How can I circumvent this and have the arguments be dynamic, so that the arguments will be updated?
Edit: currentUserId & userList are passed on to the Component via Redux
If you want things to be dynamic, consider copying the currentUserId to the state in the constructor and adjusting the state as needed with this.setState({currentUserId: })
Example:
constructor(props) {
super(props);
this.state = { currentUserId: props.currentUserId };
}
componentDidMount() {
const { userList } = this.props;
const { currentUserId } = this.state;
Mousetrap.bind(['shift+right'], () =>
nextUser(currentUserId, userList)
);
}
I don't know how your nextUser function works but it if it returns the next userId, you could do:
Mousetrap.bind(['shift+right'], () =>
this.setState({currentUserId:nextUser(currentUserId, userList)});
);
in the componentDidMount().
If you need to update the function, after the component is mounted, you need to use componentDidUpdate to react to prop changes during the life of a component.
componentDidMount will be called once (when the component became visible) and your function will be set to the current prop => onClick will select the second user.
After that, your props change (currentUserId will now be the second user), but you don't update your function. That is why it will be stuck on the second user.
To achieve what you intend to do, use componentDidUpdate in combination with componentDidMount like this:
componentDidUpdate(prevProps) {
const { currentUserId, userList } = this.props;
if(prevProps.currentUserId !== currentUserId || prevProps.userList !== userList ) {
Mousetrap.bind(['shift+right'], () =>
nextUser(currentUserId, userList)
);
}
}
As an alternative, you could also remove the parameters from nextUser and let the action/reducer handle the update by setting the currentUserId within the reducer directly.
Hope this helps.
Happy coding.

Categories