How to preload data with react-router v4? - reactjs

This is example from official docs (https://reacttraining.com/react-router/web/guides/server-rendering/data-loading):
import { matchPath } from 'react-router-dom'
// inside a request
const promises = []
// use `some` to imitate `<Switch>` behavior of selecting only
// the first to match
routes.some(route => {
// use `matchPath` here
const match = matchPath(req.url, route)
if (match)
promises.push(route.loadData(match))
return match
})
Promise.all(promises).then(data => {
// do something w/ the data so the client
// can access it then render the app
})
This documentation makes me very nervous. This code doesn't work. And this aproach doesn't work! How can I preload data in server?

This Is what I have done - which is something I came up with from the docs.
routes.cfg.js
First setup the routes config in a way that can be used for the client-side app and exported to used on the server too.
export const getRoutesConfig = () => [
{
name: 'homepage',
exact: true,
path: '/',
component: Dasboard
},
{
name: 'game',
path: '/game/',
component: Game
}
];
...
// loop through config to create <Routes>
Server
Setup the server routes to consume the config above and inspect components that have a property called needs (call this what you like, maybe ssrData or whatever).
// function to setup getting data based on routes + url being hit
async function getRouteData(routesArray, url, dispatch) {
const needs = [];
routesArray
.filter((route) => route.component.needs)
.forEach((route) => {
const match = matchPath(url, { path: route.path, exact: true, strict: false });
if (match) {
route.component.needs.forEach((need) => {
const result = need(match.params);
needs.push(dispatch(result));
});
}
});
return Promise.all(needs);
}
....
// call above function from within server using req / ctx object
const store = configureStore();
const routesArray = getRoutesConfig();
await getRouteData(routesArray, ctx.request.url, store.dispatch);
const initialState = store.getState();
container.js/component.jsx
Setup the data fetching for the component. Ensure you add the needs array as a property.
import { connect } from 'react-redux';
import Dashboard from '../../components/Dashboard/Dashboard';
import { fetchCreditReport } from '../../actions';
function mapStateToProps(state) {
return { ...state.creditReport };
}
const WrappedComponent = connect(
mapStateToProps,
{ fetchCreditReport }
)(Dashboard);
WrappedComponent.needs = [fetchCreditReport];
export default WrappedComponent;
Just a note, this method works for components hooked into a matching routes, not nested components. But for me this has always been fine. The component at route level does the data fetch, then the components that need it later either has it passed to them or you add a connector to get the data direct from the store.

Related

How to render [slug].js page after fetching data Next.js

I am trying to create a logic for my blog/:post page in Next.js but I cannot seem to figure out how.
The idea is to:
Fetch the url (using useRouter)
Call API (it is a headless CMS) to get the info of the post
Render the post
What I have right now is:
[other imports ...]
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
function BlogPost(props) {
const router = useRouter()
const { slug } = router.query
const [blogPost, setBlogPost] = useState({})
// Function to the blog post
function fetchBlogPost() {
butter.post.retrieve(slug)
.then(response => {
const blogPostData = response.data.data
setBlogPost(blogPostData)
})
}
useEffect(() => {
// We need to add this if condition because the router wont grab the query in the first render
if(!router.isReady) return;
fetchBlogPost()
}, [router.isReady])
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
But this is not rendering everything (the image is not being rendered for example). I believe it is because of the pre-render functionality that Next.js has. Also I have been reading about the getStaticProps and getStaticPaths but I am unsure on how to use them properly.
Any guidance will be welcome. Thanks!
If you're using next.js then you are on track with getStaticProps being your friend here!
Essentially getStaticProps allows you to take advantage of ISR to fetch data on the server and create a static file of your page with all of the content returned from the fetch.
To do this you'll need to make an adjustment to your current architecture which will mean that instead of the slug coming in from a query param it will be a path parameter like this: /blogs/:slug
Also this file will need to be called [slug].js and live in (most likely) a blogs directory in your pages folder.
Then the file will look something like this:
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
export const getStaticPaths = async () => {
try {
// You can query for all blog posts here to build out the cached files during application build
return {
paths:[], // this would be all of the paths returned from your query above
fallback: true, // allows the component to render with a fallback (loading) state while the app creates a static file if there isn't one available.
}
} catch (err) {
return {
paths: [],
fallback: false,
}
}
}
export const getStaticProps = async ctx => {
try {
const { slug } = ctx.params || {}
const response = await butter.post.retrieve(slug)
if(!response.data?.data) throw new Error('No post data found') // This will cause a 404 for this slug
return {
notFound: false,
props: {
postData: response.data.data,
slug,
},
revalidate: 5, // determines how long till the cached static file is invalidated.
}
} catch (err) {
return {
notFound: true,
revalidate: 5,
}
}
}
function BlogPost(props) {
const {isFallback} = useRouter() // We can render a loading state while the server creates a new page (or returns a 404).
const {postData} = props
// NOTE: postData might be undefined if isFallback is true
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
In any case, though if you decide to continue with rendering on the client instead then you might want to consider moving your fetch logic inside of the useEffect.

How to share redux state client-side and props server-side in Next JS

I'm a newbie with Next JS.
I use Next JS and Redux.
I have a short code below:
const AdminContainer = (props) => {
return (
<AdminMasterView>
<DashboardView studentList={props.studentListServer}/>
</AdminMasterView>
)
}
export const getStaticProps = (async () => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList
});
const mapDispatchToProps = {
getStudentRegisterAction
};
export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer);
I also have studentList (array type) props is declare in Redux. I want to use it to pass data because I have many tasks to do with data such as filter, order,...
Is there any way to use studentList like this and my app still is server rendering first time.
If I dispatch studentListServer to studentList, it still work. But my app isn't server rendering.
<DashboardView studentList={props.studentList}/>
Or easier, I'll check to use props.studentList for client-side and props.studentListServer for server-side. But I think it's not good.
Thank you so much!
You could use the next-redux-wrapper package. It allows to sync a Redux state on server and client. Consider the example:
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
// dispatch the action that saves the data
store.dispatch({ type: 'SET_STUDENTS', payload: response });
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
wrapper.getStaticProps wraps your getStaticProps function with the new parameter store that is a Redux store in fact.
Action with type SET_STUDENTS sets the student list on a server side. When Next.js generates the page, it will save this data in static JSON. So when the page opens on client side, next-redux-wrapper recreates a state dispatching HYDRATE action with saved on a build time static JSON that you can use to restore the studentInfoReducers reducer.
E.g. in your reducer you should implement something like:
import { HYDRATE } from 'next-redux-wrapper';
const initialState = { studentList: [] };
// studentInfoReducers reducer
function reducer(state = initialState, action) {
// this sets your student list
if (action.type === 'SET_STUDENTS') {
return {
...state,
studentList: action.payload,
};
}
// this rehydrates your store from server on a client
if (action.type === HYDRATE) {
return action.payload.studentInfoReducers;
}
return state;
}
So afterwards you should have a valid synced state on client and server at the same time:
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList // works on server and client
});
Let me know if you have any questions, next-redux-wrapper can be tricky from a first look.
You don't need to use Redux for that.
Using just cookies you can achieve bidirectional communication, see https://maxschmitt.me/posts/next-js-cookies/
Another example:
Client to Server: manually set a cookie in the client side and then read it in the server with req.headers.cookie or some library like 'cookie'
Server to Client: just read the cookie, and return what you need as a regular prop or update the cookie.
import { useState, useEffect } from "react";
import Cookie from "js-cookie";
import { parseCookies } from "../lib/parseCookies";
const Index = ({ initialRememberValue = true }) => {
const [rememberMe, setRememberMe] = useState(() =>
JSON.parse(initialRememberValue)
);
useEffect(() => {
//save/create the cookie with the value in the client
Cookie.set("rememberMe", JSON.stringify(rememberMe));
}, [rememberMe]);
return (
<div>
remember me
<input
type="checkbox"
value={rememberMe}
checked={rememberMe}
onChange={e => setRememberMe(e.target.checked)}
/>
</div>
);
};
Index.getInitialProps = ({ req }) => {
//read the cookie on the server
const cookies = parseCookies(req); //parseCookies is a simple custom function you can find
return {
//send the value as a regular prop
initialRememberValue: cookies.rememberMe
};
};
export default Index;
Reference: https://github.com/benawad/nextjs-persist-state-with-cookie/blob/master/pages/index.js

NextJs and dynamic routes - how to handle the empty parameter scenario?

I'm trying to retrieve the url parameter without using query strings, for example, http://localhost:3000/test/1, this is what I have so far:
Dir structure
test
- [pageNumber].jsx
index.jsx
import React from 'react';
import { useRouter } from 'next/router';
const Index = () => {
const router = useRouter();
return (
<>
<h1>Page number: {router.query.pageNumber}</h1>
</>
);
};
export default Index;
It works, but if I omit the pageNumber param, all I got is a 404 page, an issue we don't have on using query strings.
Now, the question: is it possible to sort this without creating an additional index.jsx page and duplicating code to handle the empty parameter scenario?
I see I may as well answer this as I got notified of MikeMajaras comment.
There are several ways of doing this, say you have a route that gets posts from a user and shows 10 posts per pages so there is pagination.
You could use an optional catch all route by creating pages/posts/[[...slug]].js and get the route parameters in the following way:
const [user=DEFAULT_USER,page=DEFAULT_PAGE] = context?.query?.slug || [];
"Disadvantage" is that pages/posts/user/1/nonexisting doesn't return a 404.
You could create pages/posts/index.js that implements all code and pages/posts/[user]/index.js and pages/posts/[user]/[page]/index.js that just export from pages/posts/index.js
export { default, getServerSideProps } from '../index';
You get the query parameters with:
const {user=DEFAULT_USER,page=DEFAULT_PAGE} = context.query;
You could also just only create pages/posts/[user]/[page]/index.js and implement all code in that one and config a redirect
module.exports = {
async redirects() {
return [
{
source: '/posts',
destination: `/posts/DEFAULT_USER`,
permanent: true,
},
{
source: '/posts/:user',
destination: `/posts/:user/DEFAULT_PAGE`,
permanent: true,
},
];
},
};

Is there a better way to handle this redux recompose branch use case?

I have a component called PartyDetails, which needs data fetched by an ajax call. I want to show a Loading component while the ajax request is in progress.
The problem is that in order to determine whether the data is loaded or not, I need access to the store. This is how my enhance looks like:
const enhance = compose(
propSetter,
lifecycleEnhancer,
loading,
)
export default enhance(PartyDetails)
where propSetter is:
const propSetter = connect((state) => {
const { party } = state
const { dataLoaded } = party
// for some reason state does not contain match, and I'm resorting to routing
const { routing: {location: { pathname }}} = state
const involvedPartyId = pathname.substring(pathname.lastIndexOf('/') + 1)
return { dataLoaded, involvedPartyId }
}, {loadParty})
and lifecycleEnhancer is:
const lifecycleEnhancer = lifecycle({
componentDidMount() {
this.props.loadParty(this.props.involvedPartyId)
}
})
and loading is ( notice that in this case, dataLoaded comes from the previous connect that has been done in propSetter ):
const loading = branch(
({dataLoaded}) => dataLoaded,
renderComponent(connect(mapStateToProps, mapDispatchToProps)(PartyDetails)),
renderComponent(Loading)
)
So basically, if the data has been fetched, I am using a 2nd connect to obtain the relevant props for PartyDetails.
I just started learning recompose a few days ago, and I could not find an example that fitted my use case. The above is what I came up with after reading through the docs, and some examples found in other articles.
Is what I'm doing a good way of handling this? Could this be done in a better way, maybe without needing 2 connect calls?
You could write all your logic for mapping state and dispatch to props in one connect:
export default compose(
connect(
state => {
const { party } = state;
const { dataLoaded } = party;
const { routing: { location: { pathname } } } = state;
const involvedPartyId = pathname.substring(pathname.lastIndexOf("/") + 1);
// also put here your `mapStateToProps` code
return { dataLoaded, involvedPartyId };
},
{
loadParty
// the same here, append `mapDispatchToProps` logic
}
),
lifecycle({
componentDidMount() {
this.props.loadParty(this.props.involvedPartyId);
}
}),
branch(
({ dataLoaded }) => dataLoaded,
renderComponent(PartyDetails),
renderComponent(Loading)
)
// as `branch` is returning HOC, we need to provide empty component to it in
// order to render whole component
)(createSink());

How to access dynamic route parameters when preloading server-side render

I am attempting to render a dynamic route preloaded with data fetched via an async thunk.
I have a static initialAction method in my Components that require preloading, and let them call the actions as needed. Once all actions are done and promises are resolved, I render the route/page.
The question that I have is: how do I reference the route parameters and/or props inside a static function?
Here is the relevant code that will call any initialAction functions that may be required to preload data.
const promises = routes.reduce((promise, route) => {
if (matchPath(req.url, route) && route.component && route.component.initialAction) {
promise.push(Promise.resolve(store.dispatch(route.component.initialAction(store))))
}
return promise;
}, []);
Promise.all(promises)
.then(() => {
// Do stuff, render, etc
})
In my component, I have the static initialAction function that will take in the store (from server), and props (from client). One way or the other, the category should be fetched via redux/thunk. As you can see, I'm not passing the dynamic permalink prop when loading via the server because I'm unable to retrieve it.
class Category extends Component {
static initialAction(store, props) {
let res = store !== null ? getCategory(REACT_APP_SITE_KEY) : props.getCategory(REACT_APP_SITE_KEY, props.match.params.permalink)
return res
}
componentDidMount(){
if(isEmpty(this.props.category.categories)){
Category.initialAction(null, this.props)
}
}
/* ... render, etc */
}
And finally, here are the routes I am using:
import Home from '../components/home/Home'
import Category from '../components/Category'
import Product from '../components/product/Product'
import NotFound from '../components/NotFound'
export default [
{
path: "/",
exact: true,
component: Home
},
{
path: "/category/:permalink",
component: Category
},
{
path: "/:category/product/:permalink",
component: Product
},
{
component: NotFound
}
]
Not entirely sure I'm even doing this in a "standard" way, but this process works thus far when on a non-dynamic route. However, I have a feeling I'm waaaay off base :)
on the server you have request object and on the client, you have location object. Extract url parameters from these objects after checking current environment.
const promises = routes.reduce((promise, route) => {
var props = matchPath(req.url, route);
if ( obj && route.component && route.component.initialAction) {
promise.push(Promise.resolve(store.dispatch(route.component.initialAction(store, props))))
}
return promise;
}, []);
now you will get url in initialAction in both desktop and server. you can extract dynamic route params from this

Resources