I'm building a site in nextjs but I came across a problem.
I have the cover of the site, where there is a list of products, and on the top menu the list of product categories.
The products are looking via getStaticProps (So that it is done by the servideor and is cached).
However, the categories are inside a separate component, where inside I need to load the category listing from my API.
getStaticProps does not work in this case as it is not a page but a component.
Fetching inside a useEffect is bad, as each access loads the api.
So the question remains, how can I do this server-side fetch and deliver the cached (json) return? (Simulating getStaticProps)
As your component is listed on every page, you could consider either using Context or local caching in the browser within the shared Category component.
Context provides a way to pass data through the component tree without
having to pass props down manually at every level.
But there are performance considerations using Context and may be overkill here. If you really don't want to hit the API, data is not changing often, and you don't need to pass functions through the component tree, then you could consider some basic browser caching using localStorage.
import React, { useState, useEffect } from 'react';
const mockAPI = () => {
return new Promise((resolve) => {
setTimeout(() => {
return resolve([
{
id: '1',
name: 'One'
},
{
id: '2',
name: 'Two'
}
]);
}, 1000);
});
};
const Component = () => {
const [categories, setCategories] = useState(null);
useEffect(() => {
(async () => {
if (categories === null) {
let data = window.localStorage.getItem('categories');
if (data === null) {
console.info('calling api...');
data = await mockAPI();
window.localStorage.setItem('categories', JSON.stringify(data));
}
setCategories(JSON.parse(data));
}
})();
}, []);
return <nav>{categories && categories.map((category) => <li key={category.id}>{category.name}</li>)}</nav>;
};
export default Component;
The caveat here is you need to know where to clear localStorage. There are many ways to implement this from using basic timers to looking at SWR
You could also consider Redux but is probably overkill for something elementary like this.
Related
When page is refreshed query is lost, disappears from react-query-devtools.
Before Next.js, I was using a react and react-router where I would pull a parameter from the router like this:
const { id } = useParams();
It worked then. With the help of the, Next.js Routing documentation
I have replaced useParams with:
import { usePZDetailData } from "../../hooks/usePZData";
import { useRouter } from "next/router";
const PZDetail = () => {
const router = useRouter();
const { id } = router.query;
const { } = usePZDetailData(id);
return <></>;
};
export default PZDetail;
Does not work on refresh. I found a similar topic, but manually using 'refetch' from react-query in useEffects doesn't seem like a good solution. How to do it then?
Edit
Referring to the comment, I am enclosing the rest of the code, the react-query hook. Together with the one already placed above, it forms a whole.
const fetchPZDetailData = (id) => {
return axiosInstance.get(`documents/pzs/${id}`);
};
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {});
};
Edit 2
I attach PZList page code with <Link> implementation
import Link from "next/link";
import React from "react";
import TableModel from "../../components/TableModel";
import { usePZSData } from "../../hooks/usePZData";
import { createColumnHelper } from "#tanstack/react-table";
type PZProps = {
id: number;
title: string;
entry_into_storage_date: string;
};
const index = () => {
const { data: PZS, isLoading } = usePZSData();
const columnHelper = createColumnHelper<PZProps>();
const columns = [
columnHelper.accessor("title", {
cell: (info) => (
<span>
<Link
href={`/pzs/${info.row.original.id}`}
>{`Dokument ${info.row.original.id}`}</Link>
</span>
),
header: "Tytuł",
}),
columnHelper.accessor("entry_into_storage_date", {
header: "Data wprowadzenia na stan ",
}),
];
return (
<div>
{isLoading ? (
"loading "
) : (
<TableModel data={PZS?.data} columns={columns} />
)}
</div>
);
};
export default index;
What you're experiencing is due to the Next.js' Automatic Static Optimization.
If getServerSideProps or getInitialProps is present in a page, Next.js
will switch to render the page on-demand, per-request (meaning
Server-Side Rendering).
If the above is not the case, Next.js will statically optimize your
page automatically by prerendering the page to static HTML.
During prerendering, the router's query object will be empty since we
do not have query information to provide during this phase. After
hydration, Next.js will trigger an update to your application to
provide the route parameters in the query object.
Since your page doesn't have getServerSideProps or getInitialProps, Next.js statically optimizes it automatically by prerendering it to static HTML. During this process the query string is an empty object, meaning in the first render router.query.id will be undefined. The query string value is only updated after hydration, triggering another render.
In your case, you can work around this by disabling the query if id is undefined. You can do so by passing the enabled option to the useQuery call.
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {
enabled: id
});
};
This will prevent making the request to the API if id is not defined during first render, and will make the request once its value is known after hydration.
I'm replacing getState with an enhancer as follows:
interface ArtificialStateEnhancerProps {
getArtificialStateGetter: StateGetterFn;
}
export const getArtificialStateEnhancer = ({
getArtificialStateGetter
}: ArtificialStateEnhancerProps) => {
return (createStore: typeof createStoreOriginal) => {
return (rootReducer, preloadedState, enhancers) => {
const store = createStore(rootReducer, preloadedState, enhancers);
const { getState } = store;
if (getArtificialStateGetter) {
return { ...store, getState: getArtificialStateGetter(getState) };
}
return { ...store };
};
};
};
When using store.getState() somewhere in my code it works an my custom getStage method is used. However within an Action or Sage (using select()) the original, native getState from redux is used.
Example action:
export function myAction(
slug: string
): ReduxAction<any> {
return async (
dispatch: Dispatch,
getState: () => any // Here getState is used but the native version of redux
): Promise<any> => {
const state = getState();
const foo = ...do something with `state`
};
}
Is this intended behavior?
It most likely depends on the ordering of where the middleware enhancer is added vs your custom enhancer.
If the middleware enhancer is added after the custom enhancer, then it is dealing with the base-level store methods. Each enhancer kind of "stacks" on top of each other.
My guess is that you've got something like:
compose(myCustomEnhancer, applyMiddleware(saga))
meaning that the middleware enhancer is after the custom one.
You'd need to flip those if that's the case.
(As a side question: could you give more details as to why you're writing a custom enhancer? It's a valid technique, but very rarely used, and it's even more rare that you would ever need or want to override getState.)
Also, note that we generally recommend against using sagas in almost all use cases, and especially if you're looking at using sagas for basic data fetching. Today, Redux Toolkit's "RTK Query" data fetching and caching API does a vastly better job of handling data fetching for you, and the RTK "listener" middleware handles the same reactive logic use case as sagas but with a simpler API, smaller bundle size, and better TS support.
See my talk The Evolution of Redux Async Logic for comparisons and explanations of why we recommend against sagas.
I am using ISR to build static product pages using next.js. Since there are a lot of product page to generate I only generated a few pages for it. The problem that I am trying to solve is the delay in transferring the view to the product page.
So I have a category page and within it have a list of products. On each product card item, I use next.js link so that the user can go to the product page.
The problem here is the delay going to the product page when the page is not yet generated. Going to the product page is slow because next.js is building the page. I want to transfer the user to the product page immediately while showing the loading state of the page via the router.isFallback condition.
What I'm trying to achieve is the same as what a normal link would do because it shows the loading state of the page but I don't want to reload the page.
Instead of using next/link or router.push, use router.replace
router.replace(`/product/${id"}`)
Let me know if this work.
What you could do is to make the props not required,
The thing that must take time during the loading of you're ISG page of nextJs is the call api in GetStaticProps,
Something like that:
export async function getStaticProps({ params }) {
const { status, data } = await axios.get<Data>(
`${server}/data`
);
if (status === 404) {
return { notFound: true };
}
return {
props: {
...data
},
revalidate: 60,
};
}
But you could also decide that you will fetch the data during the loading of the state with a fall-back blocking:
const MyPage = (props) => {
const [data,setData] = useState<Data>(null);
useEffect(() => {
(function()
const {data} = axios.get(`${server}/data`);
setData(data);
)()
},[])
return(
<div>
!data ? <div>loading ... </div> : <div>Product: {data}</div>
</div>
)
}
export async function getStaticProps({ params }){
return {
props: {
isloading: true
},
revalidate: 60,
};
}
I'm trying to use a Contentful references field to generate SSR landing pages which are populated with dynamic React components mapped to each content type.
The references field is basically an array of other content types that the user can add / edit / remove / reorder as they see fit:
The Contentful API is called in getServerSideProps.
export async function getServerSideProps(context) {
const config = require('../../config');
const contentful = require('contentful');
const client = contentful.createClient({
space: config.contentful.spaceId,
accessToken: config.contentful.deliveryAccessToken,
environment: config.contentful.environment,
});
const content = await client.getEntries({
content_type: 'landingPage',
'fields.slug': context.query.slug,
include: 2,
});
return { props: { landingPage: content.items[0]}};
};
The components are then rendered dynamically like so:
const LandingPage = (props) => {
return (
<MainLayout>
<div>{renderComponents(props.landingPage.fields.body)}</div>
</MainLayout>
);
};
renderComponents: (componentMap is just an object mapping item.sys.contentType.sys.id strings to React components)
const renderComponents = (data) => {
return data
.filter((item) => {
return item.sys.contentType.sys.id in componentMap;
})
.map((item, index) => {
const Component = componentMap[item.sys.contentType.sys.id];
const props = item.fields;
return (
<Component {...props} />
);
})
};
This all seems to works fine in both dev and production builds, however I've noticed that if I create another non-SSR page that uses these same components, all interactivity is lost from the SSR pages only.
This happens for all components on the dynamic page, even the ones that were not generated by the renderComponents function (for example, the navigation, which is standard across the entire site, and is part of MainLayout).
Deleting the non-dynamic pages immediately causes the interactivity return.
There are no error messages in either the browser console or terminal, which is making it difficult to debug exactly what is going wrong here.
Any advice appreciated,
Thanks
next.config.js is unchaged from default:
module.exports = {
useFileSystemPublicRoutes: true,
};
Fairly new to redux, react-redux, and redux toolkit, but not new to React, though I am shaky on hooks. I am attempting to dispatch an action from the click of a button, which will update the store with the clicked button's value. I have searched for how to do this high and low, but now I am suspecting I am thinking about the problem in React, without understanding typical redux patterns, because what I expect to be possible is just not done in the examples I have found. What should I be doing instead? The onclick does seem to capture the selection, but it is not being passed to the action. My goal is to show a dynamic list of buttons from data collected from an axios get call to a list of routes. Once a button is clicked, there should be a separate call to an api for data specific to that clicked button's route. Here is an example of what I currently have set up:
reducersRoutes.js
import { createSlice } from "#reduxjs/toolkit";
import { routesApiCallBegan } from "./createActionRoutes";
const slice = createSlice({
name: "routes",
initialState: {
selected: ''
},
{... some more reducers...}
routeSelected: (routes, action) => {
routes.selected = action.payload;
}
},
});
export default slice.reducer;
const { routeSelected } = slice.actions;
const url = '';
export const loadroutes = () => (dispatch) => {
return dispatch(
routesApiCallBegan({
url,
{...}
selected: routeSelected.type,
})
);
};
createActionRoutes.js
import { createAction } from "#reduxjs/toolkit";
{...some other actions...}
export const routeSelected = createAction("routeSelection");
components/routes.js:
import { useDispatch, useSelector } from "react-redux";
import { loadroutes } from "../store/reducersRoutes";
import { useEffect } from "react";
import { routeSelected } from "../store/createActionRoutes";
import Generic from "./generic";
const Routes = () => {
const dispatch = useDispatch();
const routes = useSelector((state) => state.list);
const selected = useSelector((state) => state.selected);
useEffect(() => {
dispatch(loadroutes());
}, [dispatch]);
const sendRouteSelection = (selection) => {
dispatch(routeSelected(selection))
}
return (
<div>
<h1>Available Information:</h1>
<ul>
{routes.map((route, index) => (
<button key={route[index]} className="routeNav" onClick={() => sendRouteSelection(route[0])}>{route[1]}</button>
))}
</ul>
{selected !== '' ? <Generic /> : <span>Data should go here...</span>}
</div>
);
};
export default Routes;
Would be happy to provide additional code if required, thanks!
ETA: To clarify the problem - when the button is clicked, the action is not dispatched and the value does not appear to be passed to the action, even. I would like the selection value on the button to become the routeSelected state value, and then make an api call using the routeSelected value. For the purpose of this question, just getting the action dispatched would be plenty help!
After writing that last comment, I may actually see a couple potential issues:
First, you're currently defining two different action types named routeSelected:
One is in the routes slice, generated by the key routeSelected
The other is in createActionRoutes.js, generated by the call to createAction("routeSelection").
You're importing the second one into the component and dispatching it. However, that is a different action type string name than the one from the slice - it's just 'routeSelection', whereas the one in the slice file is 'routes/routeSelected'. Because of that, the reducer logic in the slice file will never run in response to that action.
I don't think you want to have that separate createAction() call at all. Do export const { routeSelected } = slice.actions in the slice file, and dispatch that action in the component.
I'm also a little concerned about the loadroutes thunk that you have there. I see that you might have omitted some code from the middle, so I don't know all of what it's doing, but it doesn't look like it's actually dispatching actions when the fetched data is retrieved.
I'd recommend looking into using RTK's createAsyncThunk API to generate and dispatch actions as part of data fetching - see Redux Essentials, Part 5: Async Logic and Data Fetching for examples of that.