Handle URL Parameters with Gatsby - reactjs

I'm using React & Gatsby to create a website and I've got everything laid out the way I want, I'm just having some problems understanding how to use URL Route parameters to change content that is displayed.
For example, say I have a page categories.js which listens to http://website.com/categories but I want to be able to dynamically handle any URL parameters like such:
http://website.com/categories/animals
When using gatsby-link like so: <Link to="/categories/animals" /> it wants me to create a file called animals.js in a categories folder. Instead, I want categories.js to be able to handle the rendering for this page and select the content appropriate based on the category passed in the URL parameters.
Other than the items displayed, this page is exactly the same through all categories, so it doesn't make sense for every category to have it's own static page.

I think you are wrong when you say:
Other than the items displayed, this page is exactly the same through all categories, so it doesn't make sense for every category to have it's own static page.
In fact, that's exactly where I find GatsbyJS so useful, as it is a static site generator.
It means that you can give Gatsby a template component that will have the same layout for all of your categories, and then Gatsby will fetch data and create static pages for you at build time.
Gatsby is not limited to making pages from files like many static site generators. Gatsby lets you use GraphQL to query your data and map the data to pages—all at build time. (from Gatsby official tutorial)
The idea would be something like this:
in /gatsby-node.js
const path = require(`path`); // you will need it later to point at your template component
exports.createPages = ({ graphql, boundActionCreators }) => {
const { createPage } = boundActionCreators;
// we use a Promise to make sure the data are loaded
// before attempting to create the pages with them
return new Promise((resolve, reject) => {
// fetch your data here, generally with graphQL.
// for example, let say you use your data from Contentful using its associated source plugin
graphql(`
{
allContentfulCategories {
edges {
node {
id
name
description
# etc...
}
}
}
}
`).then(result => {
// first check if there is no errors
if (result.errors) {
// reject Promise if error
reject(result.errors);
}
// if no errors, you can map into the data and create your static pages
result.data.allContentfulCategories.edges.forEach(({ node }) => {
// create page according to the fetched data
createPage({
path: `/categories/${node.name}`, // your url -> /categories/animals
component: path.resolve('./src/templates/categories.js'), // your template component
context: {
// optional,
// data here will be passed as props to the component `this.props.pathContext`,
// as well as to the graphql query as graphql arguments.
id: node.id,
},
});
});
resolve();
});
});
};
And then you can simply fetch the data on your template component
in /src/templates/categories.js
import React from "react";
export default ({ data: { contentfulCategories: category } }) => {
return (
<div>
<h1>{category.name}</h1>
<div>{category.description}</div>
</div>
);
};
// we can query the needed category according to the id passed in the
// context property of createPage() in gatsby-node.js
export const query = graphql`
query CategoryQuery($id: String!) {
contentfulCategories(id: { eq: $id }) {
name
description
}
}
`;
If you insist in dynamically rendering your categories pages, you can take example of this question that is sort of similar to yours, but note that you will have to manually handle re-rendering if the page's props changes in the componentWillReceiveProps lifecycle method of the React class component.
But I really don't think this is a reliable solution.

EDIT:
I've found a better answer after searching forever! -
https://github.com/gatsbyjs/gatsby/issues/13965#issuecomment-617364363
the example for your case I imagine would be something like this:
gatsby-node.js:
createPage({
path: `/categories/:id`,
matchPath: `/categories/:id`,
component: path.resolve(`./src/pages/categories.js`),
})
categories.js:
import React from "react"
export default function Booking({ id }) {
return <div>categories #{id}</div>
}
OLD ANSWER:
I seem to have come across exactly the same issue too and couldn't find any answers. My idea was to also use the gatsby-node.js file, but I don't query anything using graphQL.
My version of /src/templates/categories.js:
const path = require(`path`)
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
createPage({
path: `/categories/animals`,
component: path.resolve(`./src/pages/categories.js`),
// The context is passed as props to the component as well
// as into the component's GraphQL query.
context: {
id: `animals`, //use this context parameter to do the dynamic stuff in your page
},
})
}
I hope this is useful.

Related

Fetching Data from API using NextJS and Material UI React

I am trying to create dynamic pages that shows individual book details (i.e. title/author etc) on a separate page based on a query string of the "id" of each book. However, I am having difficulty in understanding how to make a request to a API endpoint using NextJS that will get the book details based on its "id". I would like to use Material UI as a UI Framework.
ISSUE: When I run npm run dev the book page loads but the book's "props" are not being passed along to the BookAttributes component. The console.log(book) I added in the book page is undefined and the console.log(title) in BookAttributes is undefined as well.
I've tested the API endpoint in POSTMAN and it appears to work.
When I refactor the same code using Semantic UI-React instead of Material UI, the book pages load correctly.
I am using the NextJS Material UI starter template from the Material UI website as a baseline.
I am fairly new to NextJS and Material UI so your assistance and guidance would be greatly appreciated. Thank you for your help on this!
Here is the code I have so. I have tried to keep in clean and simple.
BOOK PAGE (within 'pages' directory)
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
function Book({ book }) {
console.log(book)
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
Book.getInitalProps = async ({ query: { _id } }) => {
const url = 'http://localhost:3000/api/book';
const payload = { params: { _id }}
const response = await axios.get(url, payload)
return { book: response.data }
}
export default Book;
BOOK API ENDPOINT (within 'pages/api' directory)
import Book from '../../models/Book';
import connectDb from '../../utils/connectDb';
connectDb()
export default async (req, res) => {
const { _id } = req.query
const book = await Book.findOne({ _id })
res.status(200).json(book);
}
BOOK ATTRIBUTE COMPONENT (within 'components' directory)
import React from 'react';
function BookAttributes({ title }) {
console.log(title)
return (
<>
<h1>{title}</h1>
</>
)
}
export default BookAttributes;
You should be using dynamic routes here if you want to work with data-fetching methods like getStaticProps or getServerSideProps.
You can create a page like pages/book/[id].js. But to generate the page you have to decide what data-fetching method you want to run. If the data for the page doesn't change very often you can choose to use static-site-generation using getStaticProps which will generate the pages at build time. If the data will be changing a lot you can either do server-side-rendering using getServerSideProps or fetch the data client-side.
Here is an example for your use-case that you can use for server-side-rendering using getServerSideProps, keep in mind the API call inside getServerSideProps might fail so you should have appropriate error handling.
In pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getServerSideProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
Using static-site-generation
Because the page is dynamic you have to provide a list of paths for which nextjs will generate the pages. You can do that by exporting an async function called getStaticPaths.
in pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getStaticPaths = async () => {
// here you have two options if you know all the ids of the books
// you can fetch that data from the api and use all the ids to generate
// a list of paths or show a fallback version of page if you don't know all
// ids and still want the page to be static
// Pseudo code might look like this
const res = await axios.get('api-endpoint-to-fetch-all-the-books')
const paths = res.data.map(book => ({ params: { id: book.id }}))
return {
paths,
fallback: false
}
}
export const getStaticProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
The fallback property in the returned value of getStaticPaths is somewhat important to understand. If you know all the necessary id for the pages you can set the fallback to false. In this case nextjs will simply show a 404 error page for all the paths that were not returned from the function getStaticPaths.
If fallback is set to true nextjs will show a fallback version of page instead of a 404 page for the paths that were not returned from the getStaticPaths function. Now where should you set fallback to true? Let's suppose in your case new books are added to the database frequently, but the data for the books doesn't change very often so you want the pages to be static. In this case, you can set fallback to true and generate a list of paths based on avaliable book ids. For the new books nextjs will first show the fallback version of the page than fetch the data based on the id provided in the request and will send the data as JSON which will be used to render the page in the client.

Dynamic route segment in nextJS static js build

is it possible to use dynamic routing path with nextJS, in a static build context?
Assuming my build is a set of html, js, css assets which could be hosted on any web server (Apache, Nginx, S3, Netlify etc...).
For instance I've got a path defined /pages/[article].js/read and I'd like to be able to use the structure: /page/articleA/read where articleA is a dynamic variable.
If that's feasible, how to achieve this?
Yes, it is possible to use dynamic routes when using static-site-generation with nextjs. You have to use data-fetching method getStaticProps to fetch the data needed based on the dynamic route param. Additionally you have to use another function getStaticPaths to generate a list of paths for which nextjs will build static pages at build time. For example,
Suppose for page /pages/articles/[articleId].js, Here is what pseudo code might look like for you.
// you have to generate and return a list of paths
export const getStaticPaths = async () => {
const articles = await /*Fetch the articles from backend or make a db query*/
const paths = articles.map(article => ({ params: { articleId: article.id }}));
return {
paths,
fallback: false
}
}
export const getStaticProps = async (ctx) => {
const articleId = ctx.params.articleId;
// fetch the data using the article id and return as props
return {
props: /* fetched data */
}
}
// create the page component and export it as the default export
You can read more about it in the docs. Keep in mind because the fallback is set to false nextjs will show a 404 page for any path that is not returned from the function getStaticPaths, You can read about fallback here.

Getting 404 when first loading dynamic routes on nextjs

I'm trying to create a blog page to test nextjs and created a dynamic route for the posts, which will be retrieved from Contentful. When navigating from Home page and clicking into a next/router <Link /> component, the blog post loads correctly, but if I get the URL and try loading the page directly from browser address bar, I'll get 404.
Steps to reproduce:
1. git clone https://github.com/zeit/next-learn-demo.git
2. cd next-learn-demo/8-deploying
3. yarn
4. next build && next export
5. cd out
6. serve
7. Navigate to http://localhost:5000/p/learn-nextjs
8. See 404
Is this a limitation of NextJS (didn't find anything related to it on documentation) or do we need to configure anything else?
The real issue is that exporting a next app will make it generate static HTML files. Even though it will still be able to request data before rendering the page, the set of available paths are not dynamic (they are generated during the next export command). See this docs and this example.
Based on this, I have 2 possible solutions:
generate a webhook to trigger a next build && next export command every time a new blog post is published in Contentful;
avoid exporting my next app and host a Node server that will handle the dynamic routes.
That's because when you directly access the link or refresh the page then it add's a slash at the end of route. An next.js doesn't recognize any route like that. To fix this, I hope there should be an easiest way to do that. However you can do this using custom server. Here is an example:
server.get("/about/", (req, res) => {
return app.render(req, res, "/about")
});
Hope this will help you.
To extend the answer provided by #Minoru, the official Next documentation covers this case in this example.
Using getStaticPaths and getStaticProps allows you to create dynamic pages at build time, avoiding the 404.
Example code for posts dynamic page:
import { GetStaticPaths, GetStaticProps } from 'next';
const Post = ({ post }) => {
const { id, content } = post;
return (
<div>
<h1>Post {id}</h1>
<p>{content}</p>
</div>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
// Get all posts via API, file, etc.
const posts = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }]; // Example
const paths = posts.map(post => ({
params: { id: post.id },
}));
return { paths, fallback: false };
};
export const getStaticProps: GetStaticProps = async context => {
const postId = context.params?.id || '';
// Get post detail via API, file, etc.
const post = { id: postId, content: `I'm the post with id ${postId}!` }; // Example
return { props: { post } };
};
export default Post;
When building the site with next build && next export we will see in the out folder that Next has created each post page
Then, when you navigate to /posts/3/ you will see the post with id 3
For reference, this docs page contains this case and many other use cases.
Don't want to infringe any old posts rules, but in case anyone else in my context I use vercel's feature webhook to new deploys and as I was using firebase I've created a simple firebase function whith is hooked to a new event creation of a page triggers the webhook. I've used fetch because we can make a GET request according to the docs
exports.newEventAdded = functions.region('us-central1').firestore.document('local_events/{localeventId}')
.onCreate((snap, context) => {
fetch('https://api.vercel.com/v1/integrations/deploy/process.env.NEXT_PUBLIC_VERCEL_WEBHOOK_ID')
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(JSON.stringify(myJson));
});
})

Need individual entry with Redux and Router

I'm using ReactJS, Redux (with server-side rendering) and react-router-redux as set up here and am getting a little thrown by how routes work with the rest of the redux state and actions.
For example, I have a members component with the route /members:
class Members extends Component {
static need = [
fetchMembers
]
render() {
...
the static need array specifies an action that populates an array on the state that is then mapped to the component props. That much works.
But then I have an individual member component with the route members/:memberId. How do I load that individual member in a way that works both client- and server-side.
What I'm doing now is the same:
class Member extends Component {
static need = [
fetchMembers
]
render() {
...
but then map just the single member
function mapStateToProps(state, ownProps) {
return {
member: state.member.members.find(member => member.id == ownProps.params.memberId),
};
}
This works but is obviously wrong. So the question is two-fold:
When the user clicks the router Link that has a query param (:memberId), how do I use that router param to query a specific document (assume a mongo database). Do I somehow trigger a separate action that populates an active member field on the redux state? Where does this happen, in the route component's componentDidMount?
How does this work with server-side rendering?
I’ve had the same question and seemed to find a way that works pretty well with my setup. I use Node, Express, React, React Router, Redux and Redux Thunk.
1) It really depends on where your data is. If the data needed for /member/:memberId is already in state (e.g. from an earlier call) you could theoretically filter through what you already have when componentDidMount is fired.
However, I'd prefer to keep things separate simply to avoid headaches. Starting to use one data source for multiple destinations/purposes throughout your app might give you long days down the road (e.g. when Component A needs more/less properties about the member than Component B or when Component A needs properties in a different format than Component B etc.).
This decision should of course be based on your use-case but due to the cost of API calls nowadays I wouldn't be afraid (at all) to make one when someone navigates to /member/:memberId.
2) I’ll answer with a simplified version of my typical setup:
Whenever a request comes through, I have this fella handle it.
// Imports and other jazz up here
app.use((req, res) => {
const store = configureStore({});
const routes = createRoutes(store);
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
const fetchedData = renderProps.components
.filter(component => component.fetchData)
.map(component => component.fetchData(store, renderProps.params));
Promise.all(fetchedData).then(() => {
const body = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
res.status(200).send(`<!doctype html>${renderToStaticMarkup(
<Html
body={body}
state={store.getState()}
/>)
}`);
});
} else {
res.status(404).send('Not found');
}
});
});
It’ll look for fetchData on the components that are about to be rendered, and make sure we have the data before we send anything to the client.
On each and every route, I have a Container. The Container’s sole purpose is to gather the data needed for that route. As you’ve touched upon this can happen server-side (fetchData in my case) or client-side (componentDidMount in my case). A typical Container of mine looks like this:
// Imports up here
class Container extends Component {
static fetchData(store, params) {
const categories = store.dispatch(getCategories());
return Promise.all([categories]);
}
componentDidMount() {
this.props.dispatch(getCategoriesIfNeeded());
}
render() {
return this.props.categories.length ? (
// Render categories
) : null;
}
}
Container.propTypes = {
categories: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired,
params: PropTypes.object.isRequired,
};
function mapStateToProps(state) {
return {
categories: state.categories,
};
}
export default connect(mapStateToProps)(Container);
In the Container above I’m using getCategories and getCategoriesIfNeeded to make sure that I have the data needed for the route. getCategories is only called server-side, and getCategoriesIfNeeded is only called client-side.
Note that I have params available for both fetchData and componentDidMount (passed from connect()), which I could potentially use to extract something like :memberId.
The two functions used to fetch data above are listed below:
// Using this for structure of reducers etc.:
// https://github.com/erikras/ducks-modular-redux
//
// actionTypes object and reducer up here
export function getCategories() {
return (dispatch, getState) => {
dispatch({
type: actionTypes.GET_REQUEST,
});
return fetch('/api/categories').then(res => {
return !res.error ? dispatch({
error: null,
payload: res.body,
type: actionTypes.GET_COMPLETE,
}) : dispatch({
error: res.error,
payload: null,
type: actionTypes.GET_ERROR,
});
});
};
}
export function getCategoriesIfNeeded() {
return (dispatch, getState) => {
return getState().categories.length ? dispatch(getCategories()) : Promise.resolve();
};
}
As displayed above I have both dispatch and getState available thanks to Redux Thunk - that handles my promises too - which gives me freedom use the data I already have, request new data and do multiple updates of my reducer.
I hope this was enough to get you moving. If not don't hesitate to ask for further explanation :)
The answer, it turns out, was pretty simple. The implementation taken from Isomorphic Redux App ties the need static property on a component back to the router by passing the routes query params into the action creator.
So for the route:
items/:id
you'd use a component like
class Item extends Component {
static need = [
fetchItem
]
render() {
specifying that it needs the fetchItem action. That action is passed the route's query params, which you can use like
export function fetchItem({id}) {
let req = ...
return {
type: types.GET_ITEM,
promise: req
};
}
For a more detailed explanation about why this work, read marcfalk's answers, which describes a very similar approach.

React router redux fetch resources if needed on child routes

I'm struggling to make this work, this is a common pattern I think but I haven't been able to see an example for this, or at a solution.
Here is the current route I am working on
/app/services/10/
in app fetch the current user’s login information
in /services fetches the list of services the user has available to them
in /10 fetch fine grained details of Service 10
So the way I do it to populate the store with some data is:
App
import Services from './routes/Services'
export default (store) => ({
path: 'main',
getComponent (nextState, cb) {
require.ensure([], require => {
const App = require('./containers/AppContainer').default,
userActions = require('../store/user').actions
store.dispatch(userActions.fetch())
cb(null, App)
}, 'app')
},
childRoutes: [
Services(store)
]
})
Services
Now the problem lies within the childRoutes:
import { injectReducer } from '../../../../store/reducers'
import Manage from './routes/Manage'
export default (store) => ({
path: 'services',
getComponent (nextState, cb) {
require.ensure([], require => {
const Services = require('./containers/ServicesContainer').default
const actions = require('./modules/services').actions
const reducer = require('./modules/services').default
store.dispatch(actions.fetchAll())
injectReducer(store, { key: 'services', reducer })
cb(null, Services)
})
},
childRoutes: [
Manage(store)
]
})
As you can see the childRoute Services has a fetchAll() async request, that as you can imagine, needed some data from the store, specifically something from the user property in the store, like for example the userId or a token.
There wouldn't be a problem if I naturally navigate. But when I refresh, then the user prop hasn't been populated yet.
If you can't see how this is a problem, as part of my route:
app/services/10
The parameter 10 needed services from the store,
export default (store) => ({
path: ':id',
getComponent ({params: {id}}, cb) {
require.ensure([], require => {
const Manage = require('./containers/ManageContainer').default
const ServicesActions = require('../../modules/integrations').actions
store.dispatch(ServicesActions.selectService(id))
cb(null, Manage)
})
}
})
Where selectService is just a function that filters out state.services
The problem is services is fetched asynchronously and when you refresh that route, the store.dispatch gets executed even before the services in the store has completed and populated the store?
How do I approach this async issue?
TL;DR : Use the lifecycle hook of your component to fetch data when they need it, and conditionally render a "loading" state if the props are not ready. Or use HoC to encapsulate this behavior in a more reusable way.
Your problem is interesting because it's not relevant only for react-router, but for any react / redux application that need data to be fetched before rendering. We all struggled at least once with this issue : "where do I fetch the data ? How do I know if the data are loaded, etc.". That's the problem frameworks like Relay try to address. One very interesting thing about Relay is that you can define some data dependencies for your components in order to let them render only when their data are "valid". Otherwise, a "loading" state is rendered.
We generally achieve a similar result by fetching the needed data in the componentDidMount lifecycle method and conditionally render a spinner if the props are not "valid" yet.
In your specific case, I I understand it correctly, it can be generalized like that :
You hit the page /services/ with react-router
Your ServicesContainer loads all the services
You hit the page /services/10, since the services are already fetched there is no problem
You now decide to refresh but the page is rendered before the async fetching has finished hence your issue.
As suggested by the other answer, you can tackle this issue by fetching the data if needed and not rendering the services until the data are fetched. Something like this :
class Services extends React.Component {
componentDidMount() {
if (!this.props.areServicesFetched) {
this.props.fetchServices()
}
}
render() {
return this.props.areServicesFetched ? (
<ul>
{this.props.services.map(service => <Service key={service.id} {...service}/>)}
</ul>
) : <p>{'Loading...'}</p>
}
}
const ServicesContainer = connect(
(state) => ({
areServicesFetched: areServicesFetched(state) // it's a selector, not shown in this example
services: getServices(state) // it's also a selector returning the services array or an empty array
}),
(dispatch) => ({
fetchServices() {
dispatch(fetchServices()) // let's say fetchServices is the async action that fetch services
}
})
)(Services)
const Service = ({ id, name }) => (
<li>{name}</li>
)
That works great. You can stop reading this answer here if it's enough for you. If your want a better reusable way to do this, continue reading.
In this example, we are introducing some sort of "is my data valid to render or how can I make them valid otherwise ?" logic inside our component. What if we want to share this logic across different components ? As said by the doc :
In an ideal world, most of your components would be stateless functions because in the future we’ll also be able to make performance optimizations specific to these components by avoiding unnecessary checks and memory allocations. This is the recommended pattern, when possible.
What we can understand here is that all our components should be pure, and not taking care of the others component, nor of the data flow (by data flow I mean, "is my data fetched ?", etc.). So let's rewrite our example with only pure components without worrying about data fetching for now :
const Services = ({ services }) => (
<ul>
{services.map(service => <Service key={service.id} {...service}/>)}
</ul>
)
Services.propTypes = {
services: React.PropTypes.arrayOf(React.PropTypes.shape({
id: React.PropTypes.string,
}))
}
const Service = ({ id, name }) => (
<li>{name}</li>
)
Service.propTypes = {
id: React.PropTypes.string,
name: React.PropTypes.string
}
Ok, so far we have our two pure components defining what props they need. That's it. Now, we need to put the "fetching data if needed when component did mount or render a loading state instead" somewhere. It's a perfect role for an Higher-Order Component or HoC.
Briefly speaking, an HoC lets you compose pure components together since they are nothing else than pure functions. An HoC is a function that takes a Component as an argument and return this Component wrapped with another one.
We want to keep separated the displaying of services and the logic to fetch them, because as I said earlier you may need the same logic of fetching the services in an another component. recompose is a little library that implements some very useful HoC for us. We're looking here at
lifecycle to add the componentDidMount lifecycle method
branch to apply a condition whether the services are fetched or not
renderComponent to render some <LoadingComponent> when services are fetching
mapProps to provide only the services prop to our <Services> component.
compose() utility to let us compose our HoC instead of nesting them
So let's build our ensureServices function which is responsible to :
connect the pure component to the redux store
Fetching the services if needed
Rendering a loading state if services are not yet received from the server
Rendering our component when the services are received
Here is an implementation :
const ensureServices = (PureComponent, LoadingComponent) => {
/* below code is taken from recompose doc https://github.com/acdlite/recompose/blob/master/docs/API.md#rendercomponent */
const identity = t => t
// `hasLoaded()` is a function that returns whether or not the component
// has all the props it needs
const spinnerWhileLoading = hasLoaded =>
branch(
hasLoaded,
identity, // Component => Component
renderComponent(LoadingComponent) // <LoadingComponent> is a React component
)
/* end code taken from recompose doc */
return connect(
(state) => ({
areAllServicesFetched: areAllServicesFetched(state), // some selector...
services: getServices(state) //some selector
}),
(dispatch) => ({
fetchServices: dispatch(fetchServices())
})
)(compose(
lifecycle({
componentDidMount() {
if (!this.props.areAllServicesFetched) {
this.props.fetchServices()
}
}
}),
spinnerWhileLoading(props => props.areAllServicesFetched),
mapProps(props => ({ services: props.services }))
)(PureComponent))
}
Now, wherever a component need the services from the store, we can just use it like this :
const Loading = () => <p>Loading...</p>
const ServicesContainer = ensureServices(Services, Loading)
Here, our <Services> component just display the services but if you have for example a <ServicesForm> component that need services to render an input for each services, we could just write something like :
const ServicesFormContainer = ensureServices(ServicesForm, Loading)
If you wan't to generalize this pattern, you could take a look to react-redux-pledge, a tiny library I own that handles this kind of data dependencies.
I've run into this quite a bit on the apps I've worked on. It seems like you're using React Router - if this is the case, you can take advantage of the onEnter/onChange hooks.
API Documentation is here: https://github.com/reactjs/react-router/blob/master/docs/API.md#onenternextstate-replace-callback
Instead of loading data in the async getComponent method, you can use the onEnter hook and use the callback parameter (just like you're doing with the getComponent) to indicate the react-router should block loading of this route until data is loaded.
Something like this could work, if you're using redux-thunk:
export default (store) => ({
path: ':id',
getComponent ({params: {id}}, cb) {
require.ensure([], require => {
const Manage = require('./containers/ManageContainer').default
const ServicesActions = require('../../modules/integrations').actions
cb(null, Manage)
})
},
onEnter: (nextState, replace, cb) => {
const actions = require('./modules/services').actions
const reducer = require('./modules/services').default
//fetch async data
store.dispatch(actions.fetchAll()).then(() => {
//after you've got the data, fire selectService method (assuming it is synchronous)
const ServicesActions = require('../../modules/integrations').actions
store.dispatch(ServicesActions.selectService(id))
cb()//this tells react-router we've loaded all data
})
}
})
I've found the pattern of loading data using the router hooks to be a pretty clean way to ensure all of the data needed for the component to render is there. It's also a great way to intercept unauthenticated users, if necessary.
An alternative approach would be to explicitly load the data in the componentDidMount method of the component.

Resources