have a question, how to check if react component detached from its parent components?
Let’s say I have a react component, that is subscribed to any async events: WebSocket, timer.
I just want to not waste computer resources on listener to this event, when react component no more in use, and deallocate react component as well.
Any though?
React provides various methods to keep track of a component's lifecycle. And in your case you need to track if a component has unmounted. So, there are 2 approach for this based on the type of component you are using:
Class Component
Use componentUnmount lifecycle method.
class YourComponent extends Component {
constructor(props) {
super(props);
}
componentWillUnmount() {
// this method is invoked immediately before a component
// is unmounted and destroyed. you can perform any necessary
// cleanup in this method, such as invalidating
// timers, canceling network requests,
// or cleaning up subscriptions
}
render() {
return (
<div>
{/* ...contents... */}
</div>
);
}
}
Functional Component
Leverage useEffect hook with cleanup.
const YourComponent = () => {
useEffect(() => {
// rest of code
return () => {
// similar to componentWillUnmount() method, this function
// would invoke at the time of component's unmount.
};
},[]);
return (
<div>
{/* ...contents... */}
</div>
);
}
Related
I have a React class component that renders null by default, and some children after an activate() function is called. Roughly like this:
class MyComponent extends React.Component {
...
activate() {
this.setState({showComponent: true})
}
...
render() {
if (this.state.showComponent) {
return <Child />
} else {
return null
}
}
}
I have an external JavaScript script in which I interact with the MyComponent (call activate function).
To keep it short, my problem is that after calling activate() in this external JS script and trying to access HTMLElements in the <Child /> component right after (with document.getElementById) I get null pointers as the <Child /> component is rendered asynchronously a bit later.
Is there a way to adapt the activate() function in MyComponent to 'wait' for all children in <Child /> to be mounted?
I already tried to exploit the async nature of setState and tried async activate() with await this.setState({showComponent: true}) but this did not change the rendering order.
So in short, is there a way to wait for children to be rendered after you perform a setState update. I guess this is a bit of a special case as it involves conditional rendering and MyComponent initially renders nothing.
Happy about any ideas!:)
How about emitting a custom event on window object inside componentDidMount of Child component and listening for this event in the external javascript and accessing the HTML element in the event handler.
we can pass a function to child component which we can call in child's componentDidMount hooks , so when the child gets mounted , it will trigger that function ... but keep in mind don't update any state variable of parent component in that passed function otherwise it will stuck in a loop (coz as the state variable changes , it will re- render all the child component and eventually component did mount will also be called ...)
class MyComponent extends React.Component {
...
activate() {
this.setState({showComponent: true})
}
callback = ()=>{
// function to be passed in child component
// don't update any state variable here ..
}
...
render() {
if (this.state.showComponent) {
return <Child callback={this.callback} />
} else {
return null
}
}
}
class child extends React.Component {
constructor (props){
super(props)
}
componentDidMount(){
this.props.callback() // this will be called when this child will be
mounted
}
}
Say I have a react hook component
const Child = (props) => {
useEffect(()=>{
// fetch data
...
});
return (
//Displays fetched data
)
}
Then it is used in the parent component
class Parent extends Component {
render () {
... <Child ... /> ...
}
}
My question is, if Parent rerenders when its state changes (but irrelevant to Child), would it cause the Child component to rerender and ultimately fetching data again?
If Child2 has a dependency array, would it suffer the same problem?
const Child2 = (props) => {
let [data, setData] = useState();
useEffect(()=>{
// fetch data
ajax(...).then((result)=>setData(result));
}, [data]);
return (
//Displays fetched data
)
}
Yes, the standard behavior in react is that when a component renders, its children render too. To improve performance you can skip some renders with shouldComponentUpdate on a class component or React.memo on a functional component, but this is a performance optimization, not something you should rely on for skipping effects.
If you're fetching data in a component, you often only want to do it on mount, or only when certain relevant data changes. This is controlled with useEffect's dependency array. To run it only on mount, provide an empty array []. The component will still rerender when the parent component changes, but its effect will not rerun.
I have a route (using React-Router) with component which it renders. Every time this route opened and its component created I need to reset some part of Redux state (one reducer's state in fact), used in this component. This reducer is shared in some other parts of the app, so I use Redux state and not local component's state. So how can I reset the reducer's state every time my component created? I am wondering about best practice to do this.
I think if I'll dispatch actions in componentDidMount method, there will be blinking of previous state for some second.
Can I dispatch action to reset some reducer's state in component's constructor?
Is there any better approach? Can I somehow to set initial state in connect() function, so component will have resetted state each time it created? I check the docs, but I cannot find some argument for this.
Yes, you can dispatch action in constructor to change reducer state
constructor(prop){
super(prop);
prop.dispatch(action);
}
Another approach you can try is setting default props so that you don't need to call reducer(dispatch action)
ButtonComponent.defaultProps = {
message: defaultValue,
};
One possible solution I can think of...
If you could go with the first approach, you can try to stop the previous state being shown while component is being re-rendered with reset state.
The only phase during which you would see the prevState is during the initial render. How about an instance variable to track the render count.
A rough draft.
import React from "react";
import { connect } from "react-redux";
import { add, reset } from "./actions";
class Topics extends React.Component {
renderCount = 0;
componentDidMount() {
// Dispatch actions to reset the redux state
// When the connected props change, component should re-render
this.props.reset();
}
render() {
this.renderCount++;
if (this.renderCount > 1) {
return (
<div>
{this.props.topics.map(topic => (
<h3 id={topic}>{topic}</h3>
))}
</div>
);
} else {
return "Initializing"; // You can return even null
}
}
}
const mapStateToProps = state => ({ topics: state });
const mapDispatchToProps = (dispatch) => {
return {
add(value){
dispatch(add(value));
},
reset(){
dispatch(reset());
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Topics);
Here renderCount is a class variable, that keeps incrementing on component render. Show a fallback UI on first render to avoid previous state from being shown and on second render (due to redux store update), you could display the store data.
A working example added below. I have added an approach to avoid the fallback UI as well. Have a look if it helps.
https://stackblitz.com/edit/react-router-starter-fwxgnl?file=components%2FTopics.js
What does the setState() function run? Does it only run render()?
What does the setState() function run? Does it only run render()
No setState not only calls the render() function but after setState, the following lifecycle functions will run in order depending on what shouldComponentUpdate returns
if shouldComponentUpdate returns true(which is true by default).
1. shouldComponentUpdate
2. componentWillUpdate
3. render()
4. componentDidUpdate
if shouldComponentUpdate returns false(if you have a custom implementation)
1. shouldComponentUpdate
One more thing to know about setState is that, it only triggers the re-render for the current component and all its children(considering no implementation of shouldComponentUpdate for any of its children), Its doesn't trigger a re-render of the parent component and hence the reconcilation doesn't happen for the parent components but only for itself and its children.
A DEMO of what happens when setState is called.
class App extends React.Component {
state = {
count: 0
}
componentWillReceiveProps(nextProps) {
console.log('componentWillReceiveProps parent');
}
shouldComponentUpdate() {
console.log('shouldComponentUpdate parent');
return true;
}
componentWillUpdate() {
console.log('componentWillUpdate parent');
}
render() {
console.log('render parent')
return (
<div>
<Child count = {this.state.count}/>
<button onClick={() => {
console.log('callingsetState');this.setState((prevState) => ({count: prevState.count + 1}))}} >Increase</button>
</div>
)
}
componentDidUpdate() {
console.log('componentDidUpdate parent')
}
}
class Child extends React.Component {
componentWillMount() {
console.log('componentWillMount child');
}
componentDidMount() {
console.log('componentDidMount child');
}
componentWillReceiveProps(nextProps) {
console.log('componentWillReceiveProps child');
}
shouldComponentUpdate() {
console.log('shouldComponentUpdate child');
return true;
}
componentWillUpdate() {
console.log('componentWillUpdate child');
}
render() {
console.log('child')
return (
<div>
<div>{this.props.count}</div>
</div>
)
}
componentDidUpdate() {
console.log('componentDidUpdate child')
}
}
ReactDOM.render(<App/>, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
To add an explanation for the question that #poepje added on your question
What setState does?
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the
updated state. This is the primary method you use to update the user
interface in response to event handlers and server responses.
React has a very good documentation on this function here
You could also see the following answer on how setState works:
setState doesn't update the state immediately
The setState() will run functions in this order:
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
If your component is receiving props it will run the componentWillRecieveProps() function with the above functions.
The first thing React will do when setState is called is merged the object you passed into setState into the current state of the component. This will kick off a process called reconciliation. The end goal of reconciliation is to, in the most efficient way possible, update the UI based on this new state.
To do this, React will construct a new tree of React elements (which you can think of as an object representation of your UI). Once it has this tree, in order to figure out how the UI should change in response to the new state, React will diff this new tree against the previous element tree.
By doing this, React will then know the exact changes which occurred, and by knowing exactly what changes occurred, will able to minimize its footprint on the UI by only making updates where absolutely necessary.
The setState() will run functions in this order:
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
If your component is receiving props it will run the componentWillRecieveProps() function with the above functions.
Just an update to this answer:
https://stackoverflow.com/a/45273993/7310034
(since lifeCycle methods are now updated)
setState() will run functions in this order:
getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapBeforeUpdate (if exists)
componentDidUpdate()
According to this:
http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
Edit: Check out the git repository for a minmal example: https://github.com/maximilianschmitt/blind-lifecycle
I have a component RequireUser that tries to ensure that the user is logged in and will otherwise not render its children. Its parent component, App, should know if a user is required and render a login form if needed.
The problem is, that the App component mounts AFTER the RequireUser component in a tree like this:
App
RequireUser
SomeOtherComponent
In RequireUser's componentDidMount I am triggering an action requireLogin that sets the UserStore's loginRequired variable to true.
This does not update the parent component (App) because it has not yet been mounted and can therefor not register changes to the store.
class RequireUser extends React.Component {
constructor() {
super();
this.state = alt.stores.UserStore.getState();
}
componentDidMount() {
this.unlisten = alt.stores.UserStore.listen(this.setState.bind(this));
if (!this.state.requireUser) {
UserActions.requireUser();
// using setTimeout will work:
// setTimeout(() => UserActions.requireUser());
}
}
componentWillUnmount() {
this.unlisten();
}
render() {
if (this.state.requireUser) {
return <div>I have required your user</div>;
}
return <div>I will require your user</div>;
}
}
class App extends React.Component {
constructor() {
super();
this.state = alt.stores.UserStore.getState();
}
componentDidMount() {
this.unlisten = alt.stores.UserStore.listen(this.setState.bind(this));
}
componentWillUnmount() {
this.unlisten();
}
render() {
return (
<div>
<div>User required? {this.state.requireUser + ''}</div>
<RequireUser />
</div>
);
}
}
Output:
User required? false
I have required your user
If I use setTimeout in RequireUser, App receives the state changes and renders, but only after a flicker:
User required? true
I have required your user
I have the feeling what I am doing is an anti-pattern and I would be grateful for suggestions of a more elegant solution than flickering with setTimeout. Thanks!
My suggested answer is to add this to the App component:
componentDidMount() {
// setup listener for subsequent changes
alt.stores.UserStore.listen(this.onChange);
// grab the current state now that we're mounted
var userStoreState = alt.stores.UserStore.getState();
this.setState(userStoreState);
}
There is no way to avoid the double render. Your RequireUser component already performs two renders.
Initial render of RequireUser
componentDidMount() callback
an action is dispatched
UserStore receives the dispatched action and updates its state
change notification is emitted
RequireUser sets state based on the state change
Second render of RequireUser
But your codebase is still considered Flux, and indeed follows the pattern intended for React apps. Essentially, you have a loading state... a state where we don't actually know if we need to require a user or not. Depending on what UserActions.requireUser() does, this may or may not be desired.
You might consider a refactor
You can fix the double-render if you rewrite RequireUser as a view-only component. This means no listeners nor setting state internally. This component simply renders elements based on the props passed in. This is literally all your RequireUser component would be:
class RequireUser extends React.Component {
render() {
if (this.props.requireUser) {
return <div>I have required your user</div>;
}
return <div>I will require your user</div>;
}
}
You will then make your App component a controller-view. The listener is added here, and any changes to state are propagated downward by props. Now we can setup in the componentWillMount callback. This gives us the single render behavior.
class App extends React.Component {
(other lifecycle methods)
componentWillMount() {
if (!this.state.requireUser) {
UserActions.requireUser();
}
var userStoreState = alt.stores.UserStore.getState();
this.setState(userStoreState);
}
componentDidMount() {
(same as above)
}
render() {
return (
<div>
<div>User required? {this.state.requireUser + ''}</div>
<RequireUser requireUser={this.state.requireUser} />
</div>
);
}
}
Flux architecture and controller-views/views: https://facebook.github.io/flux/docs/overview.html#views-and-controller-views
Your components each only gets the states from your Store once - only during the construction of each components. This means that the states in your components will NOT be in sync with the states in the store
You need to set up a store listeners on your components upon mounting in order to retrieve a trigger from the store and the most up-to-date states. Use setState() to update the states inside the component so render() will be called again to render the up-to-date states
What about putting the store listener in the constructor? That worked for me.