Prevent useEffect infinite loop when updating NextJs query string - next.js

I have a tag input that produces an array of tags. Using NextJs's useRouter, I want to add those tags as query string params as they are added. I also need to preserve the current query string params, since other filters, searches, and pagination need to remain.
Here is how I'm currently doing it.
const router = useRouter();
const { query } = router;
const [tags, setTags] = useState([]);
useEffect(() => {
router.push({
query: {
...query,
tags,
},
});
}, [tags, router, query]);
return (
<>
<TagInput tags={tags} setTags={setTags} placeholder="Search by tags" />
</>
);
However, this causes an infinite render since the useEffect updates the query but also has query as a dependency. If I remove query as a dependency, it works fine, but I get the missing dependency linting error.
Edit: Here is a codesandbox with a minimal example that reproduces the issue. It works as is, but if you uncomment the query dependency, infinite loop.
https://codesandbox.io/s/next-js-dynamic-routing-forked-rlxuqh?file=/pages/index.js

One way this can be done is to directly update the query object and read the tags from the url, rather than storing the tags in state.
const router = useRouter();
const { query } = router;
const setTags = useCallback((tags) => {
router.push({
query: {
...query,
tags
}
});
}, [router, query]);
return (
<>
<TagInput tags={query.tags || []} setTags={setTags} placeholder="Search by tags" />
</>
);

Related

React Query - useQuery callback dependent on route parameter? [duplicate]

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.

NextJS dynamically rendered components via SSR losing reactivity

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,
};

Next.js: How to create multiple dynamic pages coming from different types of API?

This is my first experience with Next.js. I am trying to create a dynamic route from the data coming from server.
I do convert the id to string but have the same error.
Server Error
Error: A required parameter (articleid) was not provided as a string in getStaticPaths for /article/[articleid]
I tried something similar with the data from web api it works fine but not for the data that I fetch from server. Can't figure out what I am missing.
Also why error message is pointing out server error in the first line?
Here is the component the error is coming from: pages/article/[articleid]/index.js
export const getStaticProps = async (context) => {
const res = await fetch(`${server}/api/article/${context.params.id}`);
const article = await res.json();
return {
props: {
article,
},
};
};
export const getStaticPaths = async () => {
const res = await fetch(`${server}/api/article/`);
const articles = await res.json();
const ids = articles.map((article) => article.id);
const paths = ids.map((id) => ({ params: { id: id.toString() } }));
return {
fallback: "blocking",
paths: paths,
};
};
`pages/api/article/[id].js file
import { articles } from "../../../data";
export default function handler({ query: { id } }, res) {
const filteredData = articles.filter((article) => article.id === id);
if (filteredData.length > 0) {
res.status(200).json(filteredData[0]);
} else {
res.status(404).json({ message: `article with id of ${id} is not found` });
}
}
UPDATE
I found out that my problem is definitely not from the code provided above.
Actually in my app I have another dynamic page where I fetch the data from another web api, which works fine. Changing the urls I found out that now web api dynamic page is throwing the same error. I assume the problem is that how I defined the paths, , [articleid] I mean.
Here is my components structure
The problem is with `pages/article/[articleid].
Here is how I am linking to the specific item
<Link href="/article/[articleid]" as={`/article/${article.id}`}>
<a className={styles.container}>
<h1>{article.title} →</h1>
<p>{article.body}</p>
</a>
</Link>
Any help will be appreciated
Actually I made it work changing the dynamic route id name in brackets, in my case [articleid], to just [id] and it worked fine. Image below.
But honestly, I didn't understand why the previous keyword ([articleid]) in brackets was not working.
I also tried another keywords inside the brackets for dynamic route like [article] and nothing worked except [id].
I didn't find anything related in Next.js docs about that.
I'd welcome any explanations why only [id] worked.

Optimize calls to exteral Api from getServerSideProps

Assume I have the following page in my next.js react application:
// Filename: [mypath].jsx
export default function MyPage(props) {
return (
<>
<Link href="/siteX">
<a>Go to siteX</a>
</Link>
<Link href="/siteY">
<a>Go to siteY</a>
</Link>
<div>{props.data.text}</div>
</>
);
}
export async function getServerSideProps(context) {
const mypath = context.params.mypath;
const res = await fetch(`https://external-api/${mypath}`)
const data = await res.json();
return { props: { data } };
}
When I access http://localhost:3000/siteX/ on server side the string siteX from the url is used for a call to an external (!) api on a different system, e.g. https://external-api/siteX. This works fine so far, but I see the following performance issue:
In the browser, when I click on a <Link>, two requests are happening: One to my own server to update getServerSideProps with the new path and a second one from my server to https://external-api/... to fetch the new data.
Do you see a way to optimize this? What I want is:
when clicking on <Link> there is only one request directly to https://external-api/... happening and data is updated directly (e.g. as a state of MyPage).
As it is now, when accessing http://localhost:3000/siteX/ the server should fetch the data and prerender the site
I could of course treat data as a state of <MyPage> and simply call a function to update it with a request when <Link> is clicked. But I also want a correct routing, history and so on.
Can you help me with that? Thank you!
After some more resesarch I came across a solution. The next.js <Link> component has a property shallow that I can set to prevent the execution of getServersideProps. That way I can query the new data manually when a link is clicked. Still the initial data query is done by getServersideProps on the server and my intial SSR is working as before.
// Filename: [mypath].jsx
export default function MyPage(props) {
const [data, setData] = useState(props.data);
function updateData(path) {
const res = await fetch(`https://external-api/${path}`)
const data = await res.json();
setData(data);
}
return (
<>
<!-- Manually query and set data -->
<div onClick={() => updateData("siteX")}>
<!-- Trigger routing, but don't execute getServerSideProps (because of shallow={true}) -->
<Link href="/siteX" shallow={true}>
<a>Go to siteX</a>
</Link>
</div>
<div onClick={() => updateData("siteY")}>
<Link href="/siteY" shallow={true}>
<a>Go to siteY</a>
</Link>
</div>
<div>{props.data.text}</div>
</>
);
}
// If page is requested directly via url (and not because a <Link> element has been clicked) do SSR as before
export async function getServerSideProps(context) {
const mypath = context.params.mypath;
const res = await fetch(`https://external-api/${mypath}`)
const data = await res.json();
return { props: { data } };
}

Use async react-select with redux-saga

I try to implement a async react-select (Select.Async). The problem is, we want to do the fetch in redux-saga. So if a user types something, the fetch-action should be triggered. Saga then fetches the record and saved them to the store. This works so far.
Unfortunately loadOptions has to return a promise or the callback should be called. Since the newly retrieved options get propagated with a changing property, I see no way to use Select.Async together with saga to do the async fetch call. Any suggestions?
<Select.Async
multi={false}
value={this.props.value}
onChange={this.onChange}
loadOptions={(searchTerm) => this.props.options.load(searchTerm)}
/>
I had a hack where i assigned the callback to a class variable and resolve it on componentWillReceiveProps. That way ugly and did not work properly so i look for a better solution.
Thanks
redux-saga is for handling side effects like asynchronously receiving options for react-select. That's why you should leave the async stuff to redux-saga. I have never used react-select but by just looking at the documentation I would solve it this way:
Your component gets very simple. Just get value and options from your redux store. optionsRequested is an action creator for the OPTIONS_REQUESTED action:
const ConnectedSelect = ({ value, options, optionsRequested }) => (
<Select
value={value}
options={options}
onInputChange={optionsRequested}
/>
)
export default connect(store => ({
value: selectors.getValue(store),
options: selectors.getOptions(store),
}), {
optionsRequested: actions.optionsRequested,
})(ConnectedSelect)
A saga definition watches for OPTIONS_REQUESTED action that is trigged by onInputChange, loads the data with given searchTerm from server and dispatches OPTIONS_RECEIVED action to update redux store.
function* watchLoadOptions(searchTerm) {
const options = yield call(api.getOptions, searchTerm)
yield put(optionsReceived(options))
}
In other words: Make your Component as pure as possible and handle all side-effect/async calls in redux-saga
I hope this answer was useful for you.
The main idea is that you are capable to dispatch redux actions using application context from
import React from 'react';
import { connect } from 'react-redux';
import Select from '#components/Control/Form/Skin/Default/Select';
import { reduxGetter, reduxSetter, required as req } from '#helpers/form';
import { companyGetTrucksInit } from "#reduxActions/company";
import AppContext from '#app/AppContext';
const FIELD_NAME = 'truck';
export const getReduxValue = reduxGetter(FIELD_NAME);
export const setReduxValue = reduxSetter(FIELD_NAME);
const SelectCompanyTruck = (props) => {
const {
required,
validate=[]
} = props;
const vRules = [...validate];
if (required)
vRules.push(req);
return (
<AppContext.Consumer>
{({ dispatchAction }) => (
<Select
loadOptions={(inputValue, callback) => {
function handleResponse(response) {
const { data: { items } } = response;
const options = items.map(i => ({ label: i.name, value: i.id }));
callback(options);
}
dispatchAction(companyGetTrucksInit, { resolve: handleResponse, inputValue });
}}
name={FIELD_NAME}
{...props}
/>
)}
</AppContext.Consumer>
);
}
export default SelectCompanyTruck;

Resources