I'm trying to set a state of the user by getting a value from my database and then using it. For some reason the state does not update itself I have tried await and async. What other options exists if this one can't be reliable to make this be a value.
I do get the following error : 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.
- node_modules/fbjs/lib/warning.js:33:20 in printWarning
- node_modules/fbjs/lib/warning.js:57:25 in warning
- node_modules/react-native/Libraries/Renderer/ReactNativeRenderer-dev.js:12196:6 in warnAboutUpdateOnUnmounted
- node_modules/react-native/Libraries/Renderer/ReactNativeRenderer-dev.js:13273:41 in scheduleWorkImpl
- node_modules/react-native/Libraries/Renderer/ReactNativeRenderer-dev.js:6224:19 in enqueueSetState
- node_modules/react/cjs/react.development.js:242:31 in setState
* null:null in componentWillMount$
- node_modules/regenerator-runtime/runtime.js:62:44 in tryCatch
- node_modules/regenerator-runtime/runtime.js:296:30 in invoke
- ... 13 more stack frames from framework internals
constructor() {
super();
this.state = {
userPhoneNumber: "",
};
}
async componentWillMount() {
await firebase.database().ref('/Users/' + firebase.auth().currentUser.uid).on('value', async snap => {
if (snap) {
await this._setPhone(snap.val())
} else {
this.props.navigation.navigate('Phone')
}
});
console.log(this.state.userPhoneNumber);
}
_setPhone = (snap) => {
const val = parseInt(snap, 10);
this.setState({
userPhoneNumber: val
})
};
If you are sure that you are receiving the correct value for snap. Then the issue that you have is that setState is asynchronous. That means it takes time for state to set.
Unfortunately they way you are checking your state to see if the value has been set is wrong.
You should use a callback in the setState function, so your setState would become:
this.setState({userPhoneNumber: val}. () => console.log(this.state.userPhoneNumber));
I would recommend taking a read of the following articles by Michael Chan that go into more detail about setting state
https://medium.learnreact.com/setstate-is-asynchronous-52ead919a3f0
https://medium.learnreact.com/setstate-takes-a-callback-1f71ad5d2296
https://medium.learnreact.com/setstate-takes-a-function-56eb940f84b6
There are also a few issues with your use of async/await and promises it looks like you are mixing the syntax between them. You either use one or the other, not both. This article goes into detail about the differences between them.
this.setState does not return a promise so using await this.setState does nothing.
This is how I would refactor your code:
componentDidMount() { // componentWillMount is deprecated
// you are using a promise to access firebase so you shouldn't be using `async/await`
firebase.database().ref('/Users/' + firebase.auth().currentUser.uid).on('value', snap => {
if (snap) {
this._setPhone(snap.val()) // remove the await as it is not doing anything
} else {
this.props.navigation.navigate('Phone')
}
});
}
_setPhone = (snap) => {
const val = parseInt(snap, 10);
this.setState({ userPhoneNumber: val}, () => console.log(this.state.userPhoneNumber)) // include the callback to check the value of state
};
Updated question
You must be calling setState when the component has been unmounted. You need to check to make sure that your component is mounted before calling setState.
One way of doing it is by having a boolean flag that monitors when the component is mounted.
componentDidMount () {
this._isMounted = true;
}
componentWillMount () {
this._isMounted = false;
}
when you set your state you can do something like this
if (this._isMounted) {
this.setState({key: value});
}
You can see more about it here https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
Just set a _isMounted property to true in componentDidMount and set
it to false in componentWillUnmount, and use this variable to check
your component’s status
This it not an ideal solution but it is the simplest, as you really should be cancelling your promises etc, so that setState is never called as the promise has been cancelled.
Related
The following member function populates asynchronously a folder_structure object with fake data:
fake(folders_: number, progress_callback_: (progress_: number) => void = (progress_: number) => null): Promise<boolean>
{
return new Promise((resolve, reject) => {
for (let i_ = 0; i_ < folders_; i_++) {
progress_callback_(i_ / folders_ * 100.);
this.add(this.id(), faker.address.city() + i_, random_choice(this.folder_structure_id()));
}
progress_callback_(folders_ / folders_ * 100.);
resolve(true);
})
}
It uses a callback to update the progress within the for loop which is then used to update the state (a progress bar) from within a useEffect() function with an empty dependency array.
let [progress_state_, set_progress_state_] = useState<number>(0);
let [fake_done_, set_fake_done_] = useState<boolean>(false);
useEffect(() =>
{
if (fake_)
folder_structure_.fake(fake_, (progress_) => {
set_progress_state_(progress_)
}).then(value => set_fake_done_(value));
}, [])
if (!fake_ || fake_done_) etc etc
However, the state is not updated (logging the progress in the console seems to work fine). Any ideas as to whether it's possible to update a state from within useEffect?
The reason your useEffect hook isn't working is that it's not called upon progress_state_ state change.
Instead of
useEffect(() =>
{
...
}, [])
Try this instead
useEffect(() =>
{
...
}, [progress_])
Adding progress_ to the dependency array means useEffect will be called every single time progress_ changes. If you leave it as an empty dependency array, then useEffect is only ever called in the very beginning on when the code is mounted to the DOM.
Here's a good explanation on dependency arrays: https://devtrium.com/posts/dependency-arrays
Addressing your final question: Yes, it is possible to update state from within useEffect.
To understand the root of your main issue, I would be curious to see how you are doing your logging. Are you logging from within fake() or from your render() function?
Is one allowed to call a setState inside the function passed into setState. for instance im trying to check for errors in user input and im calling the error checker function inside the setInpuForm, I know I can call the checkerror() from outside the setInputForm i.e inside the change handler, but out of curiosity, i decided to try it this way. But I discovered after typing the first character into the input field no changes but from the second entry up wards,its displayed. When i ran this it worked, the issue is that the very first time the input changed, the value was not updated but subsequently it worked
const errorChecker = (inputValue, element)=>{
//check for password error
if (element==='password') {
if (inputValue.length<=6) {
console.log('function error checker called')
setInputForm((form)=>{
console.log('after error check')
console.log(form.password.value)
return {
...form,
[element]: {
...form[element],
error: 'Password can not be less than 6'
}
}
})
} else {
setInputForm((form) => {
return {
...form,
[element]: {
...form[element],
error: ''
}
}
})
}
}}
<Input
key={inputForm[it].label}
changed={(event) => {
const val = event.target.value
setInputForm((form)=>{
console.log(form.password.value)
console.log('before error check')
errorChecker(val, it)
console.log('from set valuie')
return {
...form,
[it]: {
...form[it],
value: val
}
}
})
}}
I believe your issue is that you are calling two setState functions one after another, both of which rely on the previousState value. setState as you may know is not synchronous, and needs some time to finish its execution. In the meantime what you would be using in your errorChecker function would be the last state registered.
In order to avoid this, in a class based component you would usually include a callback function on your this.setState function. In a functional component you cannot do that. Instead you can use useEffect in order to detect changes to a certain state, and trigger a function like errorChecker based on those changes.
For example:
useEffect(() => {
if (inputForm[it].value !== "") {
errorChecker(inputForm[it].value, inputForm[it]);
}
}, [inputForm[it].value);
I have several actions which use the same reducer, and instead of having a dom operation in each of those actions, I want to just add it once inside my shared reducer. I know reducers are to be pure (which the returned data still is), but is this some kind of anti-pattern or an acceptable strategy?
case APPEND_POSTS:
!payload.length &&
document.getElementById('posts-cont').classList.add('no-more-posts'); // this
const total = state.posts.length + payload.length;
const limit = total > posts_to_keep_limit ? 50 : 0;
return {
...state,
posts: [...state.posts.slice(limit), ...payload],
loading: false,
};
```
Redux Action
case APPEND_POSTS:
// you don't need to use below code.
// !payload.length && document.getElementById('posts-cont').classList.add('no-more-posts'); // this
const total = state.posts.length + payload.length;
const limit = total > posts_to_keep_limit ? 50 : 0;
return {
...state,
posts: [...state.posts.slice(limit), ...payload],
nomore: true,
loading: false,
};
Your component.
function YourComp(props){
const state = useSelector(...);
return ( <div id="posts-cont" className={state.nomore ? 'no-more-posts' : ''} > {...}</div>
}
I know reducers are to be pure (which the returned data still is), but is this some kind of anti-pattern or an acceptable strategy?
The returned data is pure, but you've introduced a side-effect in the form of a DOM mutation. Therefore, this reducer is not pure.
This is indeed an anti-pattern because now, the component(s) that render posts-cont items have an invisible coupling to this reducer. It makes your codebase more difficult to read and debug.
jinongun's advice is good: let the className of the component derive its value from the store's state using a selector. AS for the general question
I have several actions which use the same reducer, and instead of
having a dom operation in each of those actions, I want to just add it
once inside my shared reducer.
DON'T EVER make DOM operations inside a reducer.
Don't ever make any operation that is not a pure computation.
But you can create an action creator that always calls a side effect (with Redux-Thunk):
function appendPosts(payload) {
return dispatch => {
mySideEffect()
dispatch({
type: APPEND_POSTS,
payload
})
}
}
function action1(params) {
return dispatch => {
dispatch({
type: ACTION1,
payload: params
})
dispatch(appendPosts(params))
}
}
function action2(params) {
return dispatch => {
dispatch({
type: ACTION2,
payload: params
})
dispatch(appendPosts(params))
}
}
// etc
This is how my state looks like:
constructor(props, context) {
super(props, context);
this.state = {
show: false,
btnLabel: 'GO!',
car: {
owner: false,
manufacturer: false,
color: false
}
};
}
and this is how I modify state:
handleClickFetchPrice() {
this.setState({btnLabel: 'Fetching data...' });
console.log(this.state.fetchPriceBtn);
const url = 'some url';
axios.get(url)
.then(res => {
let car = [...this.state.car];
car.owner = res.data.owner;
car.manufacturer = res.data.manufacturer;
car.color = res.data.color;
this.setState({car});
})
}
The attribute car is updated, but fetchPriceBtn is not - the output of console.log(this.state.fetchPriceBtn); is still GO!.
What am I overlooking? Why the fetchPriceBtn is not updated?
React setState is an asynchronous process - you don't know exactly when it will be updated, you can only schedule the update.
To achieve your desired functionality, you can provide a callback into the setState method.
this.setState({ btnLabel: 'Fetching data...' }, () => console.log(this.state.fetchPriceBtn))
You can learn more following the documentation on the method.
#christopher is right, setState is an asynchronous process. But when second time call handleClickFetchPrice() function your btnLabel is value will be equal to Fetching data...
As answered in previous answers setState is asynchronous, so your console.log can't catch up the state change immediately. Again as suggested you can use callback function to track this change but if you use console.log just for debugging or want to see what changes in your state you can do this in your render function. And using a callback just for debug is not a nice way. Its purpose somehow different and if you check the official documentation, componentDidMount method is being suggested for such logic.
render() {
console.log( this.state.foo );
return (...)
}
If you do that you see two console.log output, one before state change and one after.
Also, your state operations might be enhanced. You car property is not an array, but you are converting it to an array and setting it? Is this what you intend:
axios.get(url)
.then(res => {
const { owner, manufacturer, color } = res.data;
this.setState( prevState => ( { car: { ...prevState.car, owner, manufacturer, color } } ) );
})
Here we are not mutating our state directly, instead we are using spread operator and setting the desired properties. For your example we are setting the whole property actually.
One last note, I think you want to do that something like that:
this.setState( { btnLabel: "fetching } );
axios.get(url)
.then(res => {
const { owner, manufacturer, color } = res.data;
this.setState( prevState => ( { car: { ...prevState.car, owner, manufacturer, color }, btnLabel: "go" } ) );
})
If your intention is somehow to do a status change/check this might no be a good logic as you have seen setState is not synchronous. Do this carefully.
I want to know why when I dispatch action before my console log prints old state.
if I do next:
reducer.js
let initialState = { display: false };
const MyReducer = (state = initialState,action) => {
...
case 'SET_DISPLAY':
return { update(state,{ display : {$set: action.display } }) }
break;
default:
return state;
break;
}
ActionCreator.js
let ActionCreator = {
setDisplay(value) {
return(dispatch,getState) {
dispatch({ type: 'SET_DISPLAY',display: value})
}
}
};
app.js
componentDidMount(){
this.props.dispatch(ActionCreator.setDisplay(true))
// expected : true
console.log(this.props.display)
// prints : false.
}
const mapStateToProps = (state) => {
display : state.display
}
but I can see changes in my redux dev-tools console.
PD I use redux-thunk as Middleware.its just example,all my code seems good and works great,but,its a question.
Why console logs old state instead a new state (its ilogic, if I dispatched an action before call logs) I will apreciate your answers,thanks.
This is because you are using redux-thunk and your dispatch happens aynchronously.
this.props.dispatch(ActionCreator.setDisplay(true)) will not set display true immediately.
Since you are not making a network request or anything async in that action why dont you change the action creator to
let ActionCreator = {
setDisplay(value) {
return { type: 'SET_DISPLAY',display: value};
}
};
Now it will happen synchronously. Also dont put console log immediately after dispatching. As redux updates state, old state is not modified. Instead it creates a new state instance with updated value. This new value will be passed as props to your component via connect of react-redux.
Try printing display in render() method, you will see that it is called twice and second one will display true.
First, I would recommend not to rely on the fact that dispatching an action may be synchronous; design as if everything was asynchronous. When eventually you dispatch an async actions, you will be pleased to have your mindset ready for that.
Second, your action creator return a function (you must be using the thunk middleware), which is why you get this behaviour.
componentDidMount(){
startSomethingAsync();
}
componentDidUpdate(){
if (!this.props.asyncCompleted) return;
if(this.props.asyncResultFn) {
this.props.dispatch({ type: ... value: VALUE_CONDITIONAL_TRUE})
}
else{
this.props.dispatch({ type: ... value: VALUE_CONDITIONAL_FALSE})
}
}