Run function from a different class in react native - javascript

I have a very simple issue with my react native application, I just want to execute a function everytime a button is clicked, it become complicated when there's separate classes and components.
I have 2 screens Dashboard and Search and 2 components Navbar and Results
In Dashboard I grab some user input and store it in selectedIngredients variable
and with the Navbar component I execute a function located in the same file.
<Navbar handle={() => this.switcher()} />
This function is where should the magic happens (or maybe in Search.js screen?)
switcher() {
const { navigate } = this.props.navigation;
navigate('Select', {passedData:this.state.selectedIngredients });
Alert.alert("send data to Search.js")
}
Select is Search.js screen
Everything workin fine and I move to the expected screen with the expected user input selectedIngredients, this is the first render of Search.js screen.
componentDidMount() {
this.apiCall();
Alert.alert("did mount search.js")
}
After that I'm stuck because everytime I click on the btn on my Navbar and execute the switcher() function, componentDidMount do not run anymore so I have to refresh the page by clicking on another button, this is exactly what I'm trying to avoid because it's bad for UX, like really bad. I am not looking to update Results component automatically but just update it with one function.
The code below is not that important it only shows apiCall function and render of Results component, I don't know if I should put more information. Please someone help
apiCall() {
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
this.setState({
data: responseJson.results,
});
})
}
componentDidMount() {
this.apiCall();
Alert.alert("did mount search.js")
}
render() {
return (
<View>
<Navbar title="Results" />
<Results results={this.state.data} />
</View>
);
}
}
My attempt was to add this.props.apiCall() in switcher function but got undefined error, something like hey react native! please send this data to Search.js screen and when you arrive please execute apiCall function, it's located there, not here.

Since you are using react navigation, in Search.js you have to bind didFocus listener, so the api will be called every time your screen is focused
componentDidMount() {
const { navigation } = this.props;
navigation.addListener( 'didFocus', () => this.apiCall() );
this.apiCall();
Alert.alert("did mount search.js")
}

Related

How do I stop the re-rendering of my react component? It rerenders for every object in a drop down

After building the homepage of my website I finally figured out how to dynamically navigate to other pages. I wanted the browser to render the State homepage when a user clicked on a dropdown and selected a state. The navigation works, but it re-renders the component 50 times which I do not understand. I suspect it is due to the map function that is creating the menuitems. I could build out 50 individual menuitems but that is really ugly.
I am just starting out learning React. I have 7 YRS experience in backend development, but I am still trying to get a handle on React development. I have created a wepage with Material UI that has a dropdown that looks like this
<FormControl>
<InputLabel>Select a State</InputLabel>
<Select value={location} onChange={selectionChangeHandler}>
{locations.map((value) => (
<MenuItem value={value.toLowerCase()} key={value.toLowerCase()} component={Link} to={`${value.toLowerCase()}/home`} >
{value}
</MenuItem>
))}
</Select>
</FormControl>
This returns a dropdown with the 50 states in it. When the user clicks on a state I want the program to route to that page on click. This dynamic routing works BUT. It re-renders my component 50 times. I think this is happening because the dropdown is being built inside of a .map functions and there are 50 entries in that list.
I can remove the map function and hardcode in 50 menuitems but that is ugly.
Here is my onChange Function
const selectionChangeHandler = (event) => {
console.log(event.target.value)
}
I have also tried removing the component={Link} and using the useNavigate hook in the selectionChangeHandler like so
const selectionChangeHandler = (event) => {
console.log(event.target.value)
setlocation(event.target.value)
link = `${event.target.value}/home`
navigate(link)
}
This works but also renders 50 times. I am at a loss.
I cross posted the above to reddit and then I researched a little more. It turns out in React. When a parent component's state is updated it re-renders all child components. This may be what is going on here, but I do not know how to fix it. Even if I pass the state as a prop to the child component I still have to link to the correct page.
I am kind of nervous about posting I really tried to put work into solving my own problem before reaching out for help, and I might be reaching out for help a lot as I learn. I am committed to learning, but some problems I just cannot figure out on my own.
Link to Code Link to Code
The problem here is inside StateHome.js. You use a naked axios.get call directly in your component so it's going to re-render anytime the component changes from state change.
When you get the results you set state inside the same component which re-renders the component, which then re-runs the axios.get call, causing the loop.
I understand you want to call that endpoint when someone lands on the /alabama/home page. To tell React "Do this one time when the component loads" you must use a useEffect with an empty dependency array. So instead of this:
const StateHome = () => {
const { location } = useParams();
const [PageData, SetPageData] = useState();
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
return <h1>This is my State Home Page Component for State {location}</h1>;
};
You need to use this:
const StateHome = () => {
console.log("StateHome");
const { location } = useParams();
const [PageData, SetPageData] = useState();
useEffect(() => {
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
}, []); // <--- indicates "on mount", loads once regadless of other side-effects (setState)
return <h1>This is my State Home Page Component for State {location}</h1>;
};

EXPO useEffect not called on navigating to same screen

I have one screen which called useEffect when rendered first time in the flow.
The second time I navigate to the screen in the flow , use Effect is not called but I want to call a function upon first time before , after or during render function is called when we navigate a second time to the same screen.
Here is the navigate call for everytime navigating to this screen from various screens
navigation.navigate("x", { u:type, s:title});
The following is the structure for my screen. I am not using components but functions with navigation
const x = ({ navigation}) => {
...
return (
<View style={styles.a}>
...
</View>
);
};
export default x;
The problem here is that a screen remains mounted in react-native after the initial navigation. Thus, a useEffect with an empty dependency array won't be called subsequently on navigation.
Notice that this behavior differs from the web.
If you are coming to react-navigation from a web background, you may assume that when user navigates from route A to route B, A will unmount (its componentWillUnmount is called) and A will mount again when user comes back to it. While these React lifecycle methods are still valid and are used in react-navigation, their usage differs from the web.
The recommended way to solve this is described here. For your case this can be solved as follows.
import { useFocusEffect } from '#react-navigation/native';
const x = ({ navigation}) => {
useFocusEffect(
React.useCallback(() => {
// called when screen is focused, thus everytime on navigation
return () => {
// unfocus... cleanup ... or whatever
};
}, [])
);
...
return (
<View style={styles.a}>
...
</View>
);
};
export default x;
useEffect will be called on first load of your functional component or when some dependency will be changed. Everything remains the same, so your useEffect will not work. Consider using useFocusEffect from react-navigation lib instead.

Why am I getting the following React Native warning: Can't perform a React state update on an unmounted component

I'm trying to build a React Native app, but am still kind of new to the React/RN ecosystem, so I may just be misunderstanding something obvious with the problem I'm having.
I have an app where a lot of the pages are structured as follows:
<View>
<NavComponent />
<View>
{/* Page component structure/logic here */}
</View>
</View>
NavComponent loads a toggleable nav menu with TouchableOpacity elements like the following:
Go to Screen #1
Go to Screen #2
Go to Screen #3
The problem I'm having (and maybe this isn't a problem so much as just how React/RN works) is that if I start on screen #1, open the nav, go to screen #2, open the nav again, and then go back to screen #1, even though screen #1 shows up again, the actual rendering function for screen #1 doesn't seem to be called again, and since NavComponent is part of the rendering of each screen, when I try to open the nav from screen #1 again, I get the following warning:
Warning: Can't perform a React state update 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 %s.%s, a
useEffect cleanup function ...
Again, maybe my approach with the app is flawed to begin with, but essentially, when I go from one screen to another from the nav, I always want the new screen to re-render from scratch (including the initial Ajax call for data).
Here's a more fleshed-out example of the render function for a screen (they all follow this same basic pattern):
const screen1 = ({ navigation }) => {
const [serverData, setServerData] = useState(null);
useEffect(() => {
// getPageData is a custom method I added to axios.
axios.getPageData('/api/url/here', (data) => {
setServerData(data);
});
}, []);
if (serverData) {
const { meta, user, data } = serverData;
return (
<View>
<NavComponent />
<View style={styles.container}>
{/* Page component structure/logic here */}
</View>
</View>
);
}
return null;
};
If, for example, I added a console.log to the beginning of the render function above, it's called the first time the screen is loaded, but if I go to screen #2 and then come back to screen #1 via the nav component, the console.log isn't output again. Why?
And for what it's worth, I'm using the standard navigation.navigate('ScreenName') in NavComponent to go from screen to screen.
Any advice on how to fix the warning (and/or just better design the app) so that I can have that nav on every page would be greatly appreciated. Thank you.
your api call is resulting in the warning, in the react/native ecosystem, when a component is removed from the tree, the developer needs to cancel all subscriptions (listeners to events) and async tasks(fetching data from the web), those function need to be canceld by the developer as react/native cant do that for you.
to handle that in a class based component, you need to impelment componentWillUnmount and remove the subscriptions there
class MyClass extends Component {
componentWillUnmount() {
// remove listeners and cancel requests
}
but in a modern hook component , you need to return a cleanup function, a function to return in useEffect that will be called by react to cancel any subscriptions you have made, in your case, just return a cleanup function should remove that warning for you
const [mounted, setIsMounted] useState(false)
useEffect(() => {
// getPageData is a custom method I added to axios.
setIsMounted(true)
axios.getPageData('/api/url/here', (data) => {
if(isMounted)
setServerData(data);
});
return () => {
setIsMounted(false)
}
}, []);

Set initial state for material ui dialog

I have a MaterialUI dialog that has a few text fields, drop downs, and other things on it. Some of these elements need to be set to some value every time the dialog opens or re-opens. Others elements cannot be loaded until certain conditions exist (for example, user data is loaded).
For the 'resetting', I'm using the onEnter function. But the onEnter function doesn't run until entering (duh!)... but the render function, itself, still does - meaning any logic or accessing javascript variables in the JSX will still occur. This leaves the 'onEnter' function ill-equipped to be the place I set up and initialize my dialog.
I also can't use the constructor for setting/resetting this initial state, as the data I need to construct the state might not be available at the time the constructor loads (upon app starting up). Now, I could super-complicate my JSX in my render function and make conditionals for every data point... but that's a lot of overhead for something that gets re-rendered every time the app changes anything. (the material UI dialogs appear run the entire render function even when the 'open' parameter is set to false).
What is the best way to deal with initializing values for a material ui dialog?
Here is a super-dumbed-down example (in real life, imagine getInitialState is a much more complex, slow, and potentially async/network, function) - let's pretend that the user object is not available at app inception and is actually some data pulled or entered long after the app has started. This code fails because "user" is undefined on the first render (which occurs BEFORE the onEnter runs).
constructor(props) {
super(props);
}
getInitialState = () => {
return {
user: {username: "John Doe"}
}
}
onEnter = () => {
this.setState(this.getInitialState())
}
render() {
const { dialogVisibility } = this.props;
return (
<Dialog open={dialogVisibility} onEnter={this.onEnter}>
<DialogTitle>
Hi, {this.state.user.username}
</DialogTitle>
</Dialog> );
}
My first instinct was to put in an "isInitialized" variable in state and only let the render return the Dialog if "isInitialized" is true, like so:
constructor(props) {
super(props);
this.state = {
isInitialized: false
};
}
getInitialState = () => {
return {
user: {username: "John Doe"}
}
}
onEnter = () => {
this.setState(this.getInitialState(),
() => this.setState({isInitialized:true})
);
}
render() {
const { dialogVisibility } = this.props;
if(!this.state.isInitialized) {
return null;
}
return (
<Dialog open={dialogVisibility} onEnter={this.onEnter}>
<DialogTitle>
Hi, {this.state.user.username}
</DialogTitle>
</Dialog> );
}
As I'm sure you are aware... this didn't work, as we never return the Dialog in order to fire the onEnter event that, in turn, fires the onEnter function and actually initializes the data. I tried changing the !this.state.inInitialized conditional to this:
if(!this.state.isInitialized) {
this.onEnter();
return null;
}
and that works... but it's gives me a run-time warning: Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
That brought me to a lot of reading, specifically, this question: Calling setState in render is not avoidable which has really driven home that I shouldn't be just ignoring this warning. Further, this method results in all the logic contained in the return JSX to still occur... even when the dialog isn't "open". Add a bunch of complex dialogs and it kills performance.
Surely there is a 'correct' way to do this. Help? Thoughts?
What you need conceptually is that when you are freshly opening the dialog, you want to reset some items. So you want to be able to listen for when the value of open changes from false to true.
For hooks, the react guide provides an example for keeping the "old" value of a given item with a usePrevious hook. It is then simply a matter of using useEffect.
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function MyDialog({ dialogVisibility }) {
const prevVisibility = usePrevious(dialogVisibility);
useEffect(() => {
// If it is now open, but was previously not open
if (dialogVisibility && !prevVisibility) {
// Reset items here
}
}, [dialogVisibility, prevVisibility]);
return <Dialog open={dialogVisibility}></Dialog>;
}
The same thing can be achieved with classes if you use componentDidUpdate and the previousProps parameter it receives.
export class MyDialog extends Component {
public componentDidUpdate({ dialogVisibility : prevVisibility }) {
const { dialogVisibility } = this.props;
if (dialogVisibility && !prevVisibility) {
// Reset state here
}
}
public render() {
const { dialogVisibility } = this.props;
return <Dialog open={dialogVisibility}></Dialog>;
}
}
You should use componentDidUpdate()
This method is not called for the initial render
Use this as an opportunity to operate on the DOM when the component has been updated
If you need data preloaded before the dialog is opened, you can use componentDidMount():
is invoked immediately after a component is mounted (inserted into the tree)
if you need to load data from a remote endpoint, this is a good place to instantiate the network request
React guys added the useEffect hook exactly for cases like the one you are describing, but you would need to refactor to a functional component.
Source: https://reactjs.org/docs/hooks-effect.html
This can be solved by doing leaving the constructor, getInitialState, and onEnter functions as written and making the following addition of a ternary conditional in the render function :
render() {
const { dialogVisibility } = this.props;
return (
<Dialog open={dialogVisibility} onEnter={this.onEnter}>
{this.state.isInitialized && dialogVisibility ?
<DialogTitle>
Hi, {this.state.user.username}
</DialogTitle> : 'Dialog Not Initialized'}
</Dialog> );
)}
It actually allows the dialog to use it's "onEnter" appropriately, get the right transitions, and avoid running any extended complex logic in the JSX when rendering while not visible. It also doesn't require a refactor or added programming complexity.
...But, I admit, it feels super 'wrong'.

React render() is being called before componentDidMount()

In my componentDidMount() I am making an API call to fetch some data, this call then sets a state object that I use in my render.
componentDidMount() {
const { actions } = this.props;
this.increase = this.increase.bind(this);
// api call from the saga
actions.surveyAnswersRequest();
// set breadcrumb
actions.setBreadcrumb([{ title: 'Score' }]);
actions.setTitle('Score');
this.increase();
}
In my render function I pass some prop values onto the view file:
render() {
const { global, gallery, survey_answers, survey, survey_actual_answers } = this.props;
if (global.isFetching) {
return <Loading />;
}
return this.view({ gallery, survey_answers, survey, survey_actual_answers });
}
The problem I am having is that the survey_actual_answers prop is not being set the first time that the page is loaded, however when I refresh the page the prop returns the data fine and the rest of the script will run. It's only the first time that it returns an empty array for that prop value.
This is how I have passed my props in:
Score.propTypes = {
actions: PropTypes.object.isRequired,
global: PropTypes.object.isRequired,
survey: PropTypes.object.isRequired,
survey_answers: PropTypes.object.isRequired,
gallery: PropTypes.object.isRequired,
survey_actual_answers: PropTypes.array.isRequired,
survey_score_system: PropTypes.array.isRequired,
survey_styles: PropTypes.object.isRequired,
survey_general_doc_data: PropTypes.object.isRequired
};
function mapStateToProps(state, ownProps) {
return {
...ownProps,
global: state.global,
gallery: state.gallery,
survey: state.survey,
survey_actual_answers: state.survey.survey_actual_answers,
survey_answers: state.survey.survey_answers,
survey_score_system: state.survey.survey_score_system,
survey_styles: state.survey.survey_styles,
survey_general_doc_data: state.survey.survey_general_doc_data,
isFetching: state.isFetching
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
...globalActions,
...galleryActions,
...surveyActions
}, dispatch)
};
}
Does anyone know why this is happening? It's almost as if it's not calling componentDidMount at all.
This is happening because of how React works fundamentally. React is supposed to feel fast, fluent and snappy; the application should never get clogged up with http requests or asynchronous code. The answer is to use the lifecycle methods to control the DOM.
What does it mean when a component mounts?
It might be helpful to understand some of the React vocabularies a little better. When a component is mounted it is being inserted into the DOM. This is when a constructor is called. componentWillMount is pretty much synonymous with a constructor and is invoked around the same time. componentDidMount will only be called once after the first render.
componentWillMount --> render --> componentDidMount
How is that different than rerendering or updating?
Now that the component is in the DOM, you want to change the data that is displayed. When calling setState or passing down new props from the parent component a component update will occur.
componentWillRecieveProps --> shouldComponentUpdate-->componentWillUpdate
-->render-->componentDidUpdate
It is also good to note that http requests are usually done in componentDidMount and componentDidUpdate since these are places that we can trigger a rerender with setState.
So how do I get the data before the render occurs?
Well, there are a couple of ways that people take care of this. The first one would be to set an initial state in your component that will ensure that if the data from the http request has not arrived yet, it will not break your application. It will use a default or empty state until the http request has finished.
I usually don't like to have a loading modal, but sometimes it is necessary. For instance, when a user logs in you don't want to take them to a protected area of your site until they are finished authenticating. What I try to do is use that loading modal when a user logs in to front load as much data as I possibly can without affecting the user experience.
You can also make a component appear as loading while not affecting the user experience on the rest of the site. One of my favorite examples is the Airbnb website. Notice that the majority of the site can be used, you can scroll, click links, but the area under 'experiences' is in a loading state. This is the correct way to use React and is the reason why setState and HTTP requests are done in componentDidMount/componentDidUpdate.
Using setState in componentdidmount. This my code:
async componentDidMount() {
danhSachMon = await this.getDanhSachMon();
danhSachMon=JSON.parse(danhSachMon);
this.setState(danhSachMon);
}
render() {
return (
<View>
<FlatList
data={danhSachMon}
showsVerticalScrollIndicator={false}
renderItem={({ item }) =>
<View >
<Text>{item.title}</Text>
</View>
}
keyExtractor={(item, index) => index.toString()}
/>
</View>
)
}
componentWillMount is deprecated. Now you need to use "DidMount" and as soon as it finishes and changes the DOM on render, react will handle everything else.
Make sure you update and use the correct variables/state/props in the render.
componentDidMount() {
const { applicationId } = this.props;
if (applicationId) {
ApplicationService.getQuotesByApplicationId(applicationId).then((response) => {
const quotes = _.get(response, 'data.data');
this.setState({
quotes,
});
});
....
}
render() {
const quotes = _.get(this.state, 'quotes', null);
return (
<div className="application">
<h4 className="application__sub">Application</h4>
<h1 className="application__title">Quote Comparison</h1>
<div className="form-wrapper">
{this.renderQuotes(quotes)}
</div>
<div />
</div>
);
}
Here I get the quotes from the API and as soon as it finishes it set a variable in the state, then the render and react do their work.

Categories