I use react-native-navigation and redux. I have a Stack Navigator with two screens.
The second screen uses some data stored in redux by the first screen.
The first screen has a function that resets that data inside a useFocusEffect hook so when the focus is back on the first screen all the data will be erased.
// First Screen
useFocusEffect(
useCallback(() => {
dispatch(clearAllData());
}, []);
);
The problem is that if I try to go back to the first screen using navigation.goBack() somehow the data is cleared before the second screen is completely unmounted and it throw an error.
// Second Screen
const some_params = useSelector(state => state.data.some_params);
// error: can't read property "some_params" of null
I tried adding a little timeout on the dispatch(clearAllData()) and it worked but I don't think it's a valid solution.
I can use the optional chaining like state?.data?.some_params but I will have to use it everywhere.
Is it possible to trigger clearAllData being sure the second screen is completely unmounted?
Edit
I've tried both useIsFocused and navigation.pop() but none of them works as expected.
Finally I found a solution using InteractionManager.runAfterInteractions
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
dispatch(clearAllData());
});
return () => task.cancel();
}, [])
);
Can you try clearing data like this
import { useIsFocused } from '#react-navigation/native';
const focused = useIsFocused()
useEffect(()=>{
if(focused){
dispatch(clearAllData());
}
},[focused])
Related
I'm new to React and modifying an existing starter app.
I'm implementing a check to see if the user is logged in, based on the existence of a token in localStorage. I modified my App.js file from this:
function App() {
let [ isLoggedIn, setLoggedIn ] = useState(false);
return (
<><Index isLoggedIn={isLoggedIn} setLoggedIn={setLoggedIn} /></>
);
to this:
function App() {
let [ isLoggedIn, setLoggedIn ] = useState(false);
const storageToken = localStorage.getItem('token');
if (storageToken) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
return [same as above]
This change results in a Uncaught Error: Too many re-renders. error. Putting the check in Index.js works fine.
Looking into this some more (eg from this other question or this blog post), I understand that it's because you can't modify a state in the same function where you declare it with useState because that state change causes the overall function to run again. But my question is: why? I suspect this is background info about how React works / the order in which React calls components, so any background reading on that topic would be appreciated!
I think you should read this for understading how Reactjs actually works in these cases
App is a functional component, which has its code run on every render. Changing state triggers a new render. By calling setLoggedIn() in the main body of the function, you’re creating an infinite loop.
The solution is to only read from localStorage when the component mounts/unmounts. This can be done with an effect.
useEffect(() => {
// code to run on component mount
const storageToken = localStorage.getItem('token');
if (storageToken) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
}, []);
I looked around and tried to find a solution with React router.
With V5 you can use <Promt />.
I tried also to find a vanilla JavaScript solution, but nothing worked for me.
I use React router v6 and histroy is replaced with const navigate = useNavigation() which doesn't have a .listen attribute.
Further v6 doesn't have a <Promt /> component.
Nevertheless, at the end I used useEffect clear function. But this works for all changes of component. Also when going forward.
According to the react.js docs, "React performs the cleanup when the component unmounts."
useEffect(() => {
// If user clicks the back button run function
return resetValues();;
})
Currently the Prompt component (and usePrompt and useBlocker) isn't supported in react-router-dom#6 but the maintainers appear to have every intention reintroducing it in the future.
If you are simply wanting to run a function when a back navigation (POP action) occurs then a possible solution is to create a custom hook for it using the exported NavigationContext.
Example:
import { UNSAFE_NavigationContext } from "react-router-dom";
const useBackListener = (callback) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
const listener = ({ location, action }) => {
console.log("listener", { location, action });
if (action === "POP") {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Usage:
useBackListener(({ location }) =>
console.log("Navigated Back", { location })
);
If using the UNSAFE_NavigationContext context is something you'd prefer to avoid then the alternative is to create a custom route that can use a custom history object (i.e. from createBrowserHistory) and use the normal history.listen. See my answer here for details.
I'm trying to implement something like a Mobile Preview section where after the user do their things in the editor, the changes that they've made will be shown in the Preview section concurrently.
The issue that I'm facing now is the method that I'm using in Bulletin.js to retrieve the html content from the editor seems to be 1 step behind (as in I need to do some actions like clicking anywhere or to retrieve the last action made in the editor).
I want to make it so that the change is instant and not one step behind so that when user do things like changing font colour etc, it will be reflected to the preview section instantly.
Bulletin.js
const getContent = (htmlContentProp) => {
setHtmlContent(draftToHtml(htmlContentProp));
};
<RichTextEditor getContent={getContent} htmlContent={htmlContent} />
RichTextEditor.js
const handleEditorChange = (state) => {
setEditorState(state);
getContent(convertToRaw(editorState.getCurrentContent()));
};
Issue is here:
const handleEditorChange = (state) => {
setEditorState(state); // this is asynchronous
// so this will most likely be old value
getContent(convertToRaw(editorState.getCurrentContent()));
};
You have 2 easy options to work around this
One is to not use hook here at all, you can consume your "state" directly
const handleEditorChange = (state) => {
getContent(convertToRaw(state.getCurrentContent()));
};
Other option is to use useEffect which is more "correct" option if you for some reason need the hooks here
const handleEditorChange = (state) => {
setEditorState(state); // this is asynchronous
};
useEffect(() => {
getContent(convertToRaw(editorState.getCurrentContent()));
}, [editorState]); // this effect will trigger once the editorState actually changes value
When this line getContent(convertToRaw(editorState.getCurrentContent())) runs in the handleEditorChange function, the editorState is not yet updated with the latest value. Since React state updates are async
You can either use the state parameter in the handleEditorChange to get the latest data like below
const handleEditorChange = (state) => {
setEditorState(state);
getContent(convertToRaw(state.getCurrentContent()));
};
or use a useEffect to update state in parent based on changes in the child state.
I am aware this is a common question as I have spent the last two hours going through every answer and trying to get my state to update but nothing is working.
I am fetching text from a cms however on first load the state is undefined and my app crashes. However if I comment the line out, load the page and uncomment the line the correct values are displayed.
Here is some of the code I have tried.
The data i am hoping to get
[
{id:1},
{id:2},
{id:3},
{id:4},
]
import react, {useEffect, useState} from 'react'
import axios from 'axios'
const [carouselTitle, setCarouselTitle] = useState([])
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
await axios('api').then(
response => {
console.log(response.data)
setCarouselTitle(response.data)
console.log(carouselTitle)
})
};
return(
<h1>{carouselTitle[1].id}</h1>
)
console logging the data works fine but when i console log the state it does not work.
2ND METHOD I TRIED
useEffect(() => {
const fetchData = async () => {
const res = await axios('api');
const carouselTitleAlt = await res.data;
setCarouselTitle({ carouselTitleAlt });
console.log(carouselTitleAlt);
console.log(carouselTitle);
};
fetchData();
}, []);
Again console logging the const inside the useEffect displays the correct information but logging the state does not work.
Appreciate your responses or better ways of displaying the data.
setState is asynchronous : https://reactjs.org/docs/faq-state.html#why-doesnt-react-update-thisstate-synchronously
It means that you cannot expect to console.log the new state value the line after you called setCarouselTitle.
To log the new value, you could use another useEffect, with carouselTitle in the dependencies array, where you console.log(carouselTitle) :
useEffect(() => {
console.log(carouselTitle);
}, [carouselTitle]);
That said, your component should behave correctly, it will be refreshed when the state is updated.
In the JSX you should check that carouselTitle is not undefined (meaning that the request failed or is still pending) :
{carouselTitle && <H1>{carouselTitle[0].id}}
https://reactjs.org/docs/conditional-rendering.html#gatsby-focus-wrapper
First of all, if you pass an empty array for initial data to useState, you can't get any item in that array in here:
return(
<h1>{carouselTitle[1].id}</h1>
)
Because component returns first item of an array that has nothing. I prefer to you do it like this:
return(
<h1>{carouselTitle.length > 0 && carouselTitle[0].id}</h1>
)
And also based on this and official documentation, setState (and also setSomthing() using useState()) is asynchronous.
So state data doesn't show immediately after setting that.
You should trigger useEffect for run fetch function
useEffect(()=>{fetchData();},[carouselTitle])
I'm trying to make an e-commerce app using expo with typescript and I'm having issues with loading my categories page.
There's two ways which one can access the category page
Through bottom nav - this one works fine
Through buttons on the home page
Trying to setState based off the params that's coming in through the homepage but it's giving me the Too many re-renders error.
Below is the code that I currently have for it.
The default needs to be there because of the first method of entering the category page, I've tried putting a conditional as the default category, but category wont change the second time if being accessed through the home page again.
export default function CategoryList(props: Props) {
let { params } = useRoute<StackRouteProp<'Home'>>()
const [currentCategory, setCategory] = useState({categories: Categories[0]});
if (params) {
setCategory({categories: params.categories})
}
}
You can also use useEffect with useRef:
export default function CategoryList(props: Props) {
let { params } = useRoute<StackRouteProp<'Home'>>()
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const canSet = useRef(true)
useEffect(() => {
if (params && canSet.current) {
setCategory({ categories: params.categories })
canSet.current = false
} else {
canSet.current = true
}
}, [params])
Everytime you call setCategory, it will re-render the component. Then, every time you render the component, if params exists, it will call setCategory again, which will cause another render, which calls setCategory again, and so on and on until it hits React's render limit.
One possible way to work around this would be to add a boolean to set after the first time which will prevent it from going inside the if block the second time:
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const [hasParamsCategory, setParamsCategory] = useState(false);
if (params && !hasParamsCategory) {
setCategory({categories: params.categories});
setParamsCategory(true);
}
This still isn't perfect because in general it's not a good practice to directly call state-changing functions during render, but this will serve as a quick and dirty workaround.