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.
Related
I have a MobX store where I have a function doing an API call. It works fine it's getting the data but it doesn't update the already rendered page. I'm following this tutorial https://medium.com/#borisdedejski/next-js-mobx-and-typescript-boilerplate-for-beginners-9e28ac190f7d
My store looks like this
const isServer = typeof window === "undefined";
enableStaticRendering(isServer);
interface SerializedStore {
PageTitle: string;
content: string;
isOpen: boolean;
companiesDto: CompanyDto[],
companyCats: string[]
};
export class AwardStore {
PageTitle: string = 'Client Experience Awards';
companiesDto : CompanyDto[] = [];
companyCats: string[] = [];
loadingInitial: boolean = true
constructor() {
makeAutoObservable(this)
}
hydrate(serializedStore: SerializedStore) {
this.PageTitle = serializedStore.PageTitle != null ? serializedStore.PageTitle : "Client Experience Awards";
this.companyCats = serializedStore.companyCats != null ? serializedStore.companyCats : [];
this.companiesDto = serializedStore.companiesDto != null ? serializedStore.companiesDto : [];
}
changeTitle = (newTitle: string) => {
this.PageTitle = newTitle;
}
loadCompanies = async () => {
this.setLoadingInitial(true);
axios.get<CompanyDto[]>('MyAPICall')
.then((response) => {
runInAction(() => {
this.companiesDto = response.data.sort((a, b) => a.name.localeCompare(b.name));
response.data.map((company : CompanyDto) => {
if (company.categories !== null ) {
company.categories?.forEach(cat => {
this.addNewCateogry(cat)
})
}
})
console.log(this.companyCats);
this.setLoadingInitial(false);
})
})
.catch(errors => {
this.setLoadingInitial(false);
console.log('There was an error getting the data: ' + errors);
})
}
addNewCateogry = (cat : string) => {
this.companyCats.push(cat);
}
setLoadingInitial = (state: boolean) => {
this.loadingInitial = state;
}
}
export async function fetchInitialStoreState() {
// You can do anything to fetch initial store state
return {};
}
I'm trying to call the loadcompanies from the _app.js file. It calls it and I can see in the console.log the companies etc but the state doesn't update and I don't get to see the actual result. Here's the _app.js
class MyApp extends App {
constructor(props) {
super(props);
// Don't call this.setState() here!
this.state = {
awardStore: new AwardStore()
};
this.state.awardStore.loadCompanies();
}
// Fetching serialized(JSON) store state
static async getInitialProps(appContext) {
const appProps = await App.getInitialProps(appContext);
const initialStoreState = await fetchInitialStoreState();
return {
...appProps,
initialStoreState
};
}
// Hydrate serialized state to store
static getDerivedStateFromProps(props, state) {
state.awardStore.hydrate(props.initialStoreState);
return state;
}
render() {
const { Component, pageProps } = this.props;
return (
<Provider awardStore={this.state.awardStore}>
<Component {...pageProps} />
</Provider>
);
}
}
export default MyApp;
In the console.log I can see that this.companyCat is update but nothing is changed in the browser. Any ideas how I can do this? Thank you!
When you do SSR you can't load data through the constructor of the store because:
It's does not handle async stuff, so you can't really wait until the data is loaded
Store is created both on the server side and on the client too, so if theoretically constructor could work with async then it still would not make sense to do it here because it would load data twice, and with SSR you generally want to avoid this kind of situations, you want to load data once and reuse data, that was fetched on the server, on the client.
With Next.js the flow is quite simple:
On the server you load all the data that is needed, in your case it's loaded on the App level, but maybe in the future you might want to have loader for each page to load data more granularly. Overall it does not change the flow though
Once the data is loaded (through getInitialProps method or any other Next.js data fetching methods), you hydrate your stores and render the application on the server side and send html to the client, that's SSR
On the client the app is initialized again, though this time you don't want to load the data, but use the data which server already fetched and used. This data is provided through props to your page component (or in this case App component). So you grab the data and just hydrate the store (in this case it's done with getDerivedStateFromProps).
Based on that, everything you want to fetch should happen inside getInitialProps. And you already have fetchInitialStoreState method for that, so all you need to do is remove data fetching from store constructor and move it to fetchInitialStoreState and only return the data from it. This data will then go to the hydrate method of your store.
I've made a quick reproduction of your code here:
The huge downside if App.getInitialProps is that it runs on every page navigation, which is probably not what you want to do. I've added console.log("api call") and you can see in the console that it is logged every time you navigate to any other page, so the api will be called every time too, but you already have the data so it's kinda useless. So I recommend in the future to use more granular way of loading data, for example with Next.js getServerSideProps function instead (docs).
But the general flow won't change much anyway!
Calling awardStore.loadCompanies in the constructor of MyApp is problematic because the loadCompanies method is populating the store class. What you want is to hydrate the store with the companyCats data. Since server and client stores are distinct, you want to load the data you need on the server side i.e. fetchInitialStoreState (or load it from a page's getStaticProps/getServerSideProps method) so that you can pass it into the hydrate store method from page/app props.
Note loadCompanies is async so it'll be [] when getDerivedStateFromProps is called so there's nothing to hydrate. For your existing hydrate method to work you need initialStoreState to be something like the fetchInitialStoreState method below. Alternatively if it's fetched on the page level, the hydrate may be closer to initialData?.pageProps?.companyCats
It's common to see the store hydration as needed for each page though it's still valid to call loadCompanies() from the client side. There's a lot I didn't get a chance to touch on but hopefully this was somewhat helpful.
export const fetchInitialStoreState = async() => {
let companyCats = [];
try {
const response = await axios.get < CompanyDto[] > ('MyAPICall')
response.data.map((company: CompanyDto) => {
if (Array.isArray(company.categories) && company.categories.length > 0) {
companyCats.push(...company.categories)
}
})
} catch (error) {
// Uh oh...
}
return {
serializedStore: {
companyCats,
// PageTitle/etc
}
}
}
I have a react component, let's call it Documents. In this component I need to load multiple pieces of dependent data using fetch. I am using redux thunk for these async redux actions that perform data fetching.
So the component looks like this:
interface Props {
comments: Entity[];
documents: Entity[];
users: Entity[];
getComments: Function;
getDocuments: Function;
getUsers: Function;
}
export class Documents<Props> {
public async componentDidMount(props: Props){
// is this the right lifecycle method for this since
// it does not really need to change state
const { documents, users, comments, getDocuments, getUsers, getComments} = props;
if(!documents || !documents.length){
await getDocuments();
} else {
await getUsers(documents.map(d => d.username));
}
getComments(documents, users); // dependent upon both users and documents
}
public render() {
// render documents, users, and comments
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Documents);
Here I need to look at the documents before I can load users. And Both users and documents before I can load comments. What's the best approach for handling this in react. My render function will load small sub-components responsible for rendering these data but I don't want them to be connected components.
I think I was able to accomplish it with a functional component/useEffect hooks:
https://codesandbox.io/s/festive-varahamihira-kpssd?file=/src/Documents.js
useEffect(() => {
if (!documents || !documents.length) {
getDocuments();
} else {
getUsers(documents);
}
}, [documents]);
useEffect(() => {
if (users) {
getComments(documents, users);
}
}, [users]);
After some researches, I found some questions on stackoverflow about what I am trying to achieve, however, I don't feel that these questions and their answers gives me the "answers" or the "directions" i am looking for..
Note: I am pretty new to react even if I already made 2 projects and implemented redux into one of them. However, I ain't new at all in C# or in Go, even less in C. Based on my experience, I am just used to some architectures and I would like to reproduce one of them.
Here is a pretyy good schema from a similar question of mine:
Situation:
So let say I have pages that contains Components. I want these pages/compoments to display some stuff. One of my functionnality is to discover a map and for that, when the client moves, he gets new parts from my API. However, I don't wanna ask the server to give me the new parts and the ones I discovered already.
My idea about it would be to use a service MapService.js. This one would just store the discovered pieces of the map discovered and ask the server automatically about the new ones, and of course, store the new ones (concat).
However, I have to be logged for this, so I would like an ApiService.js that would store my authentication data and automatically put them in each of my requests.
Based on what I said, we would have something as:
Page -> Component -> Service -> API
From this, the API response would be gotten by my service, handled, then returned to the component. Handled means (data added to the previous then all returned)
I saw on internet one question that was referring "MVCS" (Model View Controller Service) pattern and I think I am looking for something as but I am not sure about how to implement it in ReactJs.
Redux seems to be something that you put all around and everywhere in your solution. What I would like is to use it as a "repository" let say, to be able to manage it from a service and not from the component itself. However, a service should be a single instance shared across the app and I don't know if something such as dependency injection could be the solution in ReactJS
Feel free to ask any edit if you need more details :)
Thanks for your help !
Here is a minimal example of Redux middleware usage. Usually, redux devs are using libraries (that give you a middleware) to have access to more appropriate APIs.
Redux middleware are chained, so each middleware can call the next middleware. The first middleware of the chain is called every time dispatch function (you can have it from react-redux connect) is called. In a middleware, if there is no next middleware it is the reducers that will be called. The next middleware can be call asynchronously after receiving an action. (Redux docs will still be better than my explainations).
In my example there is a catService that provide function that call rest API. Your services can be anything (a Class instance or a singleton for example). Usually in React/Redux stack, devs don't use object oriented development.
If a component dispatch getCat(123), the catMiddleware will be called (synchronously). Then requestGetCat will be called with the id 123. When the promise returned by requestGetCat will be resolved a setCat action will be send through the reducers to update the redux state. Once the redux state is done, the component listening for cats items object will be update too (triggering a rerender).
That can look very complexe, but in fact, it is very scalable and convenient.
// catService.js
// return a promise that return a cat object
const requestGetCat = id =>
fetch(`www.catcat.com/api/cat/${id}`)
.then(response => response.json())
// catTypes.js
export const GET_CAT = 'GET_CAT'
export const SET_CAT = 'SET_CAT'
// catActions.js
export const getCat = id => ({
type: GET_CAT,
id
})
export const setCat = (cat, id) => ({
type: SET_CAT,
id,
cat
})
// catReducer.js
const initialState = {
items: {}
}
const catReducer = (state = initialState, action) => {
if (action.type === SET_CAT) {
return {
items: {
...state.items,
[action.id]: action.cat
}
}
}
}
// catMiddleware.js
const handleGetCat = (next, action) => {
requestGetCat(action.id)
.then(cat => next(setCat(cat, action.id)))
// after retrieving the cat send an action to the reducers (or next middleware if it exist)
}
const actionHandlers = {
[GET_CAT]: handleGetCat
}
// receive every actions passing by redux (if not blocked)
// store: { dispatch, getState }
// next: next middleware or reducers (that set redux state)
// action: a redux action (dispatched) with at least type property
const catMiddleware = store => next => action => {
const handler = actionHandlers[action.type]
if (handler) {
handler(next, action)
} else {
// passing the action to the next middleware (or reducer - when there is no next middleware)
next(action)
}
}
// you have to apply your middleware
// and your reducer (see redux doc)
This one would just store the discovered pieces of the map discovered and ask the server automatically about the new ones, and of course, store the new ones
This is something I've wanted to do in the past, but never implemented a solution for.
The issue is that you essentially want to "cross the streams"..
In Redux there are two separate streams, ie dispatch an action to update the store, and read data from the store. Each of these are executed separately from a component. Combined, they can be used in a cycle by calling an action to load data into the store which triggers an update of the component which then reads from the store.
Basically you can't have non-component code that reads from the store, and if the data is missing, fires an action to load the data, then returns the data.
Thinking about it now, I'm wondering if the way to do this without adding logic to your view component is to wrap it in a component (HOC) that provides the logic.
The HOC will check the state for the location specified in the props. If it doesn't find it, it will dispatch an action to fetch it and render a loading display. When the state is updated with the new location it will update and render the wrapped component.
You could optionally always render the wrapped component and have it cope with the missing location until it is updated with the location set..
untested brain-dump below
loader HOC:
import React, { useEffect } from "react";
import actions from "./actions";
function withLocationLoader(Component) {
const Wrapper = function ({ location, locations, loadLocation, ...props }) {
useEffect(() => {
if (!locations[location]) {
loadLocation(location);
}
}, [locations]);
if (locations[location]) {
return <Component locations={locations} {...props} />;
}
return <div>Loading...</div>;
}
const mapStateToProps = (state, ownProps) => {
return { locations: state.locations };
};
const mapActionsToProps = {
loadLocation: actions.loadLocation,
};
return connect(
mapStateToProps,
mapActionsToProps
)(Wrapper);
}
export { withLoader };
component:
function MyBareComponent({ locations }) {
return <div>{JSON.stringify(locations)}</div>;
}
const MyComponent = withLocationLoader(MyBareComponent);
export { MyComponent };
actions: (utilising redux-thunk middleware)
function setLocation(location, data) {
return { type: "SET_LOCATION", payload: { location, data } };
}
export function loadLocation(location) {
return dispatch =>
Promise.resolve({ geoData: "" }) // mock api request
.then(data => dispatch(setLocation(location, data)));
}
I'm building a 'Hacker News' clone, Live Example using React/Redux and can't get this final piece of functionality to work. I have my entire App.js wrapped in BrowserRouter, and I have withRouter imported into my components using window.history. I'm pushing my state into window.history.pushState(getState(), null, `/${getState().searchResponse.params}`) in my API call action creator. console.log(window.history.state) shows my entire application state in the console, so it's pushing in just fine. I guess. In my main component that renders the posts, I have
componentDidMount() {
window.onpopstate = function(event) {
window.history.go(event.state);
};
}
....I also tried window.history.back() and that didn't work
what happens when I press the back button is, the URL bar updates with the correct previous URL, but after a second, the page reloads to the main index URL(homepage). Anyone know how to fix this? I can't find any real documentation(or any other questions that are general and not specific to the OP's particular problem) that makes any sense for React/Redux and where to put the onpopstate or what to do insde of the onpopstate to get this to work correctly.
EDIT: Added more code below
Action Creator:
export const searchQuery = () => async (dispatch, getState) => {
(...)
if (noquery && sort === "date") {
// DATE WITH NO QUERY
const response = await algoliaSearch.get(
`/search_by_date?tags=story&numericFilters=created_at_i>${filter}&page=${page}`
);
dispatch({ type: "FETCH_POSTS", payload: response.data });
}
(...)
window.history.pushState(
getState(),
null,
`/${getState().searchResponse.params}`
);
console.log(window.history.state);
};
^^^ This logs all of my Redux state correctly to the console through window.history.state so I assume I'm implementing window.history.pushState() correctly.
PostList Component:
class PostList extends React.Component {
componentDidMount() {
window.onpopstate = () => {
window.history.back();
};
}
(...)
}
I tried changing window.history.back() to this.props.history.goBack() and didn't work. Does my code make sense? Am I fundamentally misunderstanding the History API?
withRouter HOC gives you history as a prop inside your component, so you don't use the one provided by the window.
You should be able to access the window.history even without using withRouter.
so it should be something like:
const { history } = this.props;
history.push() or history.goBack()
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.