Arrow function does not notify redux state changes - reactjs

I have a react component in react native that I want to handle hardwareBackButton manually.
I have different behavior when a redux state is true or false in backHandler function that I pass to hardwareBackPressListener.
const brandSelected = useSelector(state => state.map.brandSelected);
I have this useSelector in my component to access the state.
and I have useEffect function that I monitor the changes of this state: (that has correctly work and log the state when it changes to true or false.
React.useEffect(() => {
console.log(brandSelected); // this is false correctly
}, [brandSelected]);
and finally I have a backHandler function that I pass it to hardwareBackPress Listener.
React.useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', backHandler);
return () => {
BackHandler.removeEventListener('hardwareBackPress', backHandler);
};
}, []);
and backHandler function:
const backHandler = () => {
console.log('check, backhandler', brandSelected) // it logs true continuously
if (brandSelected === true) {
dispatch(
dispatchItemToRedux({
type: CATEGORIES_SELECTION,
payload: {
brandSelected: false,
},
}),
);
return true;
}
popScreen(Screens.Map);
return true;
};
But this function does not notify that the brandSelected state changed. the first time it works correctly and dispatch function and changes the redux state correctly and useEffect function log false correctly. but in other tries it does not work correctly and nothing changed!!

The issue here is a stale enclosure of the brandSelected in the backHandler function you passed to the "hardwareBackPress" event listener on the initial render cycle. backHandler function only ever has the value from the initial render cycle and will never update/re-enclose an updated value.
To resolve you should cache the backHandler state value in a React ref that you can reference in the callback handler.
const brandSelected = useSelector(state => state.map.brandSelected);
const brandSelectedRef = React.useRef(brandSelected);
useEffect(() => {
brandSelectedRef.current = brandSelected;
}, [brandSelected]);
...
const backHandler = () => {
console.log('check, backhandler', brandSelectedRef.current)
if (brandSelectedRef.current) {
dispatch(
dispatchItemToRedux({
type: CATEGORIES_SELECTION,
payload: {
brandSelected: false,
},
}),
);
return true;
}
popScreen(Screens.Map);
return true;
};
An alternative would be to move backHandler into the useEffect hook setting the event listeners and use the brandSelected state as a dependency so the updated state value is re-enclosed in the callback.
React.useEffect(() => {
const backHandler = () => {
console.log('check, backhandler', brandSelected)
if (brandSelected) {
dispatch(
dispatchItemToRedux({
type: CATEGORIES_SELECTION,
payload: {
brandSelected: false,
},
}),
);
return true;
}
popScreen(Screens.Map);
return true;
};
BackHandler.addEventListener('hardwareBackPress', backHandler);
return () => {
BackHandler.removeEventListener('hardwareBackPress', backHandler);
};
}, [brandSelected]);

Related

useEffect not triggering re-render when it's being used with useReducer

I can't trigger re-render even the state has changed. PageContainer calls loadPage and loadPage updates state without any inssue.
But when the state has changed it dose not trigger re-rendering.
It only shows inital state(pageName.loginPage). My expected result is pageName.profilePage since token is already stored on browser. I've also checked debugger state is clearly changed to pageName.profilePage
function PageContainer() {
const { state, loadPage } = useContainer({
reducer: loginStatusCheckReducer,
});
useEffect(() => {
loadPage();
}, [state]); // or [state.page]
return (
<>
{state.page}
</>
);
}
And here is useContainer
function useContainer({ reducer }) {
const [state, dispatch] = useReducer(reducer, { page: pageName.loginPage });
const loadPage = () => {
dispatch({ type: actionType.loadPage, dispatch });
};
return { state, loadPage };
}
This is the reducer function
function loginStatusCheckReducer(state, action) {
if (action.type === actionType.loadPage) {
const token = localStorage.getItem("token");
if (token) {
state.api = api(token);
state.page = pageName.profilePage;
return state;
}
}
return state;
}
initial state:
after loadPage
Looking at the code, I will guess that is not triggering a re-render because your useEffect is passing an empty array, so it will not react to any state changes.
Try adding the variable/state that will change once loaded on the useEffect
function PageContainer() {
const { state, loadPage } = useContainer({
reducer: loginStatusCheckReducer,
});
useEffect(() => {
loadPage();
}, [state]); // here the effect will listen to state changes
console.log('state.page', state.page);
return (
<>
<h1>{state.page}</h1>
</>
);}
Following code should fix the issue. #lon has pointed out, state should not be directly mutated.
function loginStatusCheckReducer(state, action) {
if (action.type === actionType.loadPage) {
const token = localStorage.getItem("token");
if (token) {
//state.api = api(token);
//state.page = pageName.profilePage;
//return state;
return {
...state,
api: api(token),
page: pageName.profilePage,
};
}
}
}

Can't update react state, even component is not unmounted

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));
};

updated value for generic hooks

I have this hook
export const useLegalFooter = ({ customContent, isOpenOnPageLoad = false }) => {
const dispatch = useContext(LegalFooterDispatchContext);
useEffect(() => {
dispatch({ customContent, isOpenOnPageLoad });
return () => {
dispatch({ customContent: null, isOpenOnPageLoad: false });
};
}, [customContent, dispatch, isOpenOnPageLoad]);
};
and the way I use it is
useLegalFooter({
isOpenOnPageLoad: !showModalLoader,
customContent: renderQuoteAndPurchaseDisclaimers(policyType),
});
However, I noticed when the showModalLoader is updated the hook doesn't get an updated value for isOpenonPageLoad.
for example, showModalLoader is true when the component mounts, and then it changes to false. but still, the footer is showing open.
How to fix this?

How can i rewrite react-method with setState and callback using hooks?

I have a react component that contains the method with setState with a callback. I need to rewrite it to hooks. Please tell me how can i rewrite this method ?
beforeSubmitModal = action => (args) => {
this.setState({
visible: false,
selectedMenuItem: null,
companyCodeModal: {}
}, () => action(args));
};
const onDeleteCode = (id) => {
dispatch(actions.deleteCode.request({ codeId: id }));
};
const modalProps = {
onSaveOrUpdate: beforeSubmitModal(dispatch(actions.insertOrEditCode.request())),
onDelete: beforeSubmitModal(onDeleteCode),
};
you will need to use useEffect to do this
const [visible,setVisible] = useState(ture);
const doSomething = () => {
setVisible(false);
}
useEffect(() => {
//this will render every time the visible state changes
}, [visible]);
to define the states in hooks
const [visible,setVisible]=useState(false) // initial value false
const [selectedMenuItem,setCompanyCodeModal]=useState(null) // initial value null
const [companyCodeModal,setCompanyCodeModal]=useState('')
you need when they change do some action
useEffect(()=> doSomething() ,[visible,selectedMenuItem,companyCodeModal])

Stale closure problem react native AppState

I have a react native component that I want to check if a specific value is "true" in scope state, and then do some things but it always read old state in callback function.
const [scope, seScope] = useState({
isInScope: false,
loading: true,
isGranted: false,
})
const stateHandler = useCallback((state) => {
if (state === 'active') {
// it's always false not matter
if (!scope.isGranted) {
makeLocationPermission()
}
}
}, [scope])
console.log(scope.isGranted)
// in here isGranted is true
useEffect(() => {
makeIsGrantedTrue()
AppState.addEventListener('change', stateHandler)
return () => {
AppState.removeEventListener('change', stateHandler)
}
}, [])
ok I solve this just by putting "scope" on use effect dependency.
I don't know why ?! it must be on useCallback dependency.
useEffect(() => {
makeIsGrantedTrue()
AppState.addEventListener('change', stateHandler)
return () => {
AppState.removeEventListener('change', stateHandler)
}
}, [scope])

Resources