NextJs nested dynamic routes based on API - reactjs

I am stuck with Nextjs : I need to create nested dynamic routes based on my (local) data.
Here are the routes that I would like to create :
.../cars/ -> displays all the categories (sedan, suv, 4x4)
.../cars/category/ -> displays cars in the category
ex : .../cars/sedan -> displays cars in the sedan category
.../cars/category/id -> displays the details of the car from category which has id = 1
ex : .../cars/sedan/1 -> displays the details of the sedan car with id = 1
For routes 1 and 2 it's ok but I don't know how to do the last one. Can you help me please ?
data.js
export const cars = [
{
id: 1,
name: 'sedan',
models: [
{
id: 1,
name: 'model1',
image: '/sedan1.jpg',
},
{
id: 2,
name: 'model2',
image: '/sedan2.jpg',
},
{
id: 3,
name: 'model3',
image: '/sedan3.jpg',
},
],
},
{
id: 2,
name: 'suv',
models: [
{
id: 1,
name: 'model1',
image: '/suv1.jpg',
},
{
id: 2,
name: 'model2',
image: '/suv2.jpg',
},
{
id: 3,
name: 'model3',
image: '/suv3.jpg',
},
],
},
{
id: 3,
name: '4x4',
models: [
{
id: 1,
name: 'model1',
image: '/4x4_1.jpg',
},
{
id: 2,
name: 'model2',
image: '/4x4_2.jpg',
},
{
id: 3,
name: 'model3',
image: '/4x4_3.jpg',
},
],
},
];
/cars/index.js
import { cars } from '../../data';
import Link from 'next/link';
export default function Categories({ car }) {
return (
{car.map((c) => (
<Link key={c.id} href={`/cars/${c.name}`} passHref>
<div>{c.name}</div>
</Link>
))}
);
}
export const getStaticProps = async () => {
return {
props: {
car: cars,
},
};
};
/cars/[name].js
import React from 'react';
import { cars } from '../../data';
export default function CategoriesCars({ cars }) {
return (
<div>
{cars.models.map((m) => (
<p key={m.id}>{m.name}</p>
))}
</div>
);
}
export const getStaticPaths = async () => {
const paths = await cars.map((c) => ({
params: {
name: c.name,
},
}));
return { paths, fallback: false };
};
export const getStaticProps = async (context) => {
const { params } = context;
const response = await cars.filter((c) => c.name === params.name);
return {
props: {
cars: response[0],
},
};
};

The page folder must be:
pages/
cars/
[category]/
[id]/
index.jsx
index.jsx
then go /cars/sedan/2 you can access to category and id variables like this:
cars/[category]/[id]/index.jsx
import React from 'react';
import { useRouter } from 'next/router';
export default function Index() {
const router = useRouter();
// router.query.category -> sedan
// router.query.id -> 2
return <div>{JSON.stringify(router.query)}</div>;
}
// or
export const getServerSideProps = async (context) => {
const { params } = context;
console.log(params); // { category: 'sedan', id: '2' }
return {
props: {
cars: {},
},
};
};
// or if you wish use getStaticProps for SSG (with getStaticPaths)
export const getStaticPaths = async (context) => {
const paths = cars
.map((car) =>
car.models.map((model) => ({
params: {
id: model.id.toString(),
category: car.name,
},
}))
)
.flat(); // this is important
return { paths, fallback: false };
};
export const getStaticProps = async (context) => {
const { params } = context;
console.log(params);
return {
props: {
cars: {},
},
};
};
Example: StackBlitz

Related

Can't pass data from getStaticProps to the NextPage component

I recieve properly the datas desired with getStaticProps. But I can't understand why they are not passed to the BlogPrivate component. I have exactly the same structure in another file, and it works just well...
articlesStrapi is always undefined
Here is the code
import React, { useEffect, useState } from 'react';
import { GetStaticProps, NextPage } from 'next';
import { ArticleStrapi } from '../../#types/next';
import Article from './article';
type articlesStrapiProps = { articlesStrapi: ArticleStrapi[] };
const BlogPrivate: NextPage<articlesStrapiProps> = ({ articlesStrapi }) => {
console.log(articlesStrapi); ==> return undefined
return (
<div style={{ display: 'flex', flexDirection: 'row' }}>
//////
</div>
);
};
export const getStaticProps: GetStaticProps<{ articlesFromStrapi: ArticleStrapi[] }> = async () => {
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND}api/articles`);
const { data } = await res.json();
const articlesFromStrapi: ArticleStrapi[] = data;
return !articlesFromStrapi
? { notFound: true }
: {
props: { articlesFromStrapi },
revalidate: 10 // In seconds
};
};
export default BlogPrivate;
there is the result of a console log of the articlesFromStrapi in the getStaticProps :
[
{
id: 1,
attributes: {
title: 'Article 1dgdrgdr',
createdAt: '2022-11-22T13:28:16.243Z',
updatedAt: '2022-11-22T18:02:50.096Z',
publishedAt: '2022-11-22T13:49:16.161Z',
content: 'dfdsf sdf ds '
}
},
{
id: 6,
attributes: {
title: 'fdsfdsf',
createdAt: '2022-11-22T18:01:47.759Z',
updatedAt: '2022-11-22T18:01:48.440Z',
publishedAt: '2022-11-22T18:01:48.438Z',
content: 'dsf sdf dsf dsf dsf dsf dsf dsf '
}
}
]
And here are my interfaces :
export interface ArticleStrapi {
id: string;
attributes: Article;
}
export interface Article {
title: string;
content: string;
createdAt: string;
publishedAt: string;
updatedAt: string;
}
Let me know if you see any mistake I could do...
Thanks :)
Well your data names are not equivalent.
The prop that the component gets is articlesStrapi and the prop that you're returning from getStaticProps is articlesFromStrapi.
Here is the correct code:
const BlogPrivate: NextPage<articlesStrapiProps> = ({ articlesFromStrapi }) => {
console.log(articlesFromStrapi); ==> return undefined
return (
<div style={{ display: 'flex', flexDirection: 'row' }}>
//////
</div>
);
}
Or by changing the prop name in getStaticProps:
export const getStaticProps: GetStaticProps<{ articlesFromStrapi: ArticleStrapi[] }> = async () => {
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND}api/articles`);
const { data } = await res.json();
const articlesFromStrapi: ArticleStrapi[] = data;
return !articlesFromStrapi
? { notFound: true }
: {
props: { articlesStrapi: articlesFromStrapi },
revalidate: 10 // In seconds
};
};

React/Firebase. How can i filter some products by categories using firebase?

How can i filter some products by categories using firebase? This is a fragment of my code
Not sure if you have a correct db.json file, i had to flatMap the result but here is a working code. I used require to load you json file and left const [products, setProducts] = useState([]); just in case. Also i switched categories to useMemo so this variable will not update on each re-render.
import React, { useState, useEffect, useMemo } from "react";
import "./styles.scss";
import { Link } from "react-router-dom";
const dbProducs = require("./db.json");
const CategoriesPage = () => {
// const {product} = useContext(Context)
const [products, setProducts] = useState([]);
const categories = useMemo(() => {
return [
{ id: 1, title: "Tablets" },
{ id: 2, title: "Computers" },
{ id: 3, title: "Consoles" },
{ id: 4, title: "Photo and video" },
{ id: 5, title: "Technics" },
{ id: 6, title: "Game Content" },
{ id: 7, title: "Notebooks" },
{ id: 8, title: "Smartphones" },
{ id: 9, title: "Headphones" },
{ id: 10, title: "Steam" }
// {id: 11,imageSrc:steamcards, title: 'Стиральные машины'},
// {id: 12,imageSrc: coffeemaschine, title: 'One stars'},
// {id: 13,imageSrc:headphones, title: 'Холодильники'},
];
}, []);
useEffect(() => {
const flatMapped = dbProducs.flatMap((x) => x.products);
setProducts(flatMapped);
}, []);
return (
<section className="popular__categories">
<h3 className="events__title">
<span>Categories</span>
</h3>
<div className="categories__wrapper">
{categories.map((category) => (
<Link
to={`${category.id}`}
className="categories__content"
key={category.id}
>
<h2 className="categories__title">{category.title}</h2>
<img
className="categories__img"
alt={category.title}
src={category.imageSrc}
/>
<ul>
{products
.filter((p) => p.category === category.title)
.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</Link>
))}
</div>
</section>
);
};
export default CategoriesPage;
Technically it would be better to clone and extend your categories objects with additional array property with useMemo, or you can add additional Map object with key = Category(title) and value = products (filtered) but it is up to you.
Full example with Context, Routes, Navigation:

How to test component using spyFn in React Jest

I want to test my component Actions when I pass actions to children. In a nutshell, every source like twitter or facebook has its own set of actions. I'd like to check that it is called or not using spy.
This is my Actions component
const targetMetric = 'account dropdown'
const availableActions = {
addQuery: {
facebook: '^facebook://page/',
twitter: true
},
exclude: {
blog: [
'^blog://user/eventregistry/',
'^eventregistry://user/'
],
news: [
'^news://user/eventregistry/',
'^eventregistry://user/'
],
twitter: true,
youtube: true
},
reportAsNews: {
youtube: true,
mastodon: true,
twitter: true
}
}
const requiredHandlers = {
exclude: [
'onExcludeProfile'
],
reportAsNews: [
'onReportAsNews'
]
}
class Actions extends React.PureComponent {
get actions() {
const { account, handlers } = this.props
const actions = {};
Object.keys(availableActions).forEach(key =>
actions[ key ] = false
)
Object.keys(actions).forEach(key => {
const value = (
!!account.uri
&&
availableActions[ key ][ account.type ]
)
if (!value) {
return
}
if (typeof value === 'boolean') {
actions[ key ] = value
return
}
if (typeof value === 'string') {
const re = new RegExp(value, 'i')
actions[ key ] = re.test(account.uri)
return
}
if (
typeof value === 'object'
&&
Array.isArray(value)
) {
actions[ key ] = value.some(v => {
const re = new RegExp(v, 'i')
return re.test(account.uri)
})
}
})
Object.keys(actions).forEach(key => {
if (!actions[ key ] || !requiredHandlers[ key ]) {
return
}
actions[ key ] = requiredHandlers[ key ].some(i => handlers[ i ])
})
if (actions.addQuery) {
actions.addQuery = Object
.keys(this.addQueryActions)
.some(key => this.addQueryActions[ key ])
}
return actions
}
get addQueryActions() {
const { availableLanguages = [], userIsAdmin } = this.context
const { caseItem, handlers } = this.props
const actions = {
addQueryToFilter: !caseItem.isPaused && !!handlers.onAddQuery,
addQueryToAccountList: userIsAdmin && !!handlers.onAddToAccountList
}
actions.addQueryToSearch = actions.addQueryToFilter && !!availableLanguages.length
return actions
}
get actor() {
return pick(
this.props.account,
[ 'uri', 'name', 'link' ]
)
}
onExclude = () => {
const { account, handlers, isCaseLocked } = this.props
if (isCaseLocked) {
return
}
handlers.onExcludeProfile(account)
}
onReportAsNews = () => this.props.handlers.onReportAsNews(this.actor)
onAddToAccountList = () => {
const { account, from, handlers } = this.props
handlers.onAddToAccountList(account, from)
}
onAddToQuery = type => ({ language } = {}) => {
const { account, caseItem, handlers } = this.props
const { id } = caseItem
const metrics = {
index: getId(),
language,
type
}
handlers.onAddQuery({
...metrics,
expression: this.expressionFromAccount(account),
hideSearch: true,
id,
})
return metrics
}
expressionFromAccount = account => ({
and: [
{ account }
]
})
trackExcludeEvent = () => {
const { account } = this.props
this.trackEvent(
events.excludeAccounts,
{
accountsAdded: 1,
source: account.type
}
)
}
trackCreateNewQueryEvent = ({ index, language, type }) => {
const eventNameMap = {
filters: events.createNewFilter,
queries: events.createNewSearch,
}
const metrics = {
queryId: index,
target: targetMetric
}
if (type === 'queries') {
metrics[ 'language' ] = language.toLowerCase()
}
this.trackEvent(
eventNameMap[ type ],
metrics
)
}
trackReportAsNewsEvent = () => (
this.trackEvent(
events.reportAsNews,
{ source: this.props.account.type }
)
)
trackEvent = (eventName, props = {}) => {
const { from, message } = this.props
eventTracker.track(
eventName,
{
...props,
from,
messageUri: message.uri
}
)
}
getLangMenuActions = ({ handlers, isCaseLocked }, { availableLanguages }) => {
if (
isCaseLocked
||
!availableLanguages
||
!handlers.onAddQuery
) {
return []
}
const onClick = compose(
this.trackCreateNewQueryEvent,
this.onAddToQuery('queries')
)
return availableLanguages.map(({ label, value: language }) => ({
handler: onClick.bind(this, { language }),
id: `add-account-to-search-lang-${language}`,
label
}))
}
getActions = () => {
const { isCaseLocked } = this.props
const { userIsAdmin } = this.context
const actions = []
if (this.actions.addQuery) {
const addQueryAction = {
id: 'add-account-to',
isInactive: isCaseLocked && !userIsAdmin,
label: i18n.t('SOURCES.DROPDOWN_ADD_TO'),
children: []
}
if (this.addQueryActions.addQueryToSearch) {
addQueryAction.children.push({
id: 'add-account-to-search',
isInactive: isCaseLocked,
label: i18n.t('SOURCES.DROPDOWN_NEW_SEARCH'),
children: this.getLangMenuActions(this.props, this.context)
})
}
if (this.addQueryActions.addQueryToFilter) {
addQueryAction.children.push({
handler: compose(
this.trackCreateNewQueryEvent,
this.onAddToQuery('filters')
),
id: 'add-account-to-filter',
isInactive: isCaseLocked,
label: i18n.t('SOURCES.DROPDOWN_NEW_FILTER')
})
}
if (this.addQueryActions.addQueryToAccountList) {
addQueryAction.children.push({
handler: this.onAddToAccountList,
id: 'add-account-to-account-list',
label: i18n.t('SOURCES.DROPDOWN_ACCOUNT_LIST')
})
}
actions.push(addQueryAction)
}
if (this.actions.reportAsNews) {
actions.push({
handler: compose(
this.onReportAsNews,
this.trackReportAsNewsEvent
),
id: 'report-as-news',
label: i18n.t('SOURCES.DROPDOWN_REPORT_AS_NEWS')
})
}
if (this.actions.exclude) {
actions.push({
handler: compose(
this.onExclude,
this.trackExcludeEvent
),
id: 'exclude-account',
isInactive: isCaseLocked,
label: i18n.t('SOURCES.DROPDOWN_EXCLUDE')
})
}
console.log(actions)
return actions
}
render() {
return this.props.children({
actions: this.getActions()
})
}
}
This is my test file
import expect from 'expect'
const injectActions = require('inject-loader!./actions')
const Actions = injectActions({
'cm/common/event-tracker': {
eventTracker: {
track: () => {},
clear: () => {}
},
events: {
createNewFilter: '...',
createNewSearch: '...',
excludeAccounts: '...',
reportAsNews: '...',
}
},
}).default
const handlers = {
onAddQuery: () => { },
onAddToAccountList: () => { },
onExcludeProfile: () => { },
onReportAsNews: () => { }
}
const testProps = {
twitter: {
account: {
name: 'Twitter account',
uri: 'twitter://status/12345',
type: 'twitter'
},
handlers,
},
facebookPage: {
account: {
name: 'Facebook page account',
uri: 'facebook://page/12345',
type: 'facebook'
},
handlers
}
}
describe('Actions component', () => {
let node
beforeEach(() => {
node = document.createElement('div')
})
afterEach(() => {
ReactDOM.unmountComponentAtNode(node)
})
it('returns empty actions array by default', () => {
const spyFn = expect.createSpy().andReturn(null)
ReactDOM.render(
<Actions>{spyFn}</Actions>,
node
)
expect(spyFn).toHaveBeenCalledWith({ actions: [] })
})
describe('Twitter', () => {
it('returns "Exclude" action', () => {
const { account, handlers } = testProps.twitter
const spyFn = expect.createSpy()
ReactDOM.render(
<Actions
account={account}
handlers={{
onExcludeProfile: handlers.onExcludeProfile
}}
isCaseLocked={false}
>
{spyFn}
</Actions>,
node
)
expect(spyFn).toHaveBeenCalledWith({ actions: [
{
handler: () => {},
id: 'exclude-account',
isInactive: false,
label: 'Exclude',
}
] })
})
})
First unit case works fine, but the second is wrong. Actually I don't need all object there too. I'd like to be only sure that it contains id: 'exclude-account' there.
Please guys about any help.
You can use expect.objectContaining(object)
matches any received object that recursively matches the expected properties
E.g.
describe('68770432', () => {
test('should pass', () => {
const spyFn = jest.fn();
spyFn({
actions: [{ handler: () => {}, id: 'exclude-account', isInactive: false, label: 'Exclude' }],
});
expect(spyFn).toBeCalledWith({
actions: [
expect.objectContaining({
id: 'exclude-account',
}),
],
});
});
});

Zustand state does not re-render component or passes data correctly to then display filtered items

I'm using Zustand to store state, everything is working fine apart from this. When i click on the Song Buttons i want that to filter from the list.
Currently on fresh load it displays 3 songs. When clicking the button it should filter (and it does for first instance) but as soon as i click another button to filter again then nothing happens.
So if i chose / click Song 1 and Song 2 it should only show these songs.
I think the logic i wrote for that is correct but i must be doing something wrong with re-rendering.
Sorry i know people like to upload example here but i always find it hard with React files, so for this case I'm using https://codesandbox.io/s/damp-waterfall-e63mn?file=/src/App.js
Full code:
import { useEffect, useState } from 'react'
import create from 'zustand'
import { albums } from './albums'
export default function Home() {
const {
getFetchedData,
setFetchedData,
getAttrData,
setAttrData,
getAlbumData,
getButtonFilter,
setButtonFilter,
setAlbumData,
testState,
} = stateFetchData()
useEffect(() => {
if (getFetchedData) setAttrData(getFetchedData.feed.entry)
}, [getFetchedData, setAttrData])
useEffect(() => {
setAlbumData(getButtonFilter)
}, [getButtonFilter, setAlbumData])
// useEffect(() => {
// console.log('testState', testState)
// console.log('getAlbumData', getAlbumData)
// }, [getAlbumData, testState])
useEffect(() => {
setFetchedData()
}, [setFetchedData])
return (
<div>
<div>Filter to Show: {JSON.stringify(getButtonFilter)}</div>
<div>
{getAttrData.map((props, idx) => {
return (
<FilterButton
key={idx}
attr={props}
getDataProp={getButtonFilter}
setDataProp={setButtonFilter}
/>
)
})}
</div>
<div>
{getAlbumData?.feed?.entry?.map((props, idx) => {
return (
<div key={idx}>
<h1>{props.title.label}</h1>
</div>
)
})}
</div>
</div>
)
}
const FilterButton = ({ attr, getDataProp, setDataProp }) => {
const [filter, setFilter] = useState(false)
const filterAlbums = async (e) => {
const currentTarget = e.currentTarget.innerHTML
setFilter(!filter)
if (!filter) setDataProp([...getDataProp, currentTarget])
else setDataProp(getDataProp.filter((str) => str !== currentTarget))
}
return <button onClick={filterAlbums}>{attr.album}</button>
}
const stateFetchData = create((set) => ({
getFetchedData: albums,
setFetchedData: async () => {
set((state) => ({ ...state, getAlbumData: state.getFetchedData }))
},
getAttrData: [],
setAttrData: (data) => {
const tempArr = []
for (const iterator of data) {
tempArr.push({ album: iterator.category.attributes.label, status: false })
}
set((state) => ({ ...state, getAttrData: tempArr }))
},
getButtonFilter: [],
setButtonFilter: (data) => set((state) => ({ ...state, getButtonFilter: data })),
testState: {
feed: { entry: [] },
},
getAlbumData: [],
setAlbumData: (data) => {
set((state) => {
console.log('🚀 ~ file: index.js ~ line 107 ~ state', state)
const filter = state.getAlbumData.feed?.entry.filter((item) =>
data.includes(item.category.attributes.label),
)
return {
...state,
getAlbumData: {
...state.getAlbumData,
feed: {
...state.getAlbumData.feed,
entry: filter,
},
},
}
})
},
}))
Sample data:
export const albums = {
feed: {
entry: [
{ title: { label: 'Song 1' }, category: { attributes: { label: 'Song 1' } } },
{ title: { label: 'Song 2' }, category: { attributes: { label: 'Song 2' } } },
{ title: { label: 'Song 3' }, category: { attributes: { label: 'Song 3' } } },
],
},
}

Can't load mock data while mounting component

I have a component that should render a list of mock items. The initial value is an empty array, and I want to load mock data during component render. But it doesn't work correctly - list in component is empty when I try to check it out by printing in console, but Redux Devtools shows that it is not. What am I doing wrong?
Component
import React, { Component } from 'react';
import TagsBlock from './TagsBlock';
import ActionButton from './ActionButton';
import { connect } from 'react-redux';
import { actionLoadCoctails, actionToggleDetail } from '../actions/actionCreators';
class ResultsCoctails extends Component {
componentDidMount() {
this.props.actionLoadCoctails();
}
list = this.props.loadCoctails.map(({ img, name, tags}, key) => {
const showDetail = (e) => {
e.preventDefault();
this.props.actionToggleDetail();
}
return (
<div
className="item"
key={`coctail-${key}`}
>
<a
href="#"
onClick={(e) => showDetail(e)}
>
<div className="img">
<img src={img} alt="error" />
</div>
<div className="desc">
<div className="name">{name}</div>
<TagsBlock tags={tags}></TagsBlock>
</div>
</a>
</div>
)
});
render() {
return (
<div className="result-coctails">
<div className="block">
{this.list}
</div>
<ActionButton txt="morе"></ActionButton>
</div>
)
}
}
export default connect(state => ({
loadCoctails: state.loadCoctails
}), { actionLoadCoctails, actionToggleDetail })(ResultsCoctails);
Reducer
import { LOAD_COCTAILS } from '../constants';
const INIT_COCTAILS = [
{
img: 'some url',
name: 'Cocktail Mary',
tags: ['one', 'two', 'three'],
},
{
img: 'some url',
name: 'White Russian',
tags: ['one', 'two', 'three'],
},
{
img: 'some url',
name: 'Cocktail Mary',
tags: ['one', 'two', 'three'],
},
{
img: 'some url',
name: 'White Russian',
tags: ['one', 'two', 'three'],
},
{
img: 'some url',
name: 'Cocktail Mary',
tags: ['one', 'two', 'three'],
}
];
export const loadCoctails = (state = [], { type }) => {
switch(type) {
case LOAD_COCTAILS:
return {
...state, ...INIT_COCTAILS
}
default:
return state;
}
}
ActionCreator
import {
LOAD_COCTAILS,
TOGGLE_DETAIL,
LOAD_DETAIL
} from '../constants';
export const actionLoadCoctails = () => {
return {
type: LOAD_COCTAILS
}
}
export const actionToggleDetail = () => {
return {
type: TOGGLE_DETAIL
}
};
export const actionLoadDetail = (img, name, tags, deg, txt) => {
return {
type: LOAD_DETAIL,
img,
name,
tags,
deg,
txt
}
};
The problem is that the map() function can't work with objects - so, we should make an array and do map() with it:
const listArray = Object.values(this.props.loadCoctails);
const list = listArray.map(({ img, name, tags}, key) => {
.....

Resources