I've fired an action in componentDidMount :
componentDidMount() {
this.props.getQuests();
}
and would like to fire another action once it's completed, I've used the lifecycle method componentWillReceiveProps
componentWillReceiveProps = async nextProps => {
if (nextProps.questObject.length !== 0) {
const authToken = await AsyncStorage.getItem("authToken");
this.props.getProfile(authToken);
}
};
However only this.props.getQuests(); is fired and the 2nd action this.props.getProfile(authToken) is not being fired.
const mapStateToProps = ({ quest, profile }) => {
const { error, loading, questObject } = quest;
const { profileObj } = profile;
return { error, loading, questObject, profileObj };
};
export default connect(
mapStateToProps,
{ getQuests, getProfile }
)(HomeScreen);
Currently it returns 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.
You are trying to update the DOM before it is mounted.when you are using componentWillReceiveProps,make sure you compare the this.props and the latest props i.e., nextProps.Otherwise,componentWillReceiveProps will render the component unnecessarily so that might lead to infinite loop.
componentWillReceiveProps = nextProps => {
if (this.props.questObject !== nextProps.questObject &&
nextProps.questObject.length !== 0) {
try {
const authToken = await AsyncStorage.getItem("authToken");
this.props.getProfile(authToken);
}
catch(e) {
console.log(e);
}
}
};
Related
I'm trying to provide a useMessage hook for my components.
In my application, Panel is the largest component, the mother component, and it has a Message component in it that uses a Material UI's Snackbar to show messages and alerts.
Here's a code sandbox
Components use my hook this way:
import useMessage from './useMessage'
const SomeComponent = () => {
const { success, error } = useMessage()
return <button onClick={success('some message')}>Show success</button>
}
However, it does not change the severity and many times the message is empty. It does not work the way I expect it to work.
How can I fix it?
Issue
It seems the useEffect hook in the useMessage hook is the cause of all the trouble, specifically its interaction with the hide function.
const hide = () => {
setIsMessageShown(false);
};
...
useEffect(() => {
if (message && severity) {
setIsMessageShown(true);
}
return () => {
if (isMessageShown === false) {
setMessage(null);
setSeverity(null);
}
};
}, [
message,
setMessage,
severity,
setSeverity,
isMessageShown,
setIsMessageShown
]);
When hide is called it enqueues a state update to toggle the isMessageShown state false. isMessageShown is a dependency for the useEffect and there's a condition that checks if there's a truthy message and severity state. There is, since that wasn't cleared out too, so another state update is enqueued to toggle the isMessageShown state back true. At the same time, the useEffect hook is returning a cleanup function then enqueues state updates to clear the message and severity state, but the Snackbar and Alert components are still displayed.
There appears to be a state synchronicity issue from here on out and this is why the alert doesn't work the same after the first invocation.
Solution
Commenting out, or removing, the useEffect hook appears to entirely resolve the issue you describe and reproduce in the codesandbox.
Here's an updated useMessage hook.
import { usePanel } from "./PanelContext";
const useMessage = () => {
const { setIsMessageShown, setMessage, setSeverity } = usePanel();
const show = (data, action, type) => {
if (data && data.message) {
data = data.message;
}
setMessage(data);
setSeverity(type);
setIsMessageShown(true);
};
const success = (data, action) => {
show(data, action, "success");
};
const info = (data, action) => {
show(data, action, "info");
};
const warning = (data, action) => {
show(data, action, "warning");
};
const error = (data, action) => {
show(data, action, "error");
};
const hide = () => {
setIsMessageShown(false);
};
return {
success,
info,
warning,
error,
show,
hide
};
};
export default useMessage;
I did a bit of refactoring to centralize the Panel context code & logic
PanelContext.js
import React, { useContext, useState } from "react";
export const PanelContext = React.createContext();
export const usePanel = () => useContext(PanelContext);
const PanelProvider = ({ children }) => {
const [isMessageShown, setIsMessageShown] = useState();
const [message, setMessage] = useState();
const [severity, setSeverity] = useState();
return (
<PanelContext.Provider
value={{
isMessageShown,
setIsMessageShown,
message,
setMessage,
severity,
setSeverity
}}
>
{children}
</PanelContext.Provider>
);
}
export default PanelProvider;
<button onClick={() => showSuccess()}>Success</button>
I think you should using an arrow function in render
Description
I have component which shows data that get from server and display it on the table using the state, tableData and it must be set when Redux action is dispatched.
I've use action listener library which uses Redux middleware which consisting of 63 lines of code. redux-listeners-qkreltms.
For example when I register a function on analysisListIsReady({}).type which is ANALYSISLIST_IS_READY then when the action is dispatched, the function is called.
Issue
The issue is that react throws sometimes the error: Can't update react state... for setTableData so response data is ignored to be set. I want to figure it out when it happens.
I've assumed that it's because of unmounting of component, so I printed some logs, but none of logs are printed and also ComponentA is not disappeared.
It's not throing any error when I delete getAnalysisJsonPathApi and getResource, so I tried to reporuduce it, but failed... link
It's not throing any error when I delete listenMiddleware.addListener see: #2
#1
// ComponentA
const [tableData, setTableData] = useState([])
useEffect(() => {
return () => {
console.log("unmounted1")
}}, [])
useEffect(() => {
listenMiddleware.addListener(analysisListIsReady({}).type, (_) => {
try {
getAnalysisJsonPathApi().then((res) => {
//...
getResource(volumeUrl)
.then((data: any) => {
// ...
setTableData(data)
})
})
} catch (error) {
warn(error.message)
}
})
return () => {
console.log("unmounted2")
}
}, [])
export const getAnalysisJsonPathApi = () => {
return api
.post('/segment/volume')
.then(({ data }) => data)
export const getResource = async (src: string, isImage?: boolean): Promise<ArrayBuffer> =>
api
.get(src)
.then(({ data }) => data)
#2
// ComponentA
const [tableData, setTableData] = useState([])
useEffect(() => {
return () => {
console.log("unmounted1")
}}, [])
useEffect(() => {
if (steps.step2a) {
try {
getAnalysisJsonPathApi().then((res) => {
//...
getResource(volumeUrl)
.then((data: any) => {
// ...
setTableData(data)
})
})
} catch (error) {
warn(error.message)
}
}
return () => {
console.log("unmounted2")
}
}, [steps.step2a])
Well, its as you said:
because of unmounting of component
In your UseEffect() function, you need to check if the componenet is mounted or not, in other words, you need to do the componentDidMount & componentDidUpdate (if needed) logics:
const mounted = useRef();
useEffect(() => {
if (!mounted.current) {
// do componentDidMount logic
console.log('componentDidMount');
mounted.current = true;
} else {
// do componentDidUpdate logic
console.log('componentDidUpdate');
}
});
i didn't go to your question code detail, but my hint might help you, usually this error happens in fetchData function,
suppose you have a fetchData function like below:
fetchData(){
...
let call = await service.getData();
...
--->setState(newItems)//Here
}
so when api call end and state want to be updated, if component been unmounted, there is no state to be set,
you can use a bool variable and set it false when component will unmount:
let stillActive= true;
fetchData(){
active = true;
...
let call = await service.getData();
...
if(stillActive)
setState(newItems)//Here
}
}
componentWillUnmount(){
active = false;
}
I've found out it's because of redux-listeners-qkreltms, Redux middleware.
It keeps function when component is mounted into listener, but never changes its functions even component is unmounted.
middleware.addListener = (type, listener) => {
for (let i = 0; i < listeners.length; i += 1) {
if (listeners[i].type === type) {
return;
}
}
listeners.push(createListener(type, listener));
};
My context looks like this:
class AuthStoreClass {
authUser = null
constructor() {
makeAutoObservable(this)
}
login = async (params) => {
const { data: { data: authUser } } = await loginUser(params)
this.authUser = authUser
}
}
const AuthStoreContext = React.createContext(null);
export const authStoreObject = new AuthStoreClass()
export const AuthStoreProvider = ({ children }: any) => {
return <AuthStoreContext.Provider value={authStoreObject}>{children}</AuthStoreContext.Provider>;
};
export const useAuthStore = () => {
return React.useContext(AuthStoreContext);
};
And I am using the context somewhere else in a component:
const LoginPage = observer(() => {
const authStore = useAuthStore()
...
authStore.login(...)
The last line reports the following warning:
[MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: AuthStoreClass#1.authUser
Everything works as expected. How can I fix this issue?
Your login function is async and you need to use runInAction inside, or handle result in a separate action, or use some other way of handling async actions:
import { runInAction, makeAutoObservable } from "mobx"
class AuthStoreClass {
authUser = null
constructor() {
makeAutoObservable(this)
}
login = async (params) => {
const { data: { data: authUser } } = await loginUser(params)
// Wrap all changes with runInAction
runInAction(() => {
this.authUser = authUser
})
// or do it in separate function
this.setUser(authUser)
}
// This method will be wrapped into `action` automatically by `makeAutoObservable`
setUser = (user) => {
this.authUser = user
}
}
That is because, citing the docs, every step ("tick") that updates observables in an asynchronous process should be marked as action. And the code before the first await is in a different "tick" than the code after await.
More about async actions (you can even use generators!): https://mobx.js.org/actions.html#asynchronous-actions
In MobX version 6 actions are enforced by default but you can disable warnings with configure method:
import { configure } from "mobx"
configure({
enforceActions: "never",
})
But be careful doing it though, the goal of enforceActions is that you don't forget to wrap event handlers and all mutations in an action. Not doing it might cause extra re-runs of your observers. For example, if you changing two values inside some handler without action then your component might re-render twice instead of once. makeAutoObservable wraps all methods automatically but you still need to handle async methods and Promises manually.
You can also change the function to use the yield syntax, negating the need for runInAction.
*login() {
const { data: { data: authUser } } = yield loginUser(params)
this.authUser = authUser
}
I have a React / Redux / Meteor app in which I dispatch an action, that calls a method to get a value from the server, and the method has a callback in which I dispatch an action to save the returned value in the Redux store.
I'm also using Redux thunk.
Although my original action is only dispatched once, it runs twice. It seems that dispatching an action from inside a method callback, is causing the original action to be dispatched again.
In my React component:
class MyComponent extends Component {
....
render() {
...
}
}
function mapStateToProps(state, ownProps) {
return { value: state.myPartialState.value }
}
const Tracker = withTracker(({dispatch}) => {
const state = store.getState();
const isLoading = getIsLoading(state);
...
const handle = Meteor.subscribe('myData'), {
onReady: () => {
'onReady': () => {
secondaryPatternSubscriptions(patterns);
},
});
if (isLoading && handle.ready()) {
console.log('about to dispatch original action');
dispatch(getValue());
dispatch(setIsLoading(false));
} else if (!isLoading && !handle.ready()) {
dispatch(setIsLoading(true));
}
return { ... }
)(MyComponent);
export default connect(mapStateToProps)(Tracker);
In my actions file:
export const SET_VALUE = 'SET_VALUE';
export function setValue(value) {
return {
'type': 'SET_VALUE',
'payload': value,
};
}
export const getValue = () => (dispatch, getState) => {
console.log('about to call');
Meteor.call('getValue', (error, result) => {
console.log('about to dispatch second action');
dispatch(setValue(result)); // this causes the action to be dispatched again
});
// dispatch(setValue(10)); // this only runs once
};
const initialState = {
value: 0,
}
export default function myPartialState(state = initialState, action) {
switch (action.type) {
case SET_VALUE: {
return updeep({ 'value': action.payload }, state);
}
}
}
On the server, the method is like this:
Meteor.methods({
'getValue': function () {
...
return value;
},
})
I can see from the console logs that getValue is only dispatched once, but runs twice. I have checked this again and again, and I'm pretty near 100% sure that getValue is not dispatched twice.
I think it's something to do with calling an action from inside the method callback; if I comment out dispatch(setValue(result)); and replace it with a dispatch outside the method call, then getValue only runs once.
If I dispatch a different action instead of setValue, or change the setValue action so that it doesn't alter the 'value' property in the store, then again getValue only runs once. But I can't see why changing 'value' would cause the action to be run twice, when it is only dispatched once...
I've searched online and haven't found anything about this issue.
Can anybody think why my action is running twice, and a way to have it run only once? Thanks!
The result I want is my component to not render unless all the async function have dispatched. I'm using this as a wrapper to make sure everything has dispatched. I've tried two ways:
call everything in componentWillMount and use setState to set loaded = true. I can then render the component based on my state's loaded key.
ajax = async () => {
try{
const { dispatch } = this.props;
dispatch(loadPack('ars'));
dispatch(loadPack('acr'));
await dispatch(loadProds());
await dispatch(loadRepls());
await dispatch(checkEligibile());
}catch (e) { console.log(e)}
}
componentWillMount() {
this.ajax().then(() => {
this.setState({ loaded: true });
});
}
render() {
const { loaded } = this.state;
return loaded ? <Component/> : null;
}
This gets the desired results, except I see this error:
ExceptionsManager.js:71 Warning: Can only update a mounted or mounting
component. This usually means you called setState, replaceState, or
forceUpdate on an unmounted component. This is a no-op.
I tried dispatching in mapDispatchToProps. Ideally loaded should return true and I should see this.props.loaded = true to load my component. However, I'm receiving a Promise and not the result.
I'm feeling stuck here and not sure what else to try. Any suggestions?
const loadAsync = async dispatch => {
dispatch(loadPack('ars'));
dispatch(loadPack('acr'));
await dispatch(loadProds());
await dispatch(loadRepls());
await dispatch(checkEligibile());
return true
};
export const mapDispatchToProps = dispatch => ({
loaded: loadAsync(dispatch),
});
Since you are using redux, you have a global redux state. So after all dispatch, dispatch one more action that toogle a reducer state to true which indicate that all the actions has been dispatched.
In component, use dispatchStateToProps method to convert reducer state into props and then use that prop to check weather all the actions has been dispatched or not. It should roughly look something like this
ajax = async () => {
try{
const { dispatch } = this.props;
dispatch(loadPack('ars'));
dispatch(loadPack('acr'));
await dispatch(loadProds());
await dispatch(loadRepls());
await dispatch(checkEligibile());
// Add one more action here
await dispatch(everythingDispatched());
}catch (e) { console.log(e)}
}
Then a reducer state that keep track of that
dispatchTrackerReducer.js
switch(action.type){
case "everythingDispatched" :
everythingDispatched: true
break;
}
Then in component use mapStateToProps like this
render() {
const { everythingDispatched } = this.props;
return everythingDispatched ? <Component/> : null;
}
function mapStateToProps(state){
return {
everythingDispatched:state.dispatchTrackerReducer.everythingDispatche
}
}