Next.js enables us to define dynamic routes in our apps using the brackets [param]. It allows us to design URL in such a way that for example language is passed as parameter. When no route matches the user is redirected to error page.
The idea is simple and documentation abut dynamic routes in Next.js is rather limited. Does anybody know if it's possible to assign a default value to dynamic route parameter?
There are docs pages about i18n routing and redirects and special support for locale parameters (which I have not used personally).
In a more general sense, it sounds like what you want is optional catch all routes.
You can define a file in your foo directory with the name [[...slug]].js. The params which correspond to a path like /foo/us/en is { slug: ["us", "en"] } where each segment of the path becomes an element of the slug array.
You can use getStaticPaths to generate all of the known country/language pairs. Setting fallback: true allows for the user to enter another combo and not get a 404.
export const getStaticPaths = async () => {
return {
paths: [
{ params: { slug: ["us", "en"] } },
{ params: { slug: ["us", "es"] } },
{ params: { slug: ["ca", "en"] } },
{ params: { slug: ["ca", "fr"] } },
/*...*/
],
fallback: true, // allows unknown
};
};
As far as redirection, it depends on whether you want an actual redirect such that typing in /foo leads to /foo/us/en or if those are two separate pages which show the same content. I'm going to assume that we want an actual redirect.
You'll convert from slug to props in your getStaticProps function. This is also where you implement your redirects. I'm going to assume that you have (or can create) some utility functions like isValidCountry(country) and getDefaultLanguage(country)
export const getStaticProps = async ( context ) => {
const [country, language] = context.params?.slug ?? [];
// if there is no country, go to us/en
if (!country || !isValidCountry(country)) {
return {
redirect: {
statusCode: 301, // permanent redirect
destination: "/foo/us/en",
},
};
}
// if there is no language, go to the default for that country
if (!language || !isValidLanguage(language, country)) {
return {
redirect: {
statusCode: 301, // permanent redirect
destination: `/foo/${country}/${getDefaultLanguage(country)}`,
},
};
}
// typical case, return country and language as props
return {
props: {
country,
language,
},
};
};
There are things that you can do in the component itself with useRouter and isFallback, but I'm not sure if it's needed. In dev mode at least I'm getting proper redirects.
/foo/ca/en - ok
/foo/ca/fr - ok
/foo/ca/xx - redirects to /foo/ca/en
/foo/ca - redirects to /foo/ca/en
/foo - redirects to /foo/us/en
Complete code with TypeScript types:
import { GetStaticPaths, GetStaticProps } from "next";
export interface Props {
country: string;
language: string;
}
export default function Page({ country, language }: Props) {
return (
<div>
<h1>
{country} - {language}
</h1>
</div>
);
}
const pairs = [
["us", "en"],
["us", "es"],
["ca", "en"],
["ca", "fr"],
];
const isValidCountry = (c: string) => pairs.some(([cc]) => cc === c);
const isValidLanguage = (l: string, c: string) =>
pairs.some(([cc, ll]) => cc === c && ll === l);
const getDefaultLanguage = (c: string) =>
pairs.find(([cc]) => cc === c)?.[1] ?? "en";
export const getStaticProps: GetStaticProps<Props, { slug: string[] }> = async (
context
) => {
const [country, language] = context.params?.slug ?? [];
// if there is no country, go to us/en
if (!country || !isValidCountry(country)) {
return {
redirect: {
statusCode: 301, // permanent redirect
destination: "/foo/us/en",
},
};
}
// if there is no language, go to the default for that country
if (!language || !isValidLanguage(language, country)) {
return {
redirect: {
statusCode: 301, // permanent redirect
destination: `/foo/${country}/${getDefaultLanguage(country)}`,
},
};
}
// typical case, return country and language as props
return {
props: {
country,
language,
},
};
};
export const getStaticPaths: GetStaticPaths<{ slug: string[] }> = async () => {
return {
paths: pairs.map((slug) => ({
params: { slug },
})),
fallback: true, // allows unknown
};
};
Related
The code below try to check if an url is reachable or not.
The urls to check are stored in a state called trackedUrls
I update this state with an async function checkAll.
The object just before being updated seems fine, but when the component rerender, it contains a promise !
Why ?
What I should change to my code ?
import React from "react"
export default function App() {
const [trackedUrls, setTrackedUrls] = React.useState([])
// 1st call, empty array, it's ok
// 2nd call, useEffect populate trackedUrls with the correct value
// 3rd call, when checkAll is called, it contains a Promise :/
console.log("trackedUrls :", trackedUrls)
const wrappedUrls = trackedUrls.map(urlObject => {
return (
<div key={urlObject.id}>
{urlObject.label}
</div>
)
})
// check if the url is reachable
// this works well if cors-anywhere is enable, click the button on the page
async function checkUrl(url) {
const corsUrl = "https://cors-anywhere.herokuapp.com/" + url
const result = await fetch(corsUrl)
.then(response => response.ok)
console.log(result)
return result
}
// Checks if every url in trackedUrls is reachable
// I check simultaneously the urls with Promise.all
async function checkAll() {
setTrackedUrls(async oldTrackedUrls => {
const newTrackedUrls = await Promise.all(oldTrackedUrls.map(async urlObject => {
let isReachable = await checkUrl(urlObject.url)
const newUrlObject = {
...urlObject,
isReachable: isReachable
}
return newUrlObject
}))
// checkAll works quite well ! the object returned seems fine
// (2) [{…}, {…}]
// { id: '1', label: 'google', url: 'https://www.google.Fr', isReachable: true }
// { id: '2', label: 'whatever', url: 'https://qmsjfqsmjfq.com', isReachable: false }
console.log(newTrackedUrls)
return newTrackedUrls
})
}
React.useEffect(() => {
setTrackedUrls([
{ id: "1", label: "google", url: "https://www.google.Fr" },
{ id: "2", label: "whatever", url: "https://qmsjfqsmjfq.com" }
])
}, [])
return (
<div>
<button onClick={checkAll}>Check all !</button>
<div>
{wrappedUrls}
</div>
</div>
);
}
Konrad helped me to grasp the problem.
This works and it's less cumbersome.
If anyone has a solution with passing a function to setTrackedUrls, I'm interested just for educational purpose.
async function checkAll() {
const newTrackedUrls = await Promise.all(trackedUrls.map(async urlObject => {
let isReachable = await checkUrl(urlObject.url)
const newUrlObject = {
...urlObject,
isReachable: isReachable
}
return newUrlObject
}))
setTrackedUrls(newTrackedUrls)
}
You can only put data into setState.
I have a website built by Next.js and we deployed it on Vercel.
We localize our website in 5 different languages, and as I found the solution with rewrites for localizing the urls as well.
We have a custom page: customer-stories, and would like to localize these:
customer_stories: {
hu: '/esettanulmanyok',
en: '/customer-stories',
de: '/kundengeschichte',
sk: '/pribehy-zakaznikov',
cs: '/pribehy-zakazniku',
},
So in the next config file:
Inside rewrites:
{
source: '/hu/esettanulmanyok/:id*/',
destination: '/hu/customer-stories/:id*/',
locale: false,
},
{
source: '/de/kundengeschichte/:id*/',
destination: '/de/customer-stories/:id*/',
locale: false,
},
{
source: '/cs/pribehy-zakazniku/:id*/',
destination: '/cs/customer-stories/:id*/',
locale: false,
},
{
source: '/sk/pribehy-zakaznikov/:id*/',
destination: '/sk/customer-stories/:id*/',
locale: false,
},
Which is working perfectly on localhost:
http://localhost:3000/hu/esettanulmanyok/
And a dynamic id page:
http://localhost:3000/hu/esettanulmanyok/newtestwithreference/
They work as expected, but on the released site it is different:
The same url is 404: https://barion-builderio.vercel.app/hu/esettanulmanyok/
The same dynamic url is working: https://barion-builderio.vercel.app/hu/esettanulmanyok/newtestwithreference/
What is very interested, because the main page is using pagination with getInitialProps:
When I go the original page without rewrites: https://barion-builderio.vercel.app/hu/customer-stories/
And after that, I paginate on the website:
And click on the next page:
The url is changing to the correct one, but if I try to refresh on the page, again 404:
So the dynamic [id] site is working well with rewrites, but the list page with getinitialprops not working for the first time, only with the original url, and after that if the user uses the pagination the rewrites will working. On the page we use shallow routing for not loading the site again.
I don't know why is it working properly on localhost and not on the server:
Here is the full (important part) code of my customer-stories.js:
imports...
const articlesPerPage = 4
const CustomerStories = ({
stories,
texts,
tophero,
bottomhero,
enumlocalization,
page,
navbar,
footer,
subfooter,
}) => {
const { locale, pathname } = useRouter()
const [currentPage, setCurrentPage] = useState(page)
const [filteredData, setFilteredData] = useState(stories)
const router = useRouter()
const handlePageReset = () => {
console.log('reset futott')
setCurrentPage(0)
handlePaginationClick(0)
}
const {
country,
industry,
solution,
handleIndustry,
handleCountry,
handleSolution,
} = useCustomerStoriesContext()
//FILTERING ON THE SITE
useDidMountEffect(async () => {
const queryArray = {}
if (solution && solution.key != null) {
queryArray = {
...queryArray,
'data.solution.$elemMatch.solution.type': solution.key.type,
}
}
if (industry && industry.key != null) {
queryArray = {
...queryArray,
'data.industry.type.$eq': industry.key.type,
}
}
if (country && country.key != null) {
queryArray = {
...queryArray,
'data.country.type.$eq': country.key.type,
}
}
const result = await builder.getAll('barion-userstory-data', {
options: {
noTargeting: true,
query: queryArray,
page: 0,
},
omit: 'data.blocks',
})
handlePageReset()
setFilteredData((prev) => result)
}, [country, industry, solution])
const handlePaginationClick = (page) => {
//setCurrentPage(page)
router.push(
{
pathname: LOCALIZED_URLS['customer_stories'][locale],
query: page != 0 ? { page: page != 0 ? page : '' } : {},
},
undefined,
{ shallow: false }
)
}
return (
<>
<Head>
<meta name="robots" content="noindex" />
</Head>
<div>
PAGE LIST CONTENT CUSTOMER-STORIES
</div>
</>
)
}
CustomerStories.getInitialProps = async ({
req,
res,
asPath,
query,
locale,
}) => {
if (locale == 'default') {
return {
props: {
plugins: null,
texts: null,
enumlocalization: null,
integrationready: null,
},
}
}
... FETCH DATA
const page = Number(query.page) || 0
let stories = await builder.getAll('barion-userstory-data', {
options: {
noTargeting: true,
// sort: { //TODO ADD SORTING ALPHABETICALLY
// 'data.title': 1,
// },
offset: page * articlesPerPage,
},
limit: articlesPerPage * 3,
omit: 'data.blocks',
})
...
return {
stories,
tophero,
bottomhero,
texts: texts?.data.texts[locale],
enumlocalization: enumlocalization.data,
page,
navbar,
footer,
subfooter,
}
}
export default CustomerStories
I have page where I want to add some actualities. These actualities will be first set in the Sanity and then fetched via Next.js .
My Sanity schema
export default{
name:"actuality",
title:"Aktuality",
type:"document",
fields:[
{
name:"headline",
title:"Nadpis",
type:"string"
},
{
name:"publishedAt",
title:"Datum zveřejnění",
type:"datetime"
},
{
name:"body",
title:"Text",
type:"blockContent"
}
],
preview:{
select:{
title:"headline",
}
}
}
Problem is in fetching the data.
If I do this it will work, but will return only first actuality in the Sanity
export const getServerSideProps = async (pageContext: any) => {
const query = `*[ _type == "actuality"][0]`;
const recipe = await client.fetch(query);
console.log(recipe);
if (!recipe) return { props: null, notFound: true };
else
return {
props: {
headline: recipe.headline,
publishedAt: recipe.publishedAt,
body: recipe.body,
},
};
};
But if I remove the [0] it will throw error: "Reason: undefined cannot be serialized as JSON. Please use null or omit this value."
What do I need to change in order to get an array of Actualities?
Wrap the response in a data object to serialize and call {data} in your page props like this:
export const getServerSideProps = async (pageContext: any) => {
const query = `*[ _type == "actuality"]`;
const recipe = await client.fetch(query);
console.log(recipe);
if (!recipe) return { props: null, notFound: true };
else
return {
props: {
data: {
headline: recipe.headline,
publishedAt: recipe.publishedAt,
body: recipe.body,
},
},
};
};
Few things:
it returns an array if you remove [0], you can do return just the data, regardless an array or not.
props: {
data: recipe
}
if you want to return single data with obj vallue as multiple props
props: {...recipe}
I need to make this function generic. so that to avoid condition in response. passing key through parameters is an option but I am not sure how can i use it?
async function loadOptions(search: string, loadedOptions: any, { page }: AdditionalProps) {
const response: any = await networkRequest(() =>
httpClient.get(
url.replace(":page", page.toString()).replace(":keyword", search),
),
);
return {
options: response?.data?.map((item: ItemProps) => ({
label: url.includes("/employees") ? `${item?.first_name} ${item?.last_name}` : item?.name,
value: item?.id,
})),
hasMore: response?.meta?.last_page > page,
additional: {
page: page + 1,
},
};
}
return loadOptions;
};
I am a newbie in react-native. I have a folder structure like below:
-screens
-page1.js
-page2.js
-page3.js
-page4.js
-App.js
In page1.js, I have a function to store data to localStorage
let obj = {
name: 'John Doe',
email: 'test#email.com',
city: 'Singapore'
}
AsyncStorage.setItem('user', JSON.stringify(obj));
Now I have to display these data in few of my other pages. This is my code.
class Page2 extends Component {
state = {
username: false
};
async componentDidMount() {
const usernameGet = await AsyncStorage.getItem('user');
let parsed = JSON.parse(usernameGet);
if (parsed) {
this.setState({
username: parsed.name,
email: parsed.email
});
} else {
this.setState({
username: false,
email: false
});
}
}
render() {
return (
<View style={styles.container}>
<Text style={styles.saved}>
{this.state.username}
</Text>
</View>
);
}
}
export default Page2;
This is how I display data in page2. I may need to show these in other page too.
I dont want to repeat these codes in each page.
Any suggestions how to do it in react-native?
You can extract the data you need to display into it's own component and re-use it in any page that you need to display it in.
Another option is to use a higher-order component, that way you can wrap it around any components that need the data and it'll be passed down as a prop.
You can make your Constant.js where you can put all your common required utils and constants, reusable anywhere n your app.
In your Constant.js:
export const USER_DATA = {
set: ({ user}) => {
localStorage.setItem('user', JSON.stringify(obj));
},
remove: () => {
localStorage.removeItem('user');
localStorage.removeItem('refresh_token');
},
get: () => ({
user: localStorage.getItem('user'),
}),
}
in your any component, you can import it and use like this :
import { USER_DATA } from './Constants';
let user = {
name: 'John Doe',
email: 'test#email.com',
city: 'Singapore'
}
// set LocalStorage
USER_DATA.set(user);
// get LocalStorage
USER_DATA.get().user
That's you can make Constant common file and reuse them anywhere to avoid writing redundant code.
Simplified Reusable approach of localStorage
export const localData = {
add(key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
remove(key, value) {
localStorage.removeItem(key);
},
load(key) {
const stored = localStorage.getItem(key);
return stored == null ? undefined : JSON.parse(stored);
},
};
localData.add("user_name", "serialCoder")
console.log( "After set 👉", localData.load("user_name") )
localData.remove("user_name")
console.log( "After remove 👉", localData.load("user_name") )