I'm building a Next.js app and it currently is using Redux. As I am building it I am wondering if the use of Redux is really necessary and if its use is actually an anti-pattern. Here is my reasoning:
In order to properly initialize the Redux Store in Next.js, you must create a custom App component with a getInitialProps method. By doing this you are disabling the Automatic Static Optimization that Next.js provides.
By contrast, if I were to include Redux on the client-side, only after the App has mounted, then the Redux store will reset after every server-side navigation. For instance, I have a Next.js app that initializes the Redux store on the client-side, but when routing to a dynamic route such as pages/projects/[id], the page is server-side rendered, and I have to re-fetch any information that was in the store.
My questions are:
What are the benefits of a Redux store in this circumstance?
Should I initialize the store in the root App component and forego the Automatic Static Optimization?
Is there a better way to do to manage state in Next.js 9.3 with getStaticProps and the other data fetching methods
Am I missing something?
If you have a custom App with getInitialProps then the Automatic
Static Optimization that Next.js provides will be disabled for all
pages.
True, if you follow this approach.
Is there a better way ?
Yes, you can create a Redux Provider as a wrapper and wrap the component you need, the redux context will be automatically initialized and provided within that component.
Example:
const IndexPage = () => {
// Implementation
const dispatch = useDispatch()
// ...
// ...
return <Something />;
}
IndexPage.getInitialProps = ({ reduxStore }) => {
// Implementation
const { dispatch } = reduxStore;
// ...
// ...
}
export default withRedux(IndexPage)
You have now the possibility to use Redux only for the pages which need state management without disabling the optimization for the entire App.
Answering you question "Is using Redux with Next.js an anti-pattern?"
No, but it needs to be used properly.
More info on how is done here: https://github.com/vercel/next.js/tree/canary/examples/with-redux
I hope this helps
we use Redux mainly for 2 reasons.
1- pass data between components.
if you do not use redux, then you need to do prop drilling. To decide if user logged in or not, we fetch the data and then store it in redux store and then Header components connects to the store and gets the authentication info. If you are not using redux, then you need to fetch the user in each page and then pass it to the Header component.
Next.js pre-renders every page. This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering can result in better performance and SEO. next-redux-wrapper package allows you to use the redux with automatic-static-optimization. If you click on the link, there is a note saying: "Next.js provides generic getInitialProps when using class MyApp extends App which will be picked up by wrapper, so you must not extend App as you'll be opted out of Automatic Static Optimization:". I set up this package for my project and it is easy to setup.
But downside of using redux, it is not caching. You store the data and then you refetch it periodically to make sure it is up to date. and this is an extra expensive work. To achieve caching in redux, we use reselect library. This means extra dependency for your project on top of redux and will make you write more code.
There is a nice package swr which is created by next.js. Stale-While-Revalidate. it first returns the data from cache(stale), then sends the fetch request, and finally comes with the updated data again. I choose the use this in each page.
import useSWR from "swr";
export const useGetUser = () => {
// fetcher can be any asynchronous function which returns the data. useSwr will pass "/api/v1/me" to fetcher
const { data, error, ...rest } = useSWR("/api/v1/me", fetcher);
// !data && !error if both true, loading:true, data=null=>!data=true, error=null => !error=true
return { data, error, loading: !data && !error, ...rest };
};
here is resuable fetcher
export const fetcher = (url: string) =>
fetch(url).then(
async (res: Response): Promise<any> => {
const result = await res.json();
if (res.status !== 200) {
return Promise.reject(result);
} else {
return result;
}
}
);
2- Making api requests.
I set up redux store for my project and it was conflicting with the text-editor that I set up. Redux was somehow blocking the editor and i could not populate the store with the text that i wrote on the editor. So I used reusable hooks for fetching api. it looks intimating in the beginning but if you analyze it, it will make sense.
export function useApiHandler(apiCall) {
// fetching might have one those 3 states. you get error, you fetch the data, and you start with the loading state
const [reqState, setReqState] = useState({
error:null,
data:null,
loading:true, // initially we are loading
});
const handler = async (...data) => {
setReqState({ error: null, data: null, loading: true });
try {
// apiCall is a separate function to fetch the data
const res = await apiCall(...data);
setReqState({ error: null, data: res.data, loading: false });
alert(res.data);// just to check it
return res.data;
} catch (e) {
// short circuting in or. if first expression is true, we dont evaluate the second.
// short circuting in and. if first expression is true, result is the second expression
const message =
(e.response && e.response.data) || "Ooops, something went wrong...";
setReqState({ error: message, data: null, loading: false });
return Promise.reject(message);
}
};
return [handler, { ...reqState }];
}
A simple apiCall function
const createBlog = (data) => axios.post("/api/v1/blogs", data);
and then this is how we use it :
export const useCreateBlog = () => useApiHandler(createBlog);
Setting redux is easy since it is easy people are not worried about the performance of their app, they just set it up. In my opinion, if you have a large app you need to set up redux or if you are familiar with graphql you can use Apollo. Here is a good article to get an idea about using apollo as state management. apollo as state management. I built a large ecommerce website and I used redux, my in my new app, since it is relatively small I do not use next js and make it more complicated.
Redux Toolkit Query
I think redux toolkit query (RTK query) is the biggest improvement in the redux ecosystem. It is actually built on top of redux-toolkit library. redux-toolkit helped us to write our redux code much simpler and update the state easier by using immer.js behind the scene.
With "RTK Query" we can handle data fetching and state management together. All the data fetching is combined under one API and we can cache the data, invalidate the cache or refetch the query. It is actually doing what the combination of swr and context Api is doing. state management with swr and context api
If you are using Redux, you do not need to have getInitialProps on _app.js.
You can use next-redux-wrapper, and just wrap _app.js export with it.
Store example, with next-redux-wrapper and thunk:
import { createStore, applyMiddleware } from 'redux';
import { createWrapper } from 'next-redux-wrapper';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunkMiddleware from 'redux-thunk';
import rootReducer from './rootReducer';
const bindMiddleware = middleware => {
return composeWithDevTools(applyMiddleware(...middleware));
};
const initStore = (initialState = {}) => {
return createStore(rootReducer, initialState, bindMiddleware([thunkMiddleware]));
};
export const wrapper = createWrapper(initStore, { debug: true });
Then inside your _app.js, you are exporting it as functional component with
const App = ({ Component, pageProps }) => {
return (
<Component {...pageProps} />
)
}
export default wrapper.withRedux(App);
Works like a charm. Just make sure you are doing hydration ssr -> csr.
Personally I think using the Redux is not a good idea at any case. It would be better to use, for example, useContext, or in case of extreme need for centralized storage look towards mobx. But in fact, there is a simple way to use Redux with SSR without using getInitialProps.
There is an important point here - the solution I gave is applicable only if you DO NOT use the rendering of literally every page on the server - when following the route after the first render, the application renders the next page on its own. In this solution it is assumed that the store will be initialized on the server side once and then the rendering result will be transferred to the client. If you need to render the page on the server absolutely every time you navigate the route and you need to save the state of store, then perhaps you really better still look towards the next-redux-wrapper.
So to initialize store at getServerSideProps first you will need to change your storage initialization file as follows (perhaps you will have other imports):
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
let storeInstance: any;
export const makeStore = (initialState: {}) => {
storeInstance = createStore(
Reducers,
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware)) // Optional, but is a handy thing
);
return storeInstance;
};
// initializeStore used for pages that need access to store at getServerSideProps
export const initializeStore = (preloadedState) => {
let reInitiatedStore = storeInstance ?? makeStore(preloadedState)
// After navigating to a page with an initial Redux state, merge that state
// with the current state in the store, and create a new store
if (preloadedState && storeInstance) {
reInitiatedStore = makeStore({ ...storeInstance.getState(), ...preloadedState});
// Reset the current store
storeInstance = undefined;
}
// Keep in mind that in some cases this can cause strange
// and difficult to track errors, so whether or not
// to uncomment next lines depends on the architecture of your application.
// if (typeof(window) === 'undefined') {
// return reInitiatedStore; // For SSG and SSR always create a new store
// }
// Create the store once in the client
if (!storeInstance) {
storeInstance = reInitiatedStore;
}
return reInitiatedStore;
}
After that, in the page, where you need store on server side in the getServerSideProps, you can simple use initializeStore:
import { initializeStore } from '#Redux';
// Compnent code here...
export const getServerSideProps(context: any) {
const reduxStore = initializeStore();
// reduxStore = {
// dispatch: [Function (anonymous)],
// subscribe: [Function: subscribe],
// getState: [Function: getState],
// }
// Doing something with the storage...
const initialReduxState = storeInstance.getState(); // and get it state
return { props: { initialReduxState, ...someProps } };
}
Also don't forget that if you need to access the store in your _app.js, you must define store as:
const store = initializeStore(pageProps.initialReduxState);
Next.js is just a framework on top of React which simplifies Server Side Rendering setup, but it is still React. And React/Redux combo is very popular and still often used, also by me, so the answer is - it is not necessary, but totally possible! The bigger the app and the more you like functional programming, the better chance Redux will be a good option!
Related
I want to put the authenticated user in a zustand store. I get the authenticated user using react-query and that causes some problems. I'm not sure why I'm doing this. I want everything related to authentication can be accessed in a hook, so I thought zustand was a good choice.
This is the hook that fetches auth user:
const getAuthUser = async () => {
const { data } = await axios.get<AuthUserResponse>(`/auth/me`, {
withCredentials: true,
});
return data.user;
};
export const useAuthUserQuery = () => {
return useQuery("auth-user", getAuthUser);
};
And I want to put auth user in this store:
export const useAuthStore = create(() => ({
authUser: useAuthUserQuery(),
}));
This is the error that I get:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons.
you can read about it in the react documentation:
https://reactjs.org/warnings/invalid-hook-call-warning.html
(I changed the name of some functions in this post for the sake of understandability. useMeQuery = useAuthUserQuery)
I understand the error but I don't know how to fix it.
The misunderstanding here is that you don’t need to put data from react query into any other state management solution. React query is in itself a global state manager. You can just do:
const { data } = useAuthUserQuery()
in every component that needs the data. React query will automatically try to keep your data updated with background refetches. If you don’t need that for your resource, consider setting a staleTime.
—-
That being said, if you really want to put data from react-query into zustand, create a setter in zustand and call it in the onSuccess callback of the query:
useQuery(key, queryFn, { onSuccess: data => setToZustand(data) })
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 am using Next.js and would like to prefetch an API item using redux. Here's some code that almost works:
class Thing extends Component {
static getInitialProps ({ store }) {
store.dispatch(fetchProduct())
}
}
const mapStateToProps = (state, ownProps) => {
return {
product: getProduct()
}
}
I am running into the issue that the component renders before getProduct has info. How can I shape things so that I don't render until dispatch has fetched the item? I am using redux-api-middleware in case that matters. I can check for the existence/validity of 'product' in render but that sort of defeats the purpose of getInitialProps. Is there a way to achieve the equivalent of an async/await fetch with redux?
Update Ok this might not work. redux-api-middlewar seems to return an empty object as an action on SSR - so its not a timing issue.
Ok. Reading up some GH issues I found that using redux-api-middleware in SSR is somewhat problematic. I believe that part of the issue is that it doesn't appear to return a promise from dispatch that can be used with async/await. Additionally one should ideally use getInitialProps for its intended purpose so let's return the props there directly.
I've switched to redux-axios-middleware and it works like a charm.
static async getInitialProps ({ store }) {
await store.dispatch(fetchProducts())
return { products: getProducts(store.getState()) }
}
If you are using 'next-redux-wrapper' you must accept initialState argument in makeStore and pass it down to createStore so that after dispatching the store the new state will be accesible in the server and client side in the object methods.
const makestore = initialState => {
return createStore(reducer, initialState);
};
Basic setup:
1) App is React and Redux,
2) App is served by a front facing NGINX serving static files like html, pictures and of course the app itself. It also forwards all relevant requests (web sockets and/or AJAX) to the back end (phoenix/elixir).
3) Users are required to authenticate. I'm using redux-oidc library, which is client side only and it works fine.
4) After user logs on is when I get hazy on what to do next.
Question(s):
1) I can't send the state along with the first request because I don't know who the user is and, thus, don't know which state to send. Meanwhile application is already booted (empty store created, login component displayed),
2) After user logs on I can't show anything (like user specific nav bar, timeline , mailbox) I have to saturate the store and let react do its job. What approach should I take?
3) Server rendering is out because a) I'm not using Node and rendering react components using chosen framework is messy and complicated at best and b) I won't be able to export the app to NGinx since it only serves static assets and there is no server logic run there. I could, theoretically, get rid of NGinx, have server based login on the API server and send down HTML along with JSON state which could be used to render the app on the client. However, NGinx does not only serve static assets but also load balances few instances and, thus, getting rid of it is not something I want to do.
Any advice would be appreciated.
Hydrating the state after the store was created, can be achieved by creating a main reducer that can bypass the top level reducers, and replace the whole state.
Reducers are functions that get the current state, combine it with the payload of an action, and return a new state. Usually the main reducer is a combination of all top reducers using combineReducers, and the state is the combination of state pieces returned by the top level reducers.
However, the main reducer can react to actions directly. If the main reducer receives a certain action (hydrate), instead of calling the combined reducers, it returns the action's payload (the saved state). Other actions are passed to the combined reducers.
const mainReducer = (state = {}, action) =>
action.type === 'hydrate' ?
action.payload // hydrate the state
:
reducers(state, action); // create new state by using combined reducers
Working example:
const { combineReducers, createStore } = Redux;
const people = (state = [], action) => action.type === 'people' ? [...state, action.payload] : state;
const items = (state = [], action) => action.type === 'items' ? [...state, action.payload] : state;
const reducers = combineReducers({
people,
items
});
const mainReducer = (state = {}, action) => action.type === 'hydrate' ? action.payload : reducers(state, action);
const store = createStore(mainReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'people', payload: 5 });
store.dispatch({ type: 'items', payload: 'green' });
store.dispatch({ type: 'hydrate', payload: {
people: [20, 30, 50, 100],
items: ['green', 'yellow', 'red']
}});
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.min.js"></script>
Even though the accepted answer might do the trick, I think that what you are chasing is an anti-pattern, after all.
If you are doing client rendering and handling authentication client side, the only thing you should send to your client, in my opinion, is a <Spinner/>, with zero preloaded state.
Once you are on the client, initialise your store, do your authentication, and decide if you are going to fetch data and render the authenticated version of the page, or if user is not authenticated yet, show them the login form. Everything from this point on, should be handled client side.
Here is what Redux has to say about it:
https://redux.js.org/usage/server-rendering
So, if during the initial render you got "Schrödinger" user (might be authenticated or not), the only thing you can safely assume to show is a spinner.
ANOTHER OPTION
If you really need to get new preloaded data from the server on multiple requests (that's kind of what happens in NextJS apps).
If you are going to pre-render every page (and get new state that on every page change), you can do what NextJS suggests you to do:
They basically re-initialise the store and merge the new data every time it receives new preloaded data from the server.
Here is the example:
https://github.com/vercel/next.js/tree/canary/examples/with-redux-thunk
And the main part of their code:
import { useMemo } from 'react'
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import reducers from './reducers'
let store
function initStore(initialState) {
return createStore(
reducers,
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
)
}
export const initializeStore = (preloadedState) => {
let _store = store ?? initStore(preloadedState)
// After navigating to a page with an initial Redux state, merge that state
// with the current state in the store, and create a new store
if (preloadedState && store) {
_store = initStore({
...store.getState(),
...preloadedState,
})
// Reset the current store
store = undefined
}
// For SSG and SSR always create a new store
if (typeof window === 'undefined') return _store
// Create the store once in the client
if (!store) store = _store
return _store
}
export function useStore(initialState) {
const store = useMemo(() => initializeStore(initialState), [initialState])
return store
}
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.