Inside componentDidUpdate, alert is not triggered when props change.
You can run this code in codePen (https://codepen.io/anon/pen/BMmqKe?editors=1011)
const state = observable({
isOpen: false
})
state.close = function () {
state.isOpen = false
}
state.open = function () {
state.isOpen = true
}
const Home = observer(class home extends Component {
componentDidUpdate(prevProps) {
if (this.props.store.isOpen !== prevProps.store.isOpen) {
// this line is not executed
alert('reset');
}
}
render() {
return (
this.props.store.isOpen
? <button onClick={this.props.store.close}>close</button>
: <button onClick={this.props.store.open}>open</button>
);
}
})
render(<Home store={state} />, document.getElementById('app'))
this.props.store and prevProps.store will always refer to the same store object, so isOpen will always be the same on both sides of the equals operator.
You could instead use the componentWillReact life cycle hook to run some code when the component will update because of a change in an observable.
const Home = observer(class home extends Component {
componentWillReact() {
alert('reset');
}
render() {
return (
this.props.store.isOpen
? <button onClick={this.props.store.close}>close</button>
: <button onClick={this.props.store.open}>open</button>
);
}
})
You can just change the if statement in you componentDidUpdate():
!this.props.store.isOpen ? alert("reset"): null;
Related
So I'm trying to simulate the state by clicking a button. The 'before' status seems to have the correct value, but why is the 'after' not displaying the correct value even if the setState is already hit by the code?
class App extends Component {
constructor(){
super()
this.state = {isLoggedIn: false}
this.OnClick = this.OnClick.bind(this);
}
OnClick(){
this.setState(prev =>
{
return (prev.isLoggedIn = !this.state.isLoggedIn);
})
console.log(`After setState value: ${this.state.isLoggedInstrong text}`) // setState is done, why is this.state displaying incorrect value?
}
render()
{
console.log(`Before setState value: ${this.state.isLoggedIn}`)
return <Login isLoggedIn={this.state.isLoggedIn} OnClick={this.OnClick} />
}
}
import React from "react";
class Login extends React.Component
{
render()
{
const {isLoggedIn, OnClick} = this.props;
return (
<div>
<button onClick={OnClick} >{isLoggedIn ? "Log Out" : "Log In"} </button>
</div>
)
}
}
export default Login;
OUTPUT:
"Before setState value: false"
(Initial display, button value is: Log In)
When button is clicked:
"After setState value: false" <------ why false when setState has been hit already? Not real-time update until Render is called?
"Before setState value: true"
(Button value is now: Log Out)
The main problem I see in your code is you’re trying to mutate the state.
this.setState(prev => {
return (prev.isLoggedIn = !this.state.isLoggedIn);
})
You have to merge to the state not mutate it. You can do it simply by returning an object like this.
this.setState((prev) => {
return { isLoggedIn: !prev.isLoggedIn };
});
This will fix all the weird behaviours in your code.
Full Code
App.js
import { Component } from "react";
import Login from "./Login";
class App extends Component {
constructor() {
super();
this.state = { isLoggedIn: false };
this.OnClick = this.OnClick.bind(this);
}
OnClick() {
this.setState((prev) => {
return { isLoggedIn: !prev.isLoggedIn };
});
console.log(`After setState value: ${this.state.isLoggedIn}`);
}
render() {
console.log(`Before setState value: ${this.state.isLoggedIn}`);
return <Login isLoggedIn={this.state.isLoggedIn} OnClick={this.OnClick} />;
}
}
export default App;
Login.js
import { Component } from "react";
class Login extends Component {
render() {
const { isLoggedIn, OnClick } = this.props;
return (
<div>
<button onClick={OnClick}>{isLoggedIn ? "Log Out" : "Log In"} </button>
</div>
);
}
}
export default Login;
CodeSandbox - https://codesandbox.io/s/setstate-is-not-update-the-state-69141369-efw46
try this
this.setState({
isLoggedIn:!this.state.isLoggedIn
})
or
this.setState(prev => ({
isLoggedIn:!prev.isLoggedIn
}))
In React i have a button that when clicked will decrement state object value by one. When state value is decremented to 0, it should activate alert method but for some reason it only activates after state value one has reached to -1 not 0.
Any help?
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor(props){
super(props);
this.state = {
numbers:{
one:1
},
};
}
decrement = () => {
this.setState(prevState => {
return {
numbers:{
...prevState.numbers,
one: prevState.numbers.one - 1
}
}
}, () => console.log(
this.state.numbers.one,
));
if(this.state.numbers.one===0){
alert('test');
}
}
render() {
return (
<div className="App">
<div>{this.state.numbers.one}</div>
<br/>
<button onClick={this.decrement}>ok</button>
</div>
);
}
}
export default App;
Like i was saying in the comments. setState is async, you need to wait for the state change or use a lifecycle method.
So in your decrement function you can alert in the callback you are already using, which has the updated state value there.
decrement = () => {
this.setState(prevState => {
return {
numbers:{
...prevState.numbers,
one: prevState.numbers.one - 1
}
}
}, () => {
console.log(this.state.numbers.one)
if(this.state.numbers.one===0){
alert('test');
}
});
}
Alternatively, you can use the componentDidUpdate lifecycle method to check this value
componentDidUpdate(prevProps, prevState) {
if (prevState.numbers.one > 0 && this.state.numbers.one === 0) {
alert('test');
}
}
Because setState is async, you need to add the alert in the call back of setState.
class App extends Component {
constructor(props){
super(props);
this.state = {
numbers:{
one:1
},
};
}
decrement = () => {
this.setState(prevState => {
return {
numbers:{
...prevState.numbers,
one: prevState.numbers.one - 1
}
}
},
() => {
console.log(this.state.numbers.one)
if(this.state.numbers.one===0){
alert('test');
}
});
}
render() {
return (
<div className="App">
<div>{this.state.numbers.one}</div>
<br/>
<button onClick={this.decrement}>ok</button>
</div>
);
}
}
export default App;
you could implement the decrement function as below
decrement = () => {
this.setState({numbers: { ...this.state.numbers, one: this.state.numbers.one -1} },
() => {
this.state.numbers.one===0 && alert("test")
}
)
}
rotateRender() {
if(false) {
return(
<TimerPage></TimerPage>
);
} else {
return(
<RepoPage></RepoPage>
);
}
}
I have two components called TimerPage and RepoPage.
I created a simple conditional render function as above, but cannot come up with a condition to make it render iteratively after a certain amount of time.
For example, I first want to render RepoPage and switch to TimerPage after 5 minutes and then stay in TimerPage for 15 mins before I switch again to the RepoPage.
Any way to do this?
Might not be that elegant, but this works
Actually I was thinking that this block might be more elegant than the first one
const FIRST_PAGE = '5_SECONDS';
const SECOND_PAGE = '15_SECONDS';
const FirstComponent = () => (
<div>5 SECONDS</div>
);
const SecondComponent = () => (
<div>15 SECONDS</div>
);
class App extends Component {
state = {
currentPage: FIRST_PAGE
};
componentDidUpdate() {
const {currentPage} = this.state;
const isFirst = currentPage === FIRST_PAGE;
if (isFirst) {
this._showSecondPageDelayed();
} else {
this._showFirstPageDelayed();
}
}
componentDidMount() {
this._showSecondPageDelayed();
};
_showSecondPageDelayed = () => setTimeout(() => {this.setState({currentPage: SECOND_PAGE})}, 5000);
_showFirstPageDelayed = () => setTimeout(() => {this.setState({currentPage: FIRST_PAGE})}, 15000);
render() {
const {currentPage} = this.state;
const isFirst = currentPage === FIRST_PAGE;
const ComponentToRender = isFirst ? FirstComponent : SecondComponent;
return <ComponentToRender/>;
}
}
As stated in the comment section, you can create a higher order component that will cycle through your components based on the state of that component. Use setTimeout to handle the timer logic for the component.
state = {
timer: true
}
componentDidMount = () => {
setInterval(
() => {
this.setState({ timer: !this.state.timer })
}, 30000)
}
render(){
const {timer} = this.state
if(timer){
return <TimerPage />
} else {
return <RepoPage />
}
}
Edit
Changed setTimeout to setInterval so that it will loop every 5 minutes instead of just calling setState once
You could use the new context API to achieve this. The benefit is now I have a configurable, reusable provider to play with throughout my application. Here is a quick demo:
https://codesandbox.io/s/k2vvy54r8o
import React, { Component, createContext } from "react";
import { render } from "react-dom";
const ThemeContext = createContext({ alternativeTheme: false });
class ThemeWrapper extends Component {
state = {
alternativeTheme: false
};
themeInterval = null;
componentDidMount() {
this.themeInterval = setInterval(
() =>
this.setState(({ alternativeTheme }) => ({
alternativeTheme: !alternativeTheme
})),
this.props.intervalLength
);
}
componentWillUnmount() {
if (this.themeInterval) {
clearInterval(this.themeInterval);
}
}
render() {
return (
<ThemeContext.Provider value={this.state}>
{this.props.children}
</ThemeContext.Provider>
);
}
}
const App = () => (
<ThemeWrapper intervalLength={2000}>
<ThemeContext.Consumer>
{({ alternativeTheme }) =>
alternativeTheme ? <p>Alternative Theme</p> : <p>Common Theme</p>
}
</ThemeContext.Consumer>
</ThemeWrapper>
);
render(<App />, document.getElementById("root"));
Whatever you do make sure on componentWillUnmount to clear your interval or timeout to avoid a memory leak.
I am using react-router-v4 along with react 16.
I want to reset the component's internal state when the user go to a different route or comes back to the same route . Route change should destroy the internal state of a component but it doesn't . And I can't even find a way to notify the component when the route changes as it's a nested component not a direct render of a Route component. Please help.
Here's the code or live codepen example --
const initialProductNames = {
names: [
{ "web applications": 1 },
{ "user interfaces": 0 },
{ "landing pages": 0 },
{ "corporate websites": 0 }
]
};
export class ProductNames extends React.Component {
state = {
...initialProductNames
};
animProductNames = () => {
const newArray = [...this.state.names];
let key = Object.keys(newArray[this.count])[0];
newArray[this.count][key] = 0;
setTimeout(() => {
let count = this.count + 1;
if (this.count + 1 === this.state.names.length) {
this.count = 0;
count = 0;
} else {
this.count++;
}
key = Object.keys(newArray[count])[0];
newArray[count][key] = 1;
this.setState({ names: newArray });
}, 300);
};
count = 0;
componentDidMount() {
this.interval = setInterval(() => {
this.animProductNames();
}, 2000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
componentWillReceiveProps(nextProps) {
console.log(nextProps.match);
if (this.props.match.path !== nextProps.match.path) {
this.setState({ ...initialProductNames });
this.count = 0;
}
}
render() {
return (
<section className="home_products">
<div className="product_names_container">
I design & build <br />
{this.createProductNames()}
</div>
</section>
);
}
createProductNames = () => {
return this.state.names.map(nameObj => {
const [name] = Object.keys(nameObj);
return (
<span
key={name}
style={{ opacity: nameObj[name] }}
className="product_names_anim">
{name}
</span>
);
});
};
}
I got the solution . I didn't quit understood why state as property initializer doesn't reset/intialize on remount. I think it only initialize once, not on every route change] -
I wanted to know how to reset a component's state on route change. But it turns out that you don't have to . Each route renders a specific component . When route changes all other components are unmounted and all the state of those components are also destroyed. But see my code. I was using es7+ property initializer to declare state,count . That's why the state wasn't resetting/initializing again when the component remounted on route change.
To fix it, all i did is i put the state,initialProductNames,count; all of those into constructor. And now it's working perfectly .
Now fresh state on every mount and remount!!
You can use a listener on the Route change as the example on this previous question And there you can add a function to update the main state.
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
this.onRouteChanged();
}
}
onRouteChanged() {
console.log("ROUTE CHANGED");
}
The problem is not the state, it's the initialProductNames. Property initializer is a sugar syntax, in fact it is the same as creating a constructor and moving the code into the constructor. The problem is in the initialProductNames, which is created outside the component, that is, only once for the whole system.
For create a new initialProductNames for any instance of ProductNames, do that:
export class ProductNames extends React.Component {
initialProductNames = {
names: [
{ "web applications": 1 },
{ "user interfaces": 0 },
{ "landing pages": 0 },
{ "corporate websites": 0 }
]
};
state = {
...this.initialProductNames
};
// more code
componentWillReceiveProps(nextProps) {
console.log(nextProps.match);
if (this.props.match.path !== nextProps.match.path) {
this.setState({ ...this.initialProductNames });
this.count = 0;
}
}
Here is an example showing that the state is always recreated every remount: https://codesandbox.io/s/o7kpy792pq
class Hash {
constructor() {
console.log("Hash#constructor");
}
}
class Child extends React.Component {
state = {
value: new Hash()
};
render() {
return "Any";
}
}
class App extends React.Component {
state = {
show: true
};
render() {
return (
<div className="App">
<button
type="button"
onClick={() =>
this.setState({
show: !this.state.show
})
}
>
Toggle
</button>
{this.state.show && <Child />}
</div>
);
}
}
I'm trying to update the state of my component inside of an eventListener. I'm getting the following console error:
'Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the Header component'
This is my component code:
class Header extends React.Component {
constructor(props) {
super(props);
this.state = {
fixed: false
}
}
handleScroll(event) {
this.setState({
fixed: true
});
}
componentDidMount() {
window.addEventListener("scroll",() => {
this.handleScroll();
});
}
componentWillUnmount() {
window.removeEventListener("scroll",() => {
this.handleScroll();
});
}
render() {
var {
dispatch,
className = "",
headerTitle = "Default Header Title",
onReturn,
onContinue
} = this.props;
var renderLeftItem = () => {
if (typeof onReturn === 'function') {
return (
<MenuBarItem icon="navigation-back" onClick={onReturn}/>
)
}
};
var renderRightItem = () => {
if (typeof onContinue === 'function') {
return (
<MenuBarItem icon="navigation-check" onClick= {onContinue}/>
)
}
};
return (
<div className={"header " + className + this.state.fixed}>
{renderLeftItem()}
<div className="header-title">{headerTitle}</div>
{renderRightItem()}
</div>
)
}
}
Header.propTypes = {
};
let mapStateToProps = (state, ownProps) => {
return {};
};
export default connect(mapStateToProps)(Header);
IMHO this is because you do ont unregister the function as you expect it, and a scroll event is sent after an instance of this component has been unmounted
try this:
componentDidMount() {
this._handleScroll = this.handleScroll.bind(this)
window.addEventListener("scroll", this._handleScroll);
}
componentWillUnmount() {
window.removeEventListener("scroll", this._handleScroll);
}