React Re-renders When Parent State is Changed Through Child Component - javascript

In App.js, I am passing setURL(page){ ... } as a prop to HealthForm.
In HealthForm, I have an input field that takes a String of an URL and a button that initiates a fetch call to my backend server and some data is received back in a promise object. I also call that.props.changeUrl(that.state.someURL);inside the promiseStatus function because that's the only place I could place it without getting the following warning:
Warning: Can't call setState (or forceUpdate) 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 the componentWillUnmount method.
However, every time that that.props.changeUrl(that.state.someURL) is called, the page re-renders. Basically -- the input field and the additional functions that were rendered due to the fetch call -- all reset. The url state in App.js gets updated though.
Why does the whole page re-renders when I'm calling the parent props?
The app does not re-render if the line that.props.changeUrl(that.state.someURL) is simply deleted but of-course it doesn't change the App state
I need the page to not re-render because vital information is rendered after the fetch call which cannot be seen since the re-render resets that route.
App.js
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
url: '',
};
this.setURL = this.setURL.bind(this);
}
setURL(link) {
this.setState({
url: link
});
}
render(){
return(
<MuiThemeProvider>
<Router>
<div className="App">
<Route path="/" component={Header}></Route>
<Route path="/health" component={()=>(
<HealthForm changeUrl={this.setURL}/>)}></Route>
<Route path="/path1" component={wForm}></Route>
<Route path="/path2" component={xForm}></Route>
<Route path="/path3" component={yForm}></Route>
<Route path="/path4" component={zForm}></Route>
</div>
</Router>
</MuiThemeProvider>
);
}
}
HealthForm.js
class HealthForm extends React.Component {
constructor(props) {
super(props);
this.state = {
exampleURL: '',
exampleURLError: '',
status: '',
showStatus: false
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
validate = () => {
//…checks for input errors
}
handleChange(event) {
this.setState({
[event.target.name]: event.target.value
});
}
handleSubmit(event) {
event.preventDefault();
const err = this.validate();
let that = this;
if (!err) {
this.setState({
exampleURLError: ''
});
console.log(this.state);
var data = this.state.exampleURL
fetch('htpp://...', {
method: 'POST',
body: JSON.stringify(data)
})
.then((result) => {
var promiseStatus = result.text();
promiseStatus.then(function (value) {
that.setState({
status: value,
showStatus: true
});
that.props.changeUrl(that.state.jarvisURL);
});
}).catch((error) => {
console.log(error);
});
}
}
render() {
return (
<form>
<TextField
...
/>
<br/>
<Button variant="contained" size="small" color="primary" onClick={e => this.handleSubmit(e)} >
Check
</Button>
<br /> <br />
...
</form>
);
}
}
export default HealthForm;

This is happening because you're calling setState() on the App component, causing it to re-render, including re-creating all the routes you've set up. I'm not sure which router you're using exactly but it seems that it is recreating the components under the routes, probably by calling the component function that's passed in as a prop again and getting a new instance of your HealthForm component.
I assume the state you're storing inside App is required by all components in the application and that's why you're putting it there? If not, move it down into the HealthForm component, but if so maybe it's time to think about storing state externally to your components, e.g. in a state container like Redux or something else in a Flux style.
EDIT: I think the root of your problem is here:
<Route path="/health" component={()=>(<HealthForm changeUrl={this.setURL}/>)}></Route>
In the fact that a function is passed as the component prop, resulting in a new instance of the component each time. I can see why you needed to do that, to get the reference to setURL() passed into the HealthForm - it's also something that could be avoided by extracting the state out of the component.

Related

React Typescript: get sessionStorage after reload of page

After the User Sign In, a authentication token is saved in the sessionStorage and the Page gets Reloaded. Now after the reload I want to read that data and store it in a state but the render dont change from <Login /> to <Layout>...
export default class Home extends React.Component<any, State> {
constructor(props: any) {
super(props);
this.state = {
auth: true,
}
this._auth();
}
public render() {
return (
<>
{this.state.auth ?
<Login /> :
<Layout>
...
</Layout>
</>
)
};
private _auth(): void {
if (sessionStorage.getItem("accessGranted") !== "")
this.setState({ auth: true });
else
this.setState({ auth: false });
}
Do not call setState() in constructor(). Instead, if a component
needs to use a local state, assign the initial state to this.state
directly in the constructor.
componentDidMount() invokes immediately after a component mounts.
You can call setState() immediately in componentDidMount() and
triggers an extra rendering, but this happens before the browser
updates the screen, calling render() twice.
read more here
Either you can return the value of your sessionStorage from _auth method and use that value to create the state inside the constructor, OR you can simply call this._auth inside componentDidMount() which is still okay but mainly used for async computations, side effects etc.

ReactJS conditional rendering "Nothing was returned from render" [duplicate]

I have a React app that uses multiple fetch calls throughout different components. In Home page component, I have imported smaller components, all of whom have it's own fetch call.
render() {
return (
<React.Fragment>
<Banner/>
<Services />
<About />
</React.Fragment>
)
}
Banner, Services and About have their own fetch calls to different endpoints, now my question is because the response is a little bit on the slower side, how to wait for all of the child components to fetch data, then render the Homepage component. I have tried to put the state of isLoading and add a loader to wait for components to fetch, but I don't know what to wait for to set isLoading to false.
...how to wait for all of the child components to fetch data, then render the Homepage component
You don't. Instead, you move the fetches to the Homepage component's parent, and then have that parent only render the Homepage component when it has all of the necessary information to do so. In React parlance, this is "lifting state up" (e.g., up the hierarchy to the parent).
While you could render the Homepage in a "loading" form, and have it render its child components in a "loading" form, and have the child components call back to the Home page to say they have their information now, that's more complicated than simply lifting the state up to the highest component that actually needs it (so it knows it can render the Homepage).
As #TJCrowder mentioned in his answer, You'll need to lift your state up and keep it in the parent component. Make a network request there and pass the data to your child component as props. You can read more about lifting-state-up here
class YourComponent extends React.Component {
state = {isLoading: true, isError: false, banner: null, services: null, about: null};
async componentDidMount() {
try {
const [banner, services, about] = await Promise.all([
// all calls
]);
this.setState({ isLoading: false, banner, services, about });
} catch (error) {
this.setState({ isError: true, isLoading: false });
}
}
render() {
if (this.state.isLoading) {
return <div>Loading...</div>
}
return (
<React.Fragment>
<Banner data={this.state.banner} />
<Services data={this.state.services} />
<About data={this.state.about} />
</React.Fragment>
)
}
}
using promises in fetch you could, as suggested, have a isLoaded property state determine whether or not a component should render or not.
class ShouldRender extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
isLoaded: false,
}
}
componentDidMount() {
fetch('http://someresource.com/api/resource')
.then(res => res.json())
.then(data => {
this.state({
data,
isLoaded: true,
});
})
}
render() {
const { isLoaded } = this.state;
if (isLoaded) {
return <MyAwesomeReactComponent />
}
return null;
}
}
So once the state is updated it will trigger a rerendering of the component with the new state that will render the if statement true and you're JSX will appear.

State passed through context returns undefined

I'm trying to make my context work properly.
I've added it and it works as it should if I pass a string as a property to the state. However, I want to pass a prop as the state.
So this works:
export class DataProvider extends Component {
constructor(props) {
super(props);
this.state = {
continent: props.continent,
};
this.updateState = this.updateState.bind(this);
}
updateState() {
this.setState({ continent: this.props.continent});
}
componentDidMount() {
console.log(this.props.continent);
this.updateState();
}
render() {
return (
<DataContext.Provider value={{ state: this.state }}>
{this.props.children}
</DataContext.Provider>
);
}
}
But this does not work (results in undefined)
this.state = {
continent: this.props.continent,
};
Results in "undefined" when I try to access it.
I get the prop from a component named "Africa", which does this:
const Africa = ({}) => {
return (
<div>
<DataProvider continent={["Africa"]} />
........irrelevant code
It successfully passes to my DataProvider component.
But, as I stated, when I try to pass that as a property for my state, it results in "undefined".
class JumbotronPage extends Component {
static contextType = DataContext;
render() {
console.log(this.context)
A(DataProvider), B(Africa), C(JumbotronPage)
I'm not sure if it's because A and B recognizes each other.
B and C does not.
So whenever I access C from A, B gets re-rendered, resulting in giving C nothing as state. Does that make sense?
Please, forgive me for being very green and new to React. I hope I make some sense.
Thanks
Edit:
setState did not seem to work properly. I threw it into a componentDidMount, I can now set string-states, however, as soon as I pass my props to it, it's undefined.
Edit2:
This is part of my App.js:
<Route exact path="/Login" component={Login} />
<Route exact path="/Jumbotron">
<DataProvider>
<JumbotronPage />
</DataProvider>
</Route>
<Route exact path="/CreateNewMemories" component={renderForm} />
Edit3:
I created a gist, if someone has the time an patience to have a look at my abomination.
https://gist.github.com/kalleftw/e79412034eafd29a2e26b1af24149e67
Edi4:
Do I even need to set the state?
this.state = {
continent: props.continent,
};
This seems to work when I log it with componentDidMount.
However, as soon as I try to access the component "Jumbotron", the context there is undefined.
The correct way to update the state is with setState, right, but it is a function. Use this.setState({ ... }) not this.setState = { ... }

Trouble passing props to componentDidMount in child component (React)

I'm having issues passing a prop to a componentDidMount() call in a child component on my React application.
In my App.js I am passing props via Router like below:
App.js
class App extends Component {
state = {
city: ""
}
componentDidMount() {
this.setState({city: this.props.city});
}
render() {
return (
<div>
<Route path="/" exact render = {() => <Projections city={this.state.city} />} />
<Route path="/:id" component={FullPage} />
</div>
);
}
}
In my Projections.js I have the following:
Projections.js
constructor(props) {
super(props);
this.state = {
location: this.props.city
}
}
componentDidMount () {
console.log(this.state.location);
console.log(this.props.city);
}
console.log(this.state);' returns an empty string.console.log(this.props.city);` returns an empty string as well.
But I need to access the value of the city prop within componentDidMount(). console.log(this.props.city); within render() returns the prop, but not in componentDidMount()
Why is this and how do I return props within componentDidMount()?
In the constructor you should reference props, not this.props:
location: props.city
<Route path="/" exact render = {() => <Projections city={this.state.city} {...this.props} />} />
Try passing rest of props in route
this is because you assigned props in constructor that time it may or may not receive actual value. And it gets called only once in a component lifecycle.
You can use componentWillReceiveProps to get props whenever it receive and update state accordingly.
Inside Projections.js
UNSAFE_componentWillReceiveProps(nextProps){
if(nextProps.city){
this.setState({location:nextProps.city})
}
}
Here is working codesand

Using the same component for different Routes with React Router

I have a PageBuilder component that dynamically builds edit/list pages according to a configuration file. I want to have dynamic routes (like "/collection/list", "/collection/edit/123123", "/dashboard", etc.) that use the same PageBuilder component.
I'm having trouble getting this to work - if I'm in "/collection/list" for example, when clicking on a link to "/collection/edit/1231" doesn't work. Only a refresh to that URL works (and vice-versa).
I tried putting my initialization code PageBuilder's componentWilLReceiveProps but it seems to call it every second.
My routes look like this:
<Route path="/" component={App}>
<IndexRedirect to="/dashboard" />
<Route path="/:page/:collection/:action(/:entity_id)" component={PageBuilder} />
<Route path="/:page" component={PageBuilder} />
</Route>
And my PageBuilder:
constructor(props) {
super(props);
this.createSectionsHTML = this.createSectionsHTML.bind(this);
this.onChange = this.onChange.bind(this);
this.onSave = this.onSave.bind(this);
}
getPageName() {
return this.props.params.page.replace(/-/g, '_').toLowerCase();
}
componentWillReceiveProps(props) {
this.action = this.props.params.action;
}
componentWillMount() {
let pageName = this.getPageName();
this.props.dispatch(setInitialItem(pageName));
}
componentDidMount() {
let pageName = this.getPageName();
let { collection, entity_id } = this.props.params;
if (collection && entity_id) {
let { dispatch } = this.props;
dispatch(getCollectionEntity(collection, entity_id, pageName));
}
}
Any ideas of how to re-render the page each time I redirect to a different route?
It would be great if I could unmount and re-mount the component when redirecting, but I'm not sure how to go about telling React Router to do that....
Thanks!
Make this.state such that it will control how your component gets rendered.
Now, within componentWillReceiveProps, check the nextProps argument
componentWillReceiveProps(nextProps, nextState) {
if( <check in nextProps if my route has changed> ) {
let newState = Object.assign({}, this.state);
// make necessary changes to the nextState Object by calling
// functions which would change the rendering of the current page
this.setState({ nextState });
}
}
This would make componentWillReceiveProps take action only when the route changes.
Now in your render function,
render() {
const { necessary, variables, to, render } = this.state;
let renderVariables = this.utilityFunctionsReqToRender(someArgs);
return (
<toRenderJsx>
...
</toRenderJsx>
)
}
This would make your component "refresh" whenever the route changes.
componentWillReceiveProps is deprecated since React 16.3.0
(as says a warning in the browser console)
Reference :
https://reactjs.org/docs/react-component.html#componentdidupdate
So componentDidUpdate can be used to get the new state and reload data depending on params
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("componentDidUpdate " +prevState.id);
reloadData(prevState.id);
}

Categories