Child route is blocking the parent route`s render - reactjs

I have a react app which is using react-router. I`m using plain routes, but this is how components represent my routing
<Routes>
<Route component={CoreLayout}>
<Route component={AppLayout}
onEnter={fetchData}>
<Route path='dashboard'
onEnter={fetchStatistics}
component={Dashboard}>
</Route>
</Route>
</Routes>
The situation now
First, the app layout is going to block every render while it is fetching the necessary data from the server (like the User data). Then if we have the data, we can step on the child routes, like in this case the Dashboard route, where we are loading the content of the pages.
The goal
The problem is whit this strategy, we are going to show a blank white page until the onEnter on the main route is resolved.
To avoid this, I would like to load the AppLayout component, but without starting the onEnter function on the child route. To do this, I can show a waiting spinner where the child component would load, and when the data is loaded I can start loading the child`s data.
tl;dr
The question is, how can I make the parent layout to render, while the child route`s onEnter is not loaded.

Instead of using onEnter, you can have your <Dashboard> initiate its data fetching in its component(Will|Did)Mount method. Have it maintain a state.loaded boolean which displays a spinner when state.loaded = false.
class Dashboard extends React.Component {
constructor(props) {
super(props)
this.state = {
loaded: false
}
}
componentWillMount() {
// mock data fetch call that uses a promise
fetchStatistics()
.then(resp => resp.json())
.then(data => {
this.setState({
loaded: true,
data
})
})
}
render() {
// if data hasn't been loaded, render a spinner
if (!this.state.loaded) {
return <Spinner />
}
// data has been loaded, render the dashboard
return (
<div>...</div>
)
}
}
Edit:
It doesn't handle data loading errors, but here is an example of a general purpose data loading HOC that should work (haven't tested it):
/*
* #Component is the component to render
* #fetchData is a function which fetches the data. It takes
* a callback function to trigger once data fetching has
* completed.
*/
const withData = (Component, fetchData) => {
return class WithData extends React.Component {
constructor(props) {
super(props)
this.state = {
loaded: false
}
}
componentWillMount() {
this.props.fetchData(() => {
this.setState({ loaded: true })
})
}
render() {
return this.state.loaded ? (
<Component {...this.props} />
) : (
<Spinner />
)
}
}
}
Usage
function fetchStatistics(callback) {
fetch('/api/dashboard')
.then(resp => resp.json())
.then(data => {
dispatch(dashboardData(data))
callback()
})
})
<Route
path='dashboard'
component={withData(Dashboard, fetchStatistics} />

Related

Dispatching Redux actions on location change with react-router-dom

I am using React and Redux for a search application. Using react-router-dom, I'm routing /search/:term? to a Search component:
<Router>
<Switch>
<Route exact path="/search/:term?" component={Search} />
<Redirect to="/search" />
</Switch>
const Search = (props) => {
const { term } = props.match.params;
return (
<div>
<SearchForm term={term}/>
<SearchResults />
</div>
)
};
When a user submits a search in the SearchForm component, I'm dispatching an action to submit the search query. I'm also initiating a search in the constructor if a term is given, initially:
class SearchForm extends Component {
constructor(props) {
super(props);
const term = props.term ? props.term : '';
this.state = {
term: term,
}
if (term) {
this.props.submitSearch(term);
}
}
handleSubmit = (e) => {
e.preventDefault();
if (this.state.term) {
this.props.submitSearch(this.state.term);
}
}
render = () => {
<form
onSubmit={this.handleSubmit.bind(this)}>
...
</form>
}
}
I'm using withRouter from react-router-dom, so the URL updates when the search is submitted.
The problem happens when the user navigates Back in their browser. The URL navigates back, the props update (i.e. props.match.params.term), but the search does not resubmit. This is because the submitSearch action only gets dispatched in SearchForm.constructor (search on initial loading if a term is in the URL) and SearchForm.handleSubmit.
What is the best way to listen for a state change to term when the URL changes, then dispatch the search action?
I would retrieve the route parameter in componentDidMount since you are pushing a new route and therefore reloading the view.
Inside your SearchForm it would look like this.
state = {
term: '';
}
onChange = (term) => this.setState({ term })
onSubmit = () => this.props.history.push(`/search/${this.state.term}`);
And in your SearchResult :
componentDidMount() {
this.props.fetchResults(this.props.term)
}
A nice thing to do would be to keep the SearchResult component dry. There are several ways to achieve that, here is one using higher order components aka HOC :
export default FetchResultsHoc(Component) => {
#connect(state => ({ results: state.searchResults }))
class FetchResults extends React.Component {
componentDidMount(){
dispatch(fetchResults(this.props.match.params.term))
}
render(){
<Component {...this.props} />
}
}
return FetchResultsHoc;
}
That you would then call on your SearchResult component using a decorator.
import { fetchResults } from './FetchResultsHoc';
#fetchResults
export default class SearchResult extends React.PureComponent { ... }
// You have now access to this.props.results inside your class
My current solution is to dispatch submitSearch in the componentWillRecieveProps lifecycle method if the new props don't match the current props:
componentWillReceiveProps(nextProps) {
if (this.props.term !== nextProps.term) {
this.setState({
term: nextProps.term,
});
this.props.submitSearch(nextProps.term);
}
}
Then, instead of dispatching an action on form submission, I push a new location onto the history and componentWillReceiveProps does the dispatching:
handleSubmit = (e) => {
e.preventDefault();
if (this.state.term) {
this.props.history.push('/search/'+this.state.term);
}
}
This solution feels a little wrong, but it works. (Other's would seem to agree: Evil things you do with redux — dispatch in updating lifecycle methods)
Am I violating a React or Redux principle by doing this? Can I do better?

Handling responded forms in react

I'm doing a simple project that has something like 3 forms and right now I start the component with empty Inputs and then request data from API to pre-populate the form using the componentWillMount() hook.
It works for me now but if someday my app need more and more data it would be annoying to do this everytime for any new form and I would like to know if there is any lib or pattern to help pre-populating forms without using any state container (Redux, mobx, and I really don't know if they are needed in this case).
It is better to do your data fetching in componentDidMount than in componentWillMount:
If you need to load data from a remote endpoint, this is a good place
to instantiate the network request.
If you want to reuse some data fetching logic without any external state you could use Component with render props or Higher Order Components.
For example:
function withData(fetchData) {
return BaseComponent => {
class WithData extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
}
componentDidMount(){
fetchData().then(response => {
this.setState({ data: response })
})
}
render(){
return <BaseComponent {...this.props} data={this.state.data} />
}
}
return WithData;
}
}
And later you can reuse this logic:
const DataList = ({ data }) => (
<ul>
{
data.map(item =>
<li>{item.name}</li>
)
}
</ul>
)
// passing Promises as a `data` producers
const UserDataList = withData(fetchUsers)(DataList);
const GroupDataList = withData(fetchGroups)(DataList);
const CatsDataList = withData(() => fetchAnimals('cats'))(DataList);
const ListOfEverything = () => (
<Container>
<UserDataList />
<GroupDataList />
<CatsDataList />
</Container>
)

What is best approach to set data to component from API in React JS

We have product detail page which contains multiple component in single page.
Product Component looks like:
class Product extends Component {
render() {
return (
<div>
<Searchbar/>
<Gallery/>
<Video/>
<Details/>
<Contact/>
<SimilarProd/>
<OtherProd/>
</div>
);
}
}
Here we have 3 APIs for
- Details
- Similar Product
- Other Products
Now from Detail API we need to set data to these components
<Gallery/>
<Video/>
<Details/>
<Contact/>
In which component we need to make a call to API and how to set data to other components. Lets say we need to assign a,b,c,d value to each component
componentWillMount(props) {
fetch('/deatail.json').then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ data, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
OR
Do we need to create separate api for each components?
Since it's three different components you need to make the call in the component where all the components meet. And pass down the state from the parent component to child components. If your app is dynamic then you should use "Redux" or "MobX" for state management. I personally advise you to use Redux
class ParentComponent extends React.PureComponent {
constructor (props) {
super(props);
this.state = {
gallery: '',
similarPdts: '',
otherPdts: ''
}
}
componentWillMount () {
//make api call and set data
}
render () {
//render your all components
}
}
The Product component is the best place to place your API call because it's the common ancestor for all the components that need that data.
I'd recommend that you move the actual call out of the component, and into a common place with all API calls.
Anyways, something like this is what you're looking for:
import React from "react";
import { render } from "react-dom";
import {
SearchBar,
Gallery,
Video,
Details,
Contact,
SimilarProd,
OtherProd
} from "./components/components";
class Product extends React.Component {
constructor(props) {
super(props);
// Set default values for state
this.state = {
data: {
a: 1,
b: 2,
c: 3,
d: 4
},
error: null,
isLoading: true
};
}
componentWillMount() {
this.loadData();
}
loadData() {
fetch('/detail.json')
.then(response => {
// if (response.ok) {
// return response.json();
// } else {
// throw new Error('Something went wrong ...');
// }
return Promise.resolve({
a: 5,
b: 6,
c: 7,
d: 8
});
})
.then(data => this.setState({ data, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
render() {
if (this.state.error) return <h1>Error</h1>;
if (this.state.isLoading) return <h1>Loading</h1>;
const data = this.state.data;
return (
<div>
<SearchBar/>
<Gallery a={data.a} b={data.b} c={data.c} d={data.d} />
<Video a={data.a} b={data.b} c={data.c} d={data.d} />
<Details a={data.a} b={data.b} c={data.c} d={data.d} />
<Contact a={data.a} b={data.b} c={data.c} d={data.d} />
<SimilarProd/>
<OtherProd/>
</div>
);
}
}
render(<Product />, document.getElementById("root"));
Working example here:
https://codesandbox.io/s/ymj07k6jrv
You API calls will be in the product component. Catering your need to best practices, I want to make sure that you are using an implementation of FLUX architecture for data flow. If not do visit phrontend
You should send you API calls in componentWillMount() having your state a loading indicator that will render a loader till the data is not fetched.
Each of your Components should be watching the state for their respective data. Let say you have a state like {loading:true, galleryData:{}, details:{}, simProducts:{}, otherProducts:{}}. In render the similar products component should render if it finds the respective data in state. What you have to do is to just update the state whenever you receive the data.
Here is the working code snippet:
ProductComponent:
import React from 'react';
import SampleStore from '/storepath/SampleStore';
export default class ParentComponent extends React.Component {
constructor (props) {
super(props);
this.state = {
loading:true,
}
}
componentWillMount () {
//Bind Store or network callback function
this.handleResponse = this.handleResponse
//API call here.
}
handleResponse(response){
// check Response Validity and update state
// if you have multiple APIs so you can have a API request identifier that will tell you which data to expect.
if(response.err){
//retry or show error message
}else{
this.state.loading = false;
//set data here in state either for similar products or other products and just call setState(this.state)
this.state.similarProducts = response.data.simProds;
this.setState(this.state);
}
}
render () {
return(
<div>
{this.state.loading} ? <LoaderComponent/> :
<div>
<Searchbar/>
<Gallery/>
<Video/>
<Details/>
<Contact/>
{this.state.similarProducts && <SimilarProd data={this.state.similarProducts}/>}
{this.state.otherProducts && <OtherProd data={this.state.otherProducts}/>}
</div>
</div>
);
}
}
Just keep on setting the data in the state as soon as you are receiving it and render you components should be state aware.
In which component we need to make a call to API and how to set data
to other components.
The API call should be made in the Product component as explained in the other answers.Now for setting up data considering you need to make 3 API calls(Details, Similar Product, Other Products) what you can do is execute the below logic in componentDidMount() :
var apiRequest1 = fetch('/detail.json').then((response) => {
this.setState({detailData: response.json()})
return response.json();
});
var apiRequest2 = fetch('/similarProduct.json').then((response) => { //The endpoint I am just faking it
this.setState({similarProductData: response.json()})
return response.json();
});
var apiRequest3 = fetch('/otherProduct.json').then((response) => { //Same here
this.setState({otherProductData: response.json()})
return response.json();
});
Promise.all([apiRequest1,apiRequest2, apiRequest3]).then((data) => {
console.log(data) //It will be an array of response
//You can set the state here too.
});
Another shorter way will be:
const urls = ['details.json', 'similarProducts.json', 'otherProducts.json'];
// separate function to make code more clear
const grabContent = url => fetch(url).then(res => res.json())
Promise.all(urls.map(grabContent)).then((response) => {
this.setState({detailData: response[0]})
this.setState({similarProductData: response[1]})
this.setState({otherProductData: response[2]})
});
And then in your Product render() funtion you can pass the API data as
class Product extends Component {
render() {
return (
<div>
<Searchbar/>
<Gallery/>
<Video/>
<Details details={this.state.detailData}/>
<Contact/>
<SimilarProd similar={this.state.similarProductData}/>
<OtherProd other={this.state.otherProductData}/>
</div>
);
}
}
And in the respective component you can access the data as :
this.props.details //Considering in details component.

SplashScreen in react native

I need to make an image as a background for the whole first screen as a SplashScreen and after some fixed time it shows the other component.
I created two components Home and SplashScreen and here is the code i'm using:
componentDidMount() {
SplashScreen.hide();
}
render() {
return(
<View>
<SplashScreen/>
<Home/>
</View>
)
}
Please any help or idea
You need to implement this in Javascript land.
Top most component would hold a flag indicating to render splash screen.
Update this flag after specified amount of time and render desired content.
Dummy implementation may look like this...
class App extends Component {
state = {
ready: false,
}
componentDidMount () {
setTimeout(() => {
this.setState({ ready: true })
}, 5000)
}
render() {
if (this.state.ready === false) {
return <Splash />
}
return this.props.children;
}
}
// Usage example:
<App>
<RouterOrSomething />
</App>

Passing data from child route to father route

I have a route structure like this:
<Route path="/master" component={MasterPageLayout}>
<IndexRoute path="/master/products" component={ProductsPage}/>
<Route path="/master/customer/:id" component={CustomerDetailsPage}/>
<Route path="/master/product/:id" component={ProductDetailsPage}/>
<Route path="/master/price-table" component={PriceTablePage} />
</Route>
<Route path="/poc" component={DistribuitorPageLayout}>
<IndexRoute path="/poc/inventory" component={InventoryPage}/>
</Route>
Inside the MasterPageLayout I have my header and my sidemenu (common to all the nested routes above him), the props.children is rendered inside those menus structures, but my header has a specific text for each route. How can I pass the text (and maybe some other data) from the child to the father?
Passing data back up the tree is usually handled with callbacks. As you only need to get the value once I'd recommend using one of the mounting lifecycle methods to call the callback.
As you've tagged react-redux, I'll give examples for both a React and Redux. I don't believe the basic react example is actually suitable for your situation, as you are rendering props.children which makes passing the callback down more difficult, but I'll leave it in the answer in case it's useful to someone else. The redux example should be applicable to your problem.
Basic React
You can pass a callback to the child that sets a value in the components state to use when rendering
class Child extends React.Component {
componentWillMount() {
this.props.setText("for example")
}
render() {
return (
<div>whatever</div>
)
}
}
class Parent extends React.Component {
render() {
return (
<div>
<Child setText={(text) => this.setState({text})} />
{this.state.text}
</div>
)
}
}
React/Redux
You could dispatch an action to set the text when the child is mounted that sets a value in the store to render in the parent, e.g.
class ChildView extends React.Component {
componentWillMount() {
this.props.setText("for example")
}
render() {
return (
<div>whatever</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
setText: (text) => dispatch(setParentText(text))
}
}
const Child = connect(null, mapDispatchToProps)(ChildView)
const ParentView = ({ text }) => {
return (
<div>
<Child />
{text}
</div>
)
}
const mapStateToProps = (state) => {
return {
text: state.parent.text
}
}
const Parent = connect(mapStateToProps)(ParentView)
I wont worry showing the action creator and the reducer/store setup. If you're using redux you should be able to figure that bit out.
This approach will also work if Parent doesn't directly render Child, whether it is through props.children or extra layers are introduced. In fact, Parent doesn't event need to be an ancestor of Child at all for this approach to work, as long as both are rendered on the same page.

Resources