I am developing an android app using react-native and i have a but of a struggle of figuring out why my component re-renders on each page navigation.
In my case i have created a wrapper component called Container.android.js
const Container = props => {
useEffect(() => {
// fetching some data from async storage
},[])
// Using my hook here
const {code, error} = usePdaScan({
onEvent: (code) => {
// setting state via useState
},
onError: (error) => {
Alert.alert('Error', error);
},
trigger: 'always'
});
return <View>
{props.children}
</View>
}
Then i declare routes with Stack.Screen where each stack uses a component wrapped with the Container component.
<Stack.Screen name="Overview" component={Overview} />
And my Overview compnent is this
const Overview = props => {
return <Container>
<Text>Overview page</Text>
</Container>
}
My problem is that inside the Container component, there is a hook called usePdaScan. Each time i navigate to another page and the hook gets called, the Container component re-renders twice... I cant get a lead on this... Halp!
UPDATE: My homepage is a class component where it seems to work ok (render only once)
class Home extends Component {
state = {
productBarcodes: null
}
componentDidMount(){
// get data fetches data from async storage
getData('#productBarcodes').then(res => {
this.setState({
...this.state,
productBarcodes: JSON.parse(res)
})
})
}
render() {
const { productBarcodes } = this.state;
return (
<Container {...this.props}>
<View style={{marginBottom: 20, flex:1}}>
{productBarcodes.length} Products
</View>
</Container>
)
}
}
The usePdaScan hook in the Container component gets called once on my Homepage but twice on every other page like Overview etc... Both Homepage and Overview are wrapped with my Container component
Related
To keep it simple, the detail page fetches data on mount based on the movie ID in the URL, this coming from path='movie/:id' in the Route.
It's child is called Recommended, which shows you recommended movies based again on the current URL.
class MovieDetailPage extends React.Component {
// Fetch movies and cast based on the ID in the url
componentDidMount() {
this.props.getMovieDetails(this.props.match.params.id)
this.props.getMovieCast(this.props.match.params.id)
}
render() {
<div>
Movies here
</div>
<Recommended id={this.props.match.params.id}/>
}
}
The Recommended component fetches data based on the current movie as well and generates another tag pointing to another movie.
class Recommended extends React.Component {
componentDidMount() {
this.props.getRecommended(this.props.id)
}
render() {
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{
this.props.recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
>
</img>
</Link>
)
})
}
</div>
</>
)
}
}
Now how can I trigger another render of the parent component when clicking the Link generated in the Recommended component? The URL is changing but this won't trigger a render like I intent to do.
UPDATE:
<Route
path="/movie/:id"
render={(props) => (
<MovieDetailPage key={props.match.params.id}
{...props}
)}
/>
I passed in a unique key this time that triggered the re-render of the page. I tried this before but I might've screwed up the syntax.
This post got me in the right direction: Force remount component when click on the same react router Link multiple times
Add a key to the page
If you change route but your page is not getting its "mount" data then you should add a key to the page. This will cause your page to rerender and mount with the new id and get the data again.
You can read more about react keys here
A key tells react that this is a particular component, this is why you see them in on lists. By changing the key on your page you tell react that this is a new instantiation of the component and has changed. This will cause a remount.
Class component example
class MyPage extends React.Component {
componentDidMound() {
// this will fire each time the key changes since it triggers a mount
}
render() {
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
}
Functional component example
const MyPage = (props) => {
React.useEffect(() => {
// this will fire each time the key changes
}, []);
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
You can add another React lifecycle method that triggers on receiving new props (UNSAFE_componentWillReceiveProps, componentDidUpdate, getDerivedStateFromProps) in your Recommended component like this:
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.id !== this.props.id) {
nextProps.getRecommended(nextProps.id);
};
}
You can also add key to your component (which forces it to re-render completely if key changed) like this:
<Recommended key={this.props.match.params.id} id={this.props.match.params.id}/>
You can also use React Hooks to handle this more easily with useEffect:
const Recommended = (props) => {
const { id, getRecommended, recommended } = props;
useEffect(() => {
id && getRecommended(id);
}, [id]);
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
></img>
</Link>
);
})}
</div>
</>
);
};
Note: adding key to component and complete its re-render is not best practice and you should be using Component's lifecycles to avoid it if possible
When initializing widget component which loads various charts it works as it should, but when switching to another ListItem , componentDidMount do not load when switch to another item. I need to load it, because it fetch required data for it. But the thing is when I am switching between ListItem did not initialize componentDidMount
DashboardSidebar.jsx
class DashboardSidebar extends Component {
constructor(props) {
super(props);
this.state = {
tabLocation: this.props.tabLocation
};
this.onChange = this.onChange.bind(this);
}
onChange(event) {
this.setState({
tabLocation: event.target.value
});
}
render() {
const { classes } = this.props;
const { path } = this.props.match;
const { reports = [], sites = [] } = this.props;
let fromFilterString = "-"
let toFilterString = "-"
return (
<Drawer
className={classes.drawer}
variant="permanent"
classes={{
paper: classes.drawerPaper,
}}>
<div>
<List>
{reports.map((report) => (
<ListItem
onClick={this.onChange} button key={report.id}>
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText
disableTypography
primary={
<Typography type="body2">
<Link to={`${path}/${report.slug}`} style={{ color: "#000" }}>
{report.name}
</Link>
</Typography>
}
/>
</ListItem>
))}
</List>
</div>
<Divider light />
</Drawer>
)
}
}
This component seems run correct, but when clicking on other ListItem run componentDidUpdate which do not fetch required data for charts. Also I find out that when I changed in MainDashboard component key={i} to key={l.id} is started to hit componentDidMount, but then widget's do not load, but from Console I can see that it hit componentDidMount and fetch data which I console.log() .
MainDashboard.jsx
class MainDashboard extends Component {
componentDidUpdate(prevProps) {
if (this.props.match.params.report === prevProps.match.params.report) {
return true;
}
let widgets = {};
let data = {};
let layout = {};
fetch(...)
.then(response => response.json())
.then(data => {
...
this.setState({dashboard: data, isLoading: false, layouts: layout });
})
}
componentDidMount() {
this.setState({ mounted: true, isLoading: true });
let widgets = {};
let data = {};
let layout = {};
fetch(...
})
.then(response => response.json())
.then(data => {
...
this.setState({dashboard: data, isLoading: false, layouts: layout });
})
}
sortWidgets(widgets) {
...
return widgets;
}
generateDOM() {
return _.map(this.state.dashboard.widgets, function(l, i) {
....
return (
<div key={i}>
<ChartWidget
visualization={l.visualization}
name={l.visualization.name}
</div>
);
}.bind(this));
}
render() {
return (
...
);
}
}
You have three option:
1- Use another life cycle such as componentDidUpdate, fetch the new data if there is particular change in props or state
2- Use a new Key, if you use a new key on a component, it will trigger a componentDidMount, because the component is rendered again as a new entity
3- Use react.ref
I think you should read up on all three choices and choose one that will fit you the best.
So componentDidMount is actually being hit but nothing is happening in terms of data updates? in that case, your component loads/mounts first and whatever needs to happen doesn't trigger a re-render, I would look into using another lifecycle method to ensure that your data is updated.
I'm not sure if you're working on a legacy system but if upgrading to functional components is an option, I would recommend using the lifecycle method useEffect because it replaces many lifecycle methods like componentDidMount , componentDidUpdate and the unsafe componentWillUnmount and will make your life a whole lot easier and more efficient.
componentDidMount is only executed when a component is mounted.
A state update in DashboardSidebar would not cause BaseDashboard to be re-mounted so componentDidMount will not be re-executed for BaseDashboard.
Have you tried fetching the data in the onChange event handler (for switching to another ListItem) instead?
I'm practicing react and redux and I'm creating a simple app where I have a sidebar showing a list of categories that is visible on every route and the main area that initially displays all the books I have and when clicking on a category link on the sidebar the main area loading another component with all the books related to this category.
Here's my routes setup in the App.js file ...
class App extends Component {
async componentDidMount() {
try {
await this.props.asyncLoadBooks();
await this.props.asyncLoadCategories();
} catch (error) {
console.log(error);
}
}
render() {
return (
<>
<Header />
<div className="global-wrapper">
<div className="container">
<aside className="side-bar">
<Categories />
</aside>
<main className="main-content">
<Switch>
<Route exact path="/" component={Books} />
<Route
exact
path="/category/:id"
component={Category}
/>
<Route component={NotFound} />
</Switch>
</main>
</div>
</div>
</>
);
}
}
In the App.js as you can see I'm loading the data via a local JSON file with axios in the Actions files of the booksActions and categoriesAction, it's pretty straightforward.
And here's the Categories component ...
class Categories extends Component {
render() {
const { categories } = this.props;
let categoriesList;
if (categories && categories.length !== 0) {
categoriesList = categories.map(category => (
<li key={category.id}>
<Link to={`/category/${category.id}`}>{category.name}</Link>
</li>
));
} else {
categoriesList = <Loading />;
}
return (
<div>
<h2>Categories</h2>
<ul>{categoriesList}</ul>
</div>
);
}
}
const mapState = state => ({
categories: state.categories.categories
});
export default connect(mapState)(Categories);
And I'm firing another action in the ComponentDidMount() of the single Category component to get all the books related to that component and render them ...
class Category extends Component {
componentDidMount() {
this.props.getCategoryBooks(this.props.match.params.id);
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
this.props.getCategoryBooks(this.props.match.params.id);
}
}
render() {
const { categoryBooks } = this.props;
return (
<div>
{/* <h1>{this.props.match.params.id}</h1> */}
{categoryBooks &&
categoryBooks.map(book => {
return <div key={book.id}>{book.title}</div>;
})}
</div>
);
}
}
const mapState = state => ({
categories: state.categories.categories,
categoryBooks: state.books.categoryBooks
});
const mapActions = {
getCategoryBooks
};
export default connect(
mapState,
mapActions
)(Category);
Now, everything is working the first time, however, when I click on another category the <Category /> component doesn't get updated because I'm dispatching the action in the componentDidMount() thus the component already mounted the first time, so it doesn't dispatch the action again after I click on another category, now what is the best way to handle this?
The second issue is where I'm on a category route http://localhost:3000/category/9967c77a-1da5-4d69-b6a9-014ca20abd61 and I try to refresh the page, the categoris list loads fine on the sidebar, but the single component shows empty, and when I look on the redux-devtools I find that the GET_CATEGORY_BOOKS action gets fired before the LOAD_BOOKS and LOAD_CATEGORIES in the App.js file, because the child componentDidMount() method gets called before its parent equivalent method. How to solve this as well?
I hope you guys can help me in this.
Edit
As ##NguyễnThanhTú noticed, the componentDidupate had a typo, now it works when clicking on another category.
That leaves us with the second issue when reloading the page in the category route and the data doesn't show because of the App.js componentDidMount fires after its children components.
Edit
Here's a repo on Github for this project ...
https://github.com/Shaker-Hamdi/books-app
In your booksActions.js, add this:
export const getCategoryBooksV2 = categoryId => {
return async (dispatch, getState) => {
const { books } = getState();
if (books.books.length === 0) {
console.log('Only executing once') // testing purpose only
const response = await axios.get("books.json");
const data = response.data.books;
dispatch(loadBooks(data));
dispatch(getCategoryBooks(categoryId));
}
dispatch(getCategoryBooks(categoryId));
};
};
In your Category.js, use that new action creator:
import { getCategoryBooksV2 } from "../books/booksActions";
...
componentDidMount() {
this.props.getCategoryBooksV2(this.props.match.params.id);
}
...
const mapActions = {
getCategoryBooksV2
};
This solution is inspired by this example:
function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
};
}
From the Redux-Thunk Documentation
This is the demo:
I have a tricky situation with react router 4.
Imagine I have a route
<Route path='/search/:term?' render={(props) => {
return (<ProductList
setFlag={(flag)=>this.setState({flag})}
{...props}
/>)
}} />
Now you can see I am using render in Route which means it will not unmount this component at each render rather update the old instance with new props.
However, at some point inside the ProductList the user calls setFlag function which you can see updates some property in parent.
Because of this, a rerender of the parent is caused. Which also calls componentWillReceiveProps(CWRP) of ProductList. Inside CWRP of ProductList I am always (unconditionally) fetching items with new props.
This causes my problem. You can see that when user updated flag, there was no need to fetch data again in CWRP, because updating that flag wasn't related to my data.
You could say that I should put some condition in CWRP that would do some check and fetch data only when it is necessary. However, I find it impossible to come up with such check. Because for example, ProductList receives a search term. I could, for example, compare a search term from the previous render to search term of new render and if they are different then to fetch data, however, that is incorrect, because even in case of same search term a fetch should be issued (maybe the data was updated on a server).
What solution do you see in such a situation?
So that my product list doesn't fetch data everytime the flag of parent changes?
Elevate your state and move your logic out of the render method and into a parent container-component, then utilize this.setState() to stop state updates OR use shouldComponentUpdate() to continue to allow state updates BUT stop re-renders when the flag hasn't been changed (either one will prevent ProductList from being updated):
import React, { Component } from 'react';
import ProductList from './ProductList';
export default class SearchTerms extends Component {
state = { flag: '' };
shouldComponentUpdate = (nextProps, nextState) => ( this.state.flag !== nextState.flag )
handleFlag = flag => this.setState(prevState => { return this.state.flag !== flag ? { flag } : null })
render = () => ( <ProductList setFlag={this.handleFlag} {...this.state} {...this.props} /> )
}
Then the route will change to:
<Route path='/search/:term?' component={SearchTerms} />
In addition, I'd avoid using componentWillReceiveProps() altogether and instead use componentDidUpdate().
Here's an example of a parent container-component controlling several component children. The children can update the parent via a passed down parent method.
In this simple example, searchForPlayer's onChange and onSubmit updates the parent's searchTerm state and changes the URL query via parent's handleSubmit method. The URL query change triggers the parent's componentDidUpdate method, which then fetches new data and updates the displayPlayerList component.
URL before:
/players/all
URL after form submit:
/players/player?number=${this.state.searchTerm}
So if a user types out the URL to:
/players/player?number=10
or
/players/fdskmsdfk?number=10
and hits enter, it'll load a filtered list because it's only looking for a number query.
If they go to:
/player/dsfdsdfdsdf
or
player/1223345
or anything without a number query, then it'll just fetch all the players instead (this can be handled differently, but was done for simplicity).
Working example: https://codesandbox.io/s/xn3p3o6vq
containers/PlayersList.js (parent container-component)
import isEmpty from "lodash/isEmpty";
import React, { Component, Fragment } from "react";
import qs from "qs";
import DisplayPlayerList from "../components/displayPlayerList";
import NoPlayerFound from "../components/noPlayerFound";
import SearchForPlayer from "../components/searchForPlayer";
import ServerError from "../components/serverError";
import Spinner from "../components/spinner";
export default class PlayersList extends Component {
state = {
err: "",
isLoading: true,
searchTerm: "",
players: [],
noplayer: "",
number: ""
};
componentDidMount = () => this.fetchPlayers();
componentDidUpdate = (prevProps, prevState) => this.props.location.search !== prevProps.location.search && this.fetchPlayers();
fetchPlayers = () => {
const { number } = qs.parse(this.props.location.search, { ignoreQueryPrefix: true })
fetch(`https://jsonplaceholder.typicode.com/users${number ? `/${number}` : ""}`)
.then(response => response.json())
.then(players =>
this.setState({
err: "",
players: !number ? [...players] : [players],
noplayer: isEmpty(players) ? true : false,
isLoading: false,
number,
searchTerm: ""
})
)
.catch(err => this.setState({ err: err.toString() }));
};
handleChange = e => this.setState({ searchTerm: e.target.value });
handleSubmit = e => {
e.preventDefault();
this.props.history.push(`/players/player?number=${this.state.searchTerm}`);
};
render = () => (
this.state.isLoading // (if isLoading is true..)
? <Spinner /> // (then show a spinner)
: <div style={{ padding: 20, width: 500 }}> // (otherwise...)
<SearchForPlayer // (show player search form and...)
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
{...this.state}
/>
{ this.state.err // (if there's an error...)
? <ServerError {...this.state} /> // (show the error)
: this.state.noplayer // (otherwise, if there's no player...)
? <NoPlayerFound {...this.state} /> // (show no player found)
: <DisplayPlayerList {...this.state} /> // (otherwise, display updated list)
}
</div>
);
}
components/searchForPlayer.js (child component)
import React from "react";
export default ({ handleChange, handleSubmit, searchTerm }) => (
<form onSubmit={handleSubmit}>
<input
className="uk-input"
type="number"
value={searchTerm}
onChange={handleChange}
placeholder="Search for player by number..."
style={{ width: 300, marginRight: 10 }}
min={1}
/>
<button
disabled={!searchTerm}
className="uk-button uk-button-primary"
type="submit"
>
Search
</button>
</form>
);
components/displayPlayerList.js (child component)
import map from "lodash/map";
import React from "react";
export default ({ players }) => (
<ul style={{ listStyleType: "none" }}>
{map(players, ({ id, name, username, email }) => (
<li style={{ margin: "10px 0" }} key={id}>
<strong>Player # {id}</strong>
<span> - {name}</span>
</li>
))}
</ul>
);
components/noPlayerFound.js (child component)
import React from "react";
export default ({ number }) => (
<div style={{ color: "red", padding: 20 }}>
No player was found matching #{number}!
</div>
);
component/serverError.js (child component)
import React from "react";
export default ({ err }) => (
<div style={{ color: "red", padding: 20 }}>
<i style={{ marginRight: 5 }} className="fas fa-exclamation-circle" /> {err}
</div>
);
According to the docs on react-navigation you can call navigate from the top level component using the following:
import { NavigationActions } from 'react-navigation';
const AppNavigator = StackNavigator(SomeAppRouteConfigs);
class App extends React.Component {
someEvent() {
// call navigate for AppNavigator here:
this.navigator && this.navigator.dispatch(
NavigationActions.navigate({ routeName: someRouteName })
);
}
render() {
return (
<AppNavigator ref={nav => { this.navigator = nav; }} />
);
}
}
However I'm trying to figure out how can this be done if the logic that does the dispatch is in another component that is rendered on the same level as the navigator? In my case I create my navigators (A drawer with a stack navigator and other nested navigators) and then I render them using the <Drawer>. On the same level I'm loading my <PushController> component to handle push notifications. The pushcontroller actually gets the event that I want to dispatch on.
I can't figure out how to pass(?) the ref to the pushcontroller component so I can use it, currently the following isn't working. I get the console log telling me that the fcm.ACTION.OPEN_NOTIFICATION triggered but no dispatch occurs. I suppose it could be because the ref is created during a render and it isn't available to pass yet when the render occurs? But I'm also not sure you would do things this way in order to give another component access to a ref declared at the same level. Thanks for your help in advance!
Drawer + PushController rendering
render(){
return(
<View style={{flex: 1}}>
<Drawer ref={nav => { this.navigator = nav; }}/>
<PushController user={this.props.user} navigator={this.navigator}/>
</View>
)
}
PushController snippet:
import { NavigationActions } from 'react-navigation';
async doFCM() {
FCM.getInitialNotification().then(notif => {
console.log('Initial Notification', notif);
if(notif.fcm.action === "fcm.ACTION.OPEN_NOTIFICATION"){
console.log('fcm.ACTION.OPEN_NOTIFICATION triggered', notif);
this.props.navigator && this.props.navigator.dispatch(NavigationActions.navigate({routename: 'Chat'}))
}
});
}
Answer was to move the navigate call to a function defined at render and pass it to the component.
import { NavigationActions } from 'react-navigation';
...
//class definition omitted for brevity
render(){
const callNavigate = (routeName, params) => {
console.log('Params', params);
this.navigator.dispatch({
type: NavigationActions.NAVIGATE,
routeName: routeName,
params: params
})
}
return(
<View style={{flex: 1}}>
<Drawer ref={nav => this.navigator = nav }/>
<PushController callNavigate={callNavigate}/>
</View>
)
}
The function is called within PushController like this:
this.props.callNavigate('RouteName', params);