Query params update but app doesn't (sometimes)
Answered
berkserbet posted this in #help-forum
berkserbetOP
I have an ecommerce app with some filters. When the filters are checked the url on my site updates. [my site](https://storefront.community/)
But sometimes when the url changes the products don't get filtered. So there basically isn't a state change. I can recreate it, so it's not random - but I don't understand what could be the issue. I'm new to nextjs - but haven't seen url update not update the app.
But sometimes when the url changes the products don't get filtered. So there basically isn't a state change. I can recreate it, so it's not random - but I don't understand what could be the issue. I'm new to nextjs - but haven't seen url update not update the app.
898 Replies
DirtyCajunRice | AppDir
a link to your site isnt relevant. Please post code showing where you get the params, and where you consume them
berkserbetOP
Sure, I just didn't want to post a ton of code. Thank you
berkserbetOP
It's just one page.
Here is /app/page.tsx
Here is /app/ProductBrowser.tsx
Here is /app/page.tsx
import React, { Suspense } from 'react'
import ProductBrowser from './ProductBrowser';
interface Props {
searchParams: {
r: string,
search: string,
genders: string,
sizes: string,
conditions: string,
countries: string,
brands: string,
pages: number
}
}
export default function Home({searchParams: { r, search, genders, sizes, conditions, countries, brands, pages }}: Props) {
return (
<main>
<Suspense fallback={<div className="h-screen flex items-center justify-center pb-56"><span className="loading loading-dots loading-lg text-secondary text-center h-screen flex items-center justify-center"></span></div>}>
<ProductBrowser search={search} selected_subreddits={r} selected_genders={genders} selected_sizes={sizes} selected_conditions={conditions} selected_countries={countries} selected_brands={brands} pages={pages}/>
</Suspense>
</main>
);
}
Here is /app/ProductBrowser.tsx
import React from 'react'
import { promises as fs } from 'fs';
import path from 'path'
import ProductCards from './ProductCards'
interface Props {
search: string,
selected_subreddits: string,
selected_genders: string,
selected_sizes: string,
selected_conditions: string,
selected_countries: string,
selected_brands: string,
pages: number
}
const ProductBrowser = async ({ search, selected_subreddits, selected_genders, selected_sizes, selected_conditions, selected_countries, selected_brands, pages }: Props) => {
const filePath = path.join(process.cwd(), 'json/listings.json');
const file = await fs.readFile(filePath, 'utf8');
const products = JSON.parse(file)
return (
<ProductCards products={products} search={search} selected_subreddits={selected_subreddits} selected_genders={selected_genders} selected_sizes={selected_sizes} selected_conditions={selected_conditions} selected_countries={selected_countries} selected_brands={selected_brands} pages={pages}/>
)
}
export default ProductBrowser
DirtyCajunRice | AppDir
um
so…
berkserbetOP
I was about to share the last file
DirtyCajunRice | AppDir
first off
berkserbetOP
Do you already see an issue?
DirtyCajunRice | AppDir
there are some silly coding habits im gonna point out
so you can improve
berkserbetOP
Please do! I'm new to this
DirtyCajunRice | AppDir
the search params are the same as the props for the component
you are directly passing them
so define them in the component only
export the interface
and import it to page and use it
then you dont have to spread searchParams
you can do <ProductBrowser {...searchParams} />
berkserbetOP
I think I get what you're saying
DirtyCajunRice | AppDir
secondly
berkserbetOP
Let me quickly try that
DirtyCajunRice | AppDir
ALL search params are string
doesnt matter that it can be a number. it is a string
so if you want to make pages a number, you need to do that after
thirdly, all searchParams can also be an array of strings
so you need to check for that
berkserbetOP
So I updated my
app/page.tsx
to be this:import React, { Suspense } from 'react'
import ProductBrowser from './ProductBrowser';
interface Props {
searchParams: {
r: string,
search: string,
genders: string,
sizes: string,
conditions: string,
countries: string,
brands: string,
pages: number
}
}
export default function Home({searchParams}: Props) {
return (
<main>
<Suspense fallback={<div className="h-screen flex items-center justify-center pb-56"><span className="loading loading-dots loading-lg text-secondary text-center h-screen flex items-center justify-center"></span></div>}>
<ProductBrowser searchParams={searchParams}/>
</Suspense>
</main>
);
}
Just exporting the searchParams
DirtyCajunRice | AppDir
well… you can do it that way too… but you dont need to
berkserbetOP
Can I access search params directly from components without passing it around?
DirtyCajunRice | AppDir
export type ProductSearchParams: Record<'r' | 'search' | 'genders' | 'sizes' | 'conditions' | 'countries' | 'brands' | 'pages', string | string[]>
put that in your ProductBrowser.tsx
and change your Props to that
so remove Props interface completely
then also import that into your page.tsx
berkserbetOP
Sorry I'm a bit confused - what's the first thing I need to do from my existing codebase?
DirtyCajunRice | AppDir
interface Props {
searchParams: ProductSearchParams
}
export default function Home({searchParams}: Props)
copy that type into your product browser file
berkserbetOP
Giving a few errors
Is it supposed to be =
rather than :
Also
'Record' only refers to a type, but is being used as a value here.ts(2693)
DirtyCajunRice | AppDir
yeah
im on mobile
berkserbetOP
Errors gone
DirtyCajunRice | AppDir
show me your product browser page again now updated
berkserbetOP
I haven't been able to het it to work yet, trying to understand the page.tsx changes
DirtyCajunRice | AppDir
nono
ignore page.tsx
ignore getting it working
just show it
its 3 steps
berkserbetOP
import React from 'react'
import { promises as fs } from 'fs';
import path from 'path'
import ProductCards from './ProductCards'
export type ProductSearchParams = Record<'r' | 'search' | 'genders' | 'sizes' | 'conditions' | 'countries' | 'brands' | 'pages', string | string[]>
interface Props {
search: string,
selected_subreddits: string,
selected_genders: string,
selected_sizes: string,
selected_conditions: string,
selected_countries: string,
selected_brands: string,
pages: number
}
const ProductBrowser = async ({ search, selected_subreddits, selected_genders, selected_sizes, selected_conditions, selected_countries, selected_brands, pages }: Props) => {
const filePath = path.join(process.cwd(), 'json/listings.json');
const file = await fs.readFile(filePath, 'utf8');
const products = JSON.parse(file)
return (
<ProductCards products={products} search={search} selected_subreddits={selected_subreddits} selected_genders={selected_genders} selected_sizes={selected_sizes} selected_conditions={selected_conditions} selected_countries={selected_countries} selected_brands={selected_brands} pages={pages}/>
)
}
export default ProductBrowser
I don't use ProductSearchParams
DirtyCajunRice | AppDir
nice. delete Props
berkserbetOP
Cool, app still works
DirtyCajunRice | AppDir
change the : Props in the component to ProductSearchParams
remove all the selected_ prefix nonsense
berkserbetOP
Then what do I pass to ProductCards?
DirtyCajunRice | AppDir
the props
berkserbetOP
const ProductBrowser = async ({}: ProductSearchParams) => {
const filePath = path.join(process.cwd(), 'json/listings.json');
const file = await fs.readFile(filePath, 'utf8');
const products = JSON.parse(file)
return (
<ProductCards products={products} search={search} selected_subreddits={selected_subreddits} selected_genders={selected_genders} selected_sizes={selected_sizes} selected_conditions={selected_conditions} selected_countries={selected_countries} selected_brands={selected_brands} pages={pages}/>
)
}
export default ProductBrowser
DirtyCajunRice | AppDir
just named properly
nono
leave the props
just remove the prefix
you added a selected_ prefix for no reason
to all the keys
berkserbetOP
import React from 'react'
import { promises as fs } from 'fs';
import path from 'path'
import ProductCards from './ProductCards'
export type ProductSearchParams = Record<'r' | 'search' | 'genders' | 'sizes' | 'conditions' | 'countries' | 'brands' | 'pages', string | string[]>
const ProductBrowser = async ({ r, search, genders, sizes, conditions, countries, brands, pages }: ProductSearchParams) => {
const filePath = path.join(process.cwd(), 'json/listings.json');
const file = await fs.readFile(filePath, 'utf8');
const products = JSON.parse(file)
return (
<ProductCards products={products} search={search} selected_subreddits={r} selected_genders={genders} selected_sizes={sizes} selected_conditions={conditions} selected_countries={countries} selected_brands={brands} pages={pages}/>
)
}
export default ProductBrowser
DirtyCajunRice | AppDir
yes exactly
and when we are done you can do the same thing to ProductCards
but 1 thing at a time
berkserbetOP
Seeing a lot of
Type 'string | string[]' is not assignable to type 'string'.
Type 'string[]' is not assignable to type 'string'.
DirtyCajunRice | AppDir
yep. what i was saying you need to check for. we will get to that
go to page.tsx
import ProductSearchParams
from your other file
berkserbetOP
Done
import { ProductSearchParams } from './ProductBrowser';
DirtyCajunRice | AppDir
make your searchParams: ProductSearchParams
in your page props def
berkserbetOP
import React, { Suspense } from 'react'
import ProductBrowser from './ProductBrowser';
import { ProductSearchParams } from './ProductBrowser';
interface Props {
ProductSearchParams: {
r: string,
search: string,
genders: string,
sizes: string,
conditions: string,
countries: string,
brands: string,
pages: number
}
}
export default function Home({ProductSearchParams: { r, search, genders, sizes, conditions, countries, brands, pages }}: Props) {
return (
<main>
<Suspense fallback={<div className="h-screen flex items-center justify-center pb-56"><span className="loading loading-dots loading-lg text-secondary text-center h-screen flex items-center justify-center"></span></div>}>
<ProductBrowser search={search} selected_subreddits={r} selected_genders={genders} selected_sizes={sizes} selected_conditions={conditions} selected_countries={countries} selected_brands={brands} pages={pages}/>
</Suspense>
</main>
);
}
DirtyCajunRice | AppDir
then where you have your ProductBrowser in page.tsx just make it <ProductBrowser {...searchParams } />
nono
berkserbetOP
I see. 🙂
import React, { Suspense } from 'react'
import ProductBrowser from './ProductBrowser';
import { ProductSearchParams } from './ProductBrowser';
interface Props {
searchParams: ProductSearchParams
}
export default function Home({searchParams: { r, search, genders, sizes, conditions, countries, brands, pages }}: Props) {
return (
<main>
<Suspense fallback={<div className="h-screen flex items-center justify-center pb-56"><span className="loading loading-dots loading-lg text-secondary text-center h-screen flex items-center justify-center"></span></div>}>
<ProductBrowser {...searchParams } />
</Suspense>
</main>
);
}
Error is
Cannot find name 'searchParams'. Did you mean 'URLSearchParams'?ts(2552)
DirtyCajunRice | AppDir
import React, { Suspense } from 'react'
import ProductBrowser from './ProductBrowser';
import { ProductSearchParams } from './ProductBrowser';
interface Props {
searchParams: ProductSearchParams
}
export default function Home({ searchParams }: Props) {
return (
<main>
<Suspense fallback={<div className="h-screen flex items-center justify-center pb-56"><span className="loading loading-dots loading-lg text-secondary text-center h-screen flex items-center justify-center"></span></div>}>
<ProductBrowser {…searchParams} />
</Suspense>
</main>
);
}
simple
like this
gotta change those dots tho
iphone fucks up the dots
gotta type them manually haha
berkserbetOP
Done!
Works
No errors
DirtyCajunRice | AppDir
see how the page has no errors?
magic 😉
now
berkserbetOP
Magic
DirtyCajunRice | AppDir
now before we move on
where are you going to host this thing
berkserbetOP
Vercel I host it on
DirtyCajunRice | AppDir
ok. then your fs read is silly and wasteful
and the component doesnt need async nor suspense
berkserbetOP
🙂
DirtyCajunRice | AppDir
just import the json
berkserbetOP
The json can get big
DirtyCajunRice | AppDir
doesnt matter
berkserbetOP
And changes often
Cool
DirtyCajunRice | AppDir
it doesnt change without redeploying
berkserbetOP
Correct
DirtyCajunRice | AppDir
so its literally the same thing either way
berkserbetOP
Trying
import products from '../json/listings.json'
?DirtyCajunRice | AppDir
you use cwd
so 1 dot
berkserbetOP
VScode autofilled
DirtyCajunRice | AppDir
1 dot == same folder 2 dots == parent folder
vscode is caca
😂
berkserbetOP
Got it
🙂
DirtyCajunRice | AppDir
ok so remove async and just pass that along
also remove suspense from parent as the compnent isnt async so it doesnt do anything
berkserbetOP
New page.tsx:
import React, { Suspense } from 'react'
import ProductBrowser from './ProductBrowser';
import { ProductSearchParams } from './ProductBrowser';
interface Props {
searchParams: ProductSearchParams
}
export default function Home({ searchParams }: Props) {
return (
<main>
<ProductBrowser {...searchParams} />
</main>
);
}
DirtyCajunRice | AppDir
so clean
berkserbetOP
Current ProductBrowser.tsx
import React from 'react'
import ProductCards from './ProductCards'
export type ProductSearchParams = Record<'r' | 'search' | 'genders' | 'sizes' | 'conditions' | 'countries' | 'brands' | 'pages', string | string[]>
import products from './json/listings.json'
const ProductBrowser = ({ r, search, genders, sizes, conditions, countries, brands, pages }: ProductSearchParams) => {
return (
<ProductCards products={products} search={search} selected_subreddits={r} selected_genders={genders} selected_sizes={sizes} selected_conditions={conditions} selected_countries={countries} selected_brands={brands} pages={pages}/>
)
}
export default ProductBrowser
DirtyCajunRice | AppDir
less clean haha
berkserbetOP
I don't even need the productbrowser now I think
DirtyCajunRice | AppDir
you do not 🙂
berkserbetOP
I can just pass directly
DirtyCajunRice | AppDir
so update your Product card params to use the same logic you used for the browser
berkserbetOP
On it
So my ProductCards.tsx starts as:
'use client'
import React from 'react'
import Link from 'next/link'
import SearchContainer from './SearchContainer';
import Image from 'next/image'
import { useRouter } from 'next/navigation';
export type ProductSearchParams = Record<'r' | 'search' | 'genders' | 'sizes' | 'conditions' | 'countries' | 'brands' | 'pages', string | string[]>
import products from './json/listings.json'
DirtyCajunRice | AppDir
do you wanna make the ProductSearchParams cleaner?
how comfortable are you with typescript
berkserbetOP
I do
Not super
Up to you
DirtyCajunRice | AppDir
const productSearchParams = ['r', 'search', 'genders', 'sizes', 'conditions', 'countries', 'brands', 'pages'] as const;
type ProductSearchParam = typeof allSearchParams[number];
export type ProductSearchParams = Record<ProductSearchParam, string | string[]>;
tada
berkserbetOP
Amazing!
DirtyCajunRice | AppDir
if any of that is confusing, ask
copying without understanding is the worst sin lol
berkserbetOP
What is "as const"
DirtyCajunRice | AppDir
coding on phone is such a pain lol
berkserbetOP
What is it for I mean
DirtyCajunRice | AppDir
means its read only and cant be edited
so its “static†basically
you cant infer a type from a mutable array
berkserbetOP
What is allSearchParams?
DirtyCajunRice | AppDir
only a static (immutable) arrat
berkserbetOP
I don't have that variable
DirtyCajunRice | AppDir
just a list of the keys we will use, separated for organization purposes
its the first thing
oh
whoops
all should be product
berkserbetOP
Got it
DirtyCajunRice | AppDir
again. phone -,/
so the type is saying “this type is all the keys of this arrayâ€
berkserbetOP
So what does the second line do
type ProductSearchParam = typeof allSearchParams[number]
DirtyCajunRice | AppDir
what i just said
berkserbetOP
Is it looping somehow
DirtyCajunRice | AppDir
knew you would ask haha
its the typescript way to say “all the items in the array as type optionsâ€
berkserbetOP
Sorry I don't get it
" as type options"
This part
DirtyCajunRice | AppDir
when you hover over it
does it show you the options?
berkserbetOP
Like that?
DirtyCajunRice | AppDir
yes but hover over the left side
the type itself
berkserbetOP
This one?
DirtyCajunRice | AppDir
see what it did?
it plucked all the items out of the array and made them “this or this or this†as a type
berkserbetOP
But how does it know the type?
We haven't given any
DirtyCajunRice | AppDir
we did
we said it is the items in the array
its a new type
of those strings
berkserbetOP
Got it
DirtyCajunRice | AppDir
“just typescript thingsâ€
😂
berkserbetOP
🙂
DirtyCajunRice | AppDir
and you know what a Record is?
berkserbetOP
And I get the third line
DirtyCajunRice | AppDir
ok cool
berkserbetOP
I don't actually
What is Record
I assumed but not sure
DirtyCajunRice | AppDir
a record is just an object, but where you already know all the key and value types
so you dont have to manually define it because it can be inferred
your original one was a “manual†record
this one is “automatic†because we have a key type
berkserbetOP
Which is string or array of strings
Right?
DirtyCajunRice | AppDir
thats the value type
the key type is the array of productSearchParams
so string…. but “specificâ€
berkserbetOP
Very cool
DirtyCajunRice | AppDir
so record says “for each item in the key type, the value type isâ€
berkserbetOP
The next thing I did was import the json
DirtyCajunRice | AppDir
yes.
do you have a type for your json?
berkserbetOP
import listings from './json/listings.json'
But giving error
DirtyCajunRice | AppDir
what error
screenshot your folder/file window
berkserbetOP
'listings' is declared but its value is never read.ts(6133)
Cannot find module './json/listings.json' or its corresponding type declarations.ts(2307)
Cannot find seems to be important
DirtyCajunRice | AppDir
^
berkserbetOP
I think it's in the right spot
DirtyCajunRice | AppDir
wait
is app and json at the same level?
berkserbetOP
No
Its a level up 🙂
I see
..
DirtyCajunRice | AppDir
also
do you have type aliases in tsconfig?
it would help you
not type
path aliases
berkserbetOP
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
},
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "*/listings.json"],
"exclude": ["node_modules"]
}
This is what I got
DirtyCajunRice | AppDir
yeah
so you can import smarter
berkserbetOP
"baseUrl": "./src",
"paths": {
"@modules/*": ["rest/modules/*"],
"@services/*": ["services/*"]
}
Found this online
Like that?
DirtyCajunRice | AppDir
nono
yours is good
leave it
berkserbetOP
Cool
No more error in the import
My json is a list of objects which I define in Cards
DirtyCajunRice | AppDir
import Listings from “@/json/listings.json"
you can import this way
with the aliases
much prettier imo anyway haha
berkserbetOP
Yup!
No error
DirtyCajunRice | AppDir
whats your listings json look like
just show me like the first 20-30 lines
berkserbetOP
[
{
"title": "Lululemon City Keeper Gloves",
"brands": [
"Lululemon"
],
"actions": [
"Selling"
],
"status": "Active",
"genders": [
"Women"
],
"sizes": [],
"categories": [],
"price": 29,
"currency": "USD",
"countries": [],
"images": [
"https://i.redd.it/71paagbg2afc1.jpg",
"https://i.redd.it/7rfw7gbg2afc1.jpg"
],
"conditions": [
"New With Tags"
],
"colors": [],
"shipping_info": "Shipping included.",
"search_keywords": null,
"timestamp": 1706489846,
"source": "Reddit",
"reddit_subreddit": "lululemonBST",
"reddit_author": "mrasifs",
"reddit_post_id": "1adie9g",
"reddit_thread_id": null,
"reddit_comment_id": null,
"listing_number": 0
},
{
"title": "Og Align 25\u201d (4) Dark Forest",
"brands": [
"Lululemon"
],
"acti
interface Product {
title: string;
brands: string[];
actions: string[];
status: string | null;
genders: string[];
sizes: string[];
categories: string[];
price: number | null;
currency: string | null;
countries: string[];
images: string[];
conditions: string[];
colors: string[];
shipping_info: string | null;
search_keywords: string | null;
timestamp: number; // in seconds
source: string;
reddit_subreddit: string;
reddit_author: string;
reddit_post_id: string | null;
reddit_thread_id: string | null;
reddit_comment_id: string | null;
listing_number: number;
}
Defined here
DirtyCajunRice | AppDir
wheres that at
berkserbetOP
In ProductCards.tsx
DirtyCajunRice | AppDir
make yourself a types folder
so you keep things organized
berkserbetOP
But that is the only type
No other one
DirtyCajunRice | AppDir
is it? we have already made another type
😉
berkserbetOP
allSearchParams?
Yeah
DirtyCajunRice | AppDir
mhm
and you will make more.
berkserbetOP
Where should I put the types folder?
DirtyCajunRice | AppDir
same level as json
outside app
berkserbetOP
Created it
DirtyCajunRice | AppDir
make a products.ts file
move that type and the 3 lines we made into that file
to keep things organized
then fix imports if needed
berkserbetOP
const allSearchParams = ['r', 'search', 'genders', 'sizes', 'conditions', 'countries', 'brands', 'pages'] as const;
type ProductSearchParam = typeof allSearchParams[number];
export type ProductSearchParams = Record<ProductSearchParam, string | string[]>;
interface Product {
title: string;
brands: string[];
actions: string[];
status: string | null;
genders: string[];
sizes: string[];
categories: string[];
price: number | null;
currency: string | null;
countries: string[];
images: string[];
conditions: string[];
colors: string[];
shipping_info: string | null;
search_keywords: string | null;
timestamp: number; // in seconds
source: string;
reddit_subreddit: string;
reddit_author: string;
reddit_post_id: string | null;
reddit_thread_id: string | null;
reddit_comment_id: string | null;
listing_number: number;
}
Thats the whole file
DirtyCajunRice | AppDir
dont forget to export Product
berkserbetOP
const allSearchParams = ['r', 'search', 'genders', 'sizes', 'conditions', 'countries', 'brands', 'pages'] as const;
type ProductSearchParam = typeof allSearchParams[number];
export type ProductSearchParams = Record<ProductSearchParam, string | string[]>;
export interface Product {
title: string;
brands: string[];
actions: string[];
status: string | null;
genders: string[];
sizes: string[];
categories: string[];
price: number | null;
currency: string | null;
countries: string[];
images: string[];
conditions: string[];
colors: string[];
shipping_info: string | null;
search_keywords: string | null;
timestamp: number; // in seconds
source: string;
reddit_subreddit: string;
reddit_author: string;
reddit_post_id: string | null;
reddit_thread_id: string | null;
reddit_comment_id: string | null;
listing_number: number;
}
DirtyCajunRice | AppDir
ok. very nice
berkserbetOP
Now trying import
DirtyCajunRice | AppDir
@/types/products
import { Product } from
something like that
berkserbetOP
My page.tsx:
import React from 'react'
import ProductCards from './ProductCards';
import ProductSearchParams from '@/types/products';
interface Props {
searchParams: ProductSearchParams
}
export default function Home({ searchParams }: Props) {
return (
<main>
<ProductCards {...searchParams} />
</main>
);
}
ProductSearchParams has an error
Module '"/Users/berkserbetcioglu/Code/storefront/types/products"' has no default export. Did you mean to use 'import { ProductSearchParams } from "/Users/berkserbetcioglu/Code/storefront/types/products"' instead?ts(2613)
Fixed
What even is the { } for?
DirtyCajunRice | AppDir
good. if you couldnt figure that one out i might have given up on you
berkserbetOP
🙂
DirtyCajunRice | AppDir
means an export in the file
since you can export many things
berkserbetOP
Also done with importing to ProductCard
'use client'
import React from 'react'
import Link from 'next/link'
import SearchContainer from './SearchContainer';
import Image from 'next/image'
import { useRouter } from 'next/navigation';
import products from '@/json/listings.json'
import { Product } from '@/types/products';
Still have this error
DirtyCajunRice | AppDir
does it need to be client?
berkserbetOP
It has a bunch of product listings that update based on filters
So I think so
DirtyCajunRice | AppDir
arent your filters in the url…?
berkserbetOP
Yeah
DirtyCajunRice | AppDir
so…
berkserbetOP
Ah
DirtyCajunRice | AppDir
not client
😉
berkserbetOP
App broke, but I think I'm on the right track 🙂
I use
useRouter()
DirtyCajunRice | AppDir
for what
to update the url?
berkserbetOP
Yeah
router.push(path, { scroll: false });
DirtyCajunRice | AppDir
instead use redirect()
same thing and no use
berkserbetOP
Cool
import redirect from 'nextjs-redirect'
this one?DirtyCajunRice | AppDir
nono
nextjs/navigation iiirc
this
helpers gotta know the docs
or we would be lost
berkserbetOP
Updated
DirtyCajunRice | AppDir
whats your productcards look like now
berkserbetOP
Unhandled Runtime Error
Error: Event handlers cannot be passed to Client Component props.
<input name="r" type=... id="r" value=... className=... defaultChecked=... onClick={function}>
^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
DirtyCajunRice | AppDir
yeah you will change those to a form
with server action
berkserbetOP
Cool
DirtyCajunRice | AppDir
pause for a sec
lets clean up the search params
berkserbetOP
This is the start of my Product Cards
import React from 'react'
import Link from 'next/link'
import SearchContainer from './SearchContainer';
import Image from 'next/image'
import products from '@/json/listings.json'
import { Product } from '@/types/products';
import { redirect } from 'next/navigation'
interface Props {
products: Product[],
search: string,
r: string,
genders: string,
sizes: string,
conditions: string,
countries: string,
brands: string,
pages: number
}
const ProductCards = ({r, search, genders, sizes, conditions, countries, brands, pages}: Props) => {
I know Props can be gone
DirtyCajunRice | AppDir
right
in this case tho, we wanna sanitize your props
berkserbetOP
const ProductCards = ({r, search, genders, sizes, conditions, countries, brands, pages}: ProductSearchParams) => {
I did this
DirtyCajunRice | AppDir
so lets make a helper function above ProductCards
berkserbetOP
function sanitize(params: ProductSearchParams) {
return params
}
?With like if's in there
DirtyCajunRice | AppDir
we can be fancy though
sec
berkserbetOP
I like that
DirtyCajunRice | AppDir
function sanitize(params: ProductSearchParams) {
const stringsOnly = Object.fromEntries(Object.entries(params).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v]));
return {... stringsOnly, pages: Number(stringsOnly.pages)};
}
does that func make sense?
berkserbetOP
For some of them I just use comma seperated strings
Then split by the comma to form an array
Is that ok?
DirtyCajunRice | AppDir
comma separated is not how nextjs does arrays
so thats fine
to make an array someone would have to do ?pages=2&pages=3
multiple keys turn it into an array
berkserbetOP
I can have longish ones
DirtyCajunRice | AppDir
thats totally fine
berkserbetOP
I don't quite understand
DirtyCajunRice | AppDir
so lets work from inside to outside
you know what Object.entries does?
berkserbetOP
I don't
DirtyCajunRice | AppDir
takes an object, and turns it into an array
each item in the array is an array of [key, value]
berkserbetOP
Got it
DirtyCajunRice | AppDir
so it would be like
[["pages", "2"], ["r", "poo"]]
[["pages", "2"], ["r", "poo"]]
berkserbetOP
Perfect
DirtyCajunRice | AppDir
we then map those values
berkserbetOP
Yup
DirtyCajunRice | AppDir
and keep the key but check the value
berkserbetOP
If array then v[0]
Why is that?
DirtyCajunRice | AppDir
yeah. basically say “if you manually put more than 1 key on my site i dont care i want the first oneâ€
berkserbetOP
Cool
DirtyCajunRice | AppDir
never trust the user
berkserbetOP
stringsOnly has an array of arrays now
DirtyCajunRice | AppDir
or a fart after age 30
jkjk
berkserbetOP
With the first val
Of each key
DirtyCajunRice | AppDir
and each value as a string
berkserbetOP
Yup, then the last one is noing number conversion - not sure how
DirtyCajunRice | AppDir
then because you want pages to be a number, we return a new object that we spread all the keys back in, but override the pages key to a number version
berkserbetOP
Oh cool
So little code to accomplish that
DirtyCajunRice | AppDir
you could also just do
stringsOnly.pages = Number(stringsOnly.pages)
stringsOnly.pages = Number(stringsOnly.pages)
but i like clean short code
berkserbetOP
Yeah for sure
Where do I call sanitize?
DirtyCajunRice | AppDir
so heres the fun part
your object spread you have in ProductCards definition
move that INSIDE the component
berkserbetOP
🙂 crazy
DirtyCajunRice | AppDir
as like
const {put it here} = sanitize(props)
const {put it here} = sanitize(props)
berkserbetOP
Got it
DirtyCajunRice | AppDir
and your func should just be (props: ProductSearchParams
then you get the same functionality, but clean
berkserbetOP
Some errors on the vars:
'const' declarations must be initialized.ts(1155)
Variable 'brands' implicitly has an 'any' type
const ProductCards = (props: ProductSearchParams) => {
const r, search, genders, sizes, conditions, countries, brands, pages = sanitize(props)
DirtyCajunRice | AppDir
nono
inside {}
berkserbetOP
Oh
DirtyCajunRice | AppDir
its still an object
berkserbetOP
Like this?
const ProductCards = (props: ProductSearchParams) => {
const { r, search, genders, sizes, conditions, countries, brands, pages } = sanitize(props)
DirtyCajunRice | AppDir
nice
no errors right?
berkserbetOP
I have errors
Property 'r' does not exist on type '{ pages: number; }'.ts(2339)
DirtyCajunRice | AppDir
i have emotional errors
berkserbetOP
🙂
DirtyCajunRice | AppDir
hahahaha
berkserbetOP
I appreciate you so greatly
DirtyCajunRice | AppDir
show me your sanitize func
berkserbetOP
function sanitize(params: ProductSearchParams) {
const stringsOnly = Object.fromEntries(Object.entries(params).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v]));
return {... stringsOnly, pages: Number(stringsOnly.pages)};
}
DirtyCajunRice | AppDir
ok. we will do this the verbose way
go to your types
and export ProductSearchParam
so we have access to it
berkserbetOP
I do that
export type ProductSearchParams = Record<ProductSearchParam, string | string[]>;
DirtyCajunRice | AppDir
nope
that is a different type
🙂
hint: no s at the end
berkserbetOP
Ah
export type ProductSearchParam = typeof allSearchParams[number];
DirtyCajunRice | AppDir
yes
ok so now at the end of your sanitize where you return the object
berkserbetOP
I imported it
Current:
return {... stringsOnly, pages: Number(stringsOnly.pages)};
DirtyCajunRice | AppDir
add this at the end
as Record<Exclude<ProductSearchParam, 'pages'>, string> & { pages: number };
as Record<Exclude<ProductSearchParam, 'pages'>, string> & { pages: number };
berkserbetOP
Addind
DirtyCajunRice | AppDir
im ready for the questions
😂
berkserbetOP
I get it
DirtyCajunRice | AppDir
oh? very good
berkserbetOP
Exclude pages mark string
Pages number
DirtyCajunRice | AppDir
my man
ok. is the other error now gone?
berkserbetOP
Why couldn't we use ProductSearchParams
Error gone
DirtyCajunRice | AppDir
Because then we need to use a much fancier typescript type to exclude because that is already a record type
and i cant be assed to type that out on my phone
😅
berkserbetOP
Yup!
In my borwser localhost I see a related error
./app/ProductCards.tsx
Error:
× cannot reassign to a variable declared with `const`
â•â”€[/Users/berkserbetcioglu/Code/storefront/app/ProductCards.tsx:12:1]
12 │ }
13 │
14 │ const ProductCards = (props: ProductSearchParams) => {
15 │ const { r, search, genders, sizes, conditions, countries, brands, pages } = sanitize(props)
·
So in the declaration of those variables with sanitize
DirtyCajunRice | AppDir
are you using those variable names anywhere else?
inside that component
berkserbetOP
I likely am
DirtyCajunRice | AppDir
if you want to be safe
berkserbetOP
I will update
DirtyCajunRice | AppDir
you can not destructure it
and save as like. initialParams
then get them via initialParams.whatever
berkserbetOP
Ah
Yeah sounds good
const { initialParams } = sanitize(props)
Like that?
const initialParams = sanitize(props)
Like that
DirtyCajunRice | AppDir
yeah exactly
berkserbetOP
Cool, worked
Updating vars in file
DirtyCajunRice | AppDir
once you change your router.push to redirects, your problem should be gone
berkserbetOP
Getting an error with my products variable
I imported like this:
import products from '@/json/listings.json'
The error is long:
Argument of type '({ title: string; brands: string[]; actions: string[]; status: string; genders: string[]; sizes: string[]; categories: string[]; price: number; currency: string; countries: string[]; images: string[]; conditions: string[]; ... 10 more ...; listing_number: number; } | ... 29 more ... | { ...; })[]' is not assignable to parameter of type 'Product[]'.
Type '{ title: string; brands: string[]; actions: string[]; status: string; genders: string[]; sizes: string[]; categories: string[]; price: number; currency: string; countries: string[]; images: string[]; conditions: string[]; ... 10 more ...; listing_number: number; } | ... 29 more ... | { ...; }' is not assignable to type 'Product'.
Type '{ title: string; brands: string[]; actions: string[]; status: string; genders: string[]; sizes: string[]; categories: string[]; price: number; currency: string; countries: string[]; images: string[]; conditions: string[]; ... 10 more ...; reddit_thread_id?: undefined; }' is not assignable to type 'Product'.
Types of property 'reddit_thread_id' are incompatible.
Type 'undefined' is not assignable to type 'string | null'.ts(2345)
DirtyCajunRice | AppDir
ah yeah
if the key isnt there, you dont put null
you put ?:
berkserbetOP
I either put an empty array if array type or null
DirtyCajunRice | AppDir
Types of property 'reddit_thread_id' are incompatible.
Type 'undefined' is not assignable to type 'string | null'.ts(2345)
berkserbetOP
Hmm
DirtyCajunRice | AppDir
according to this, you have an undefined somewhere in that json, but dont have it in your type
berkserbetOP
Let me check
DirtyCajunRice | AppDir
show me your Project interface
berkserbetOP
--What is that?--
export interface Product {
title: string;
brands: string[];
actions: string[];
status: string | null;
genders: string[];
sizes: string[];
categories: string[];
price: number | null;
currency: string | null;
countries: string[];
images: string[];
conditions: string[];
colors: string[];
shipping_info: string | null;
search_keywords: string | null;
timestamp: number; // in seconds
source: string;
reddit_subreddit: string;
reddit_author: string;
reddit_post_id: string | null;
reddit_thread_id: string | null;
reddit_comment_id: string | null;
listing_number: number;
}
DirtyCajunRice | AppDir
this is what i meant
change your reddit_thread_id
to add a ? before:
berkserbetOP
?reddit_thread_id: string | null;
Like that?
DirtyCajunRice | AppDir
reddit_thread_id?: string | null;
berkserbetOP
Done
DirtyCajunRice | AppDir
error gone?
berkserbetOP
Nope seemingly there will be more with that json
Argument of type '({ title: string; brands: string[]; actions: string[]; status: string; genders: string[]; sizes: string[]; categories: string[]; price: number; currency: string; countries: string[]; images: string[]; conditions: string[]; ... 10 more ...; listing_number: number; } | ... 29 more ... | { ...; })[]' is not assignable to parameter of type 'Product[]'.
Type '{ title: string; brands: string[]; actions: string[]; status: string; genders: string[]; sizes: string[]; categories: string[]; price: number; currency: string; countries: string[]; images: string[]; conditions: string[]; ... 10 more ...; listing_number: number; } | ... 29 more ... | { ...; }' is not assignable to type 'Product'.
Type '{ title: string; brands: string[]; actions: string[]; status: string; genders: string[]; sizes: string[]; categories: string[]; price: number; currency: string; countries: string[]; images: string[]; conditions: string[]; ... 10 more ...; reddit_thread_id?: undefined; }' is not assignable to type 'Product'.
Types of property 'shipping_info' are incompatible.
Type 'never[]' is not assignable to type 'string'.
I'm not sure what
never[]
isBut shipping_info is null often
DirtyCajunRice | AppDir
basically, you need to match up your json with your type
so go through those 1 by 1 and fix
or fix the type
berkserbetOP
But why is it broken now?
DirtyCajunRice | AppDir
for that one its adding [] to the shipping_info
berkserbetOP
It's the same json
DirtyCajunRice | AppDir
yeah your json is wonky haha
doesnt adhere like you thought it did
berkserbetOP
Got it, fixing
Done!
🙂
DirtyCajunRice | AppDir
so you are set?
berkserbetOP
Not quite, just settled errors in VScode
Now the form thing I believe
Error: Event handlers cannot be passed to Client Component props.
<input name="r" type=... id="r" value=... className=... defaultChecked=... onClick={function}>
^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
read throught this
berkserbetOP
On it
berkserbetOP
Do functions that do redirects need to be client side?
DirtyCajunRice | AppDir
nope
redirect() is designed to work in server actions
berkserbetOP
Cool, I'm just trying to figure out how to turn my 6 <input>'s into <form>'s
I think
DirtyCajunRice | AppDir
dont
turn all into 1 form
berkserbetOP
Hmm
DirtyCajunRice | AppDir
then you can build the url the same way regardless
berkserbetOP
This is what I have for each:
<input
name="r"
type="checkbox"
id="r"
value={subreddit_name}
className="checkbox checkbox-sm"
defaultChecked={checkHandler("r", {subreddit_name})}
onClick={(e) => handleClick(e.target)}
/>
DirtyCajunRice | AppDir
find the closest parent that contains all the inputs
and add a <form> to wrap it
berkserbetOP
But I have 6 seperate dropdowns
DirtyCajunRice | AppDir
yes
berkserbetOP
Each dropdown has on input for many options
DirtyCajunRice | AppDir
i understand 🤣 Its fine
trust.
berkserbetOP
I trust 🙂
Doing it now
It will basically be
return (
<form>
Is that ok?
DirtyCajunRice | AppDir
thats fine
what is your handleClick doing btw
just rebuilding the url?
berkserbetOP
Yeah
function handleClick(checkbox: any) {
if (typeof window !== "undefined") {
queryParams = new URLSearchParams(window.location.search);
}
const checkboxes = document.getElementsByName(checkbox.name);
let selected: string[] = [];
checkboxes.forEach((item: any) => {
if (item.checked) selected.push(item.value);
});
if (selected.length === 0) {
queryParams.delete(checkbox.name);
} else {
queryParams.set(checkbox.name, selected.join(','));
}
queryParams.delete("pages");
const path = window.location.pathname + "?" + queryParams.toString();
redirect(path);
}
Probably badly
DirtyCajunRice | AppDir
sokay. we can clean it up.
leave the function, but remove the onClick for all of them
berkserbetOP
Sure
Done
DirtyCajunRice | AppDir
one question... does the user need to submit at all?
or are you immediately changing the value
berkserbetOP
Immediate change
I also have a "Load More..." button that increments page
All others are checkboxes
DirtyCajunRice | AppDir
ok. we are gonna make input (JUST INPUT) a client component
make a new file
like... checkbox.tsx
berkserbetOP
On it
I have the file
Actuallly
I have a seperate searchbar
It's in a seperate component
With 'use client'
So not only immidiate
But in ProductCards all immidiate
DirtyCajunRice | AppDir
yeah thats fine
berkserbetOP
Awesome
What do I put in the file?
DirtyCajunRice | AppDir
'use client';
export const CheckBox = (props: DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>) => {
const onClick = (e: any) => e.currentTarget.form?.requestSubmit();
return <input {...props} onClick={onClick} />
}
was typing it out
berkserbetOP
Amazing thanks!
Can't find
InputHTMLAttributes
and DetailedHTMLProps
so trying to figure out import
DirtyCajunRice | AppDir
import { DetailedHTMLProps, InputHTMLAttributes } from "react";
berkserbetOP
Awesome worked
DirtyCajunRice | AppDir
should be a drop-in replacement for your <input>
berkserbetOP
Testing!
DirtyCajunRice | AppDir
well you still need to make your server action
berkserbetOP
So I replace the input with CheckBox?
DirtyCajunRice | AppDir
yes
keep all the props the same
just change the tag
berkserbetOP
Yup
The onclick is what I need to do now
DirtyCajunRice | AppDir
function submitForm(formData: FormData) {
'use server';
console.log("Form Data:", formData);
}
start with this
<form action={submitForm}>
berkserbetOP
Where should I have form?
DirtyCajunRice | AppDir
^
berkserbetOP
I see
DirtyCajunRice | AppDir
you are just adding the action prop
berkserbetOP
Yeah
Trying to figure this out:
To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations#convention
DirtyCajunRice | AppDir
O.o
are you using an old version of next...?
berkserbetOP
I didn't think so
Let me check
Next.js v13.5.6
DirtyCajunRice | AppDir
yeah old
14.1.0
berkserbetOP
I'll update
It's because my node was older
DirtyCajunRice | AppDir
XD
berkserbetOP
I have an issue when trying to update
Working on it
I seemingly have multiple versions
But next is pointing at an old one
DirtyCajunRice | AppDir
let me know when you sort that XD
berkserbetOP
I think it worked now
Sorry about that
DirtyCajunRice | AppDir
np
multitasking
berkserbetOP
Seeing:
× Server actions must be async functions
Should I make it
DirtyCajunRice | AppDir
yeah thats true
yes
berkserbetOP
Is this wrong?
async function submitForm(formData: FormData) {
'use server';
console.log("Form Data:", formData);
}
Big red error on browser
Same error
DirtyCajunRice | AppDir
yeah thats fine
berkserbetOP
DirtyCajunRice | AppDir
thats an old error
doesnt show your change
stop and start the dev env
berkserbetOP
Ok yeah
Got a runtime error:
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
DirtyCajunRice | AppDir
did you leave onClick somewhere
berkserbetOP
I had left the button
Commenting out
Still same error
DirtyCajunRice | AppDir
gotta find where its happening
berkserbetOP
⨯ Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
<... loader={function} src=... onError=... className=... width=... height=... alt=...>
^^^^^^^^^^
at stringify (<anonymous>)
⨯ Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
<... loader={function} src=... onError=... className=... width=... height=... alt=...>
^^^^^^^^^^
at stringify (<anonymous>)
What I do I need to look for?
DirtyCajunRice | AppDir
well...
i cant see your file
try commenting out whats in the CheckBox component first
to make sure its not that
berkserbetOP
Doesn't seem to be that
Can I send the file?
DirtyCajunRice | AppDir
sure
berkserbetOP
DirtyCajunRice | AppDir
you have stuff that matches
ctrl+f for loader
thats where it is
berkserbetOP
Matches what?
DirtyCajunRice | AppDir
matches the error
the error is because you are using an image loader
berkserbetOP
Ah
Yeah I am
Should I make it client
DirtyCajunRice | AppDir
if you want to keep doing that, you need to wrap your image component in a client component file
berkserbetOP
Yeah I have to I think
Lots of images in my app
On it
I assume I need to move the loader out too
DirtyCajunRice | AppDir
you would move it with the component
ImageKitImage
or whatever
berkserbetOP
I'm not quite sure how to wrap it
I created
image.tsx
So far have this in it:
'use client'
const imageKitLoader = ({
src,
width,
quality,
}: {
src: string;
width: number;
quality?: number;
}) => {
if (src[0] === '/') src = src.slice(1);
const params = [`w-${width}`];
if (quality) {
params.push(`q-${quality}`);
}
const paramsString = params.join(',');
var urlEndpoint = 'https://ik.imagekit.io/y7y0qqdyl';
if (urlEndpoint[urlEndpoint.length - 1] === '/')
urlEndpoint = urlEndpoint.substring(0, urlEndpoint.length - 1);
if (src.includes("redd.it")) {
return `${urlEndpoint}/r/${src.split("/").pop()}?tr=${paramsString}`;
} else if (src.includes("postimg")) {
return `${urlEndpoint}/postimg/${src.split(".cc/").pop()}?tr=${paramsString}`;
} else if (src.includes("imgur")) {
return `${urlEndpoint}/imgur/${src.split("/").pop()}?tr=${paramsString}`;
} else if (src.includes("https://ik.imagekit.io/y7y0qqdyl/")) {
return `${src}?tr=${paramsString}`;
} else {
return `https://ik.imagekit.io/y7y0qqdyl/Image_not_available_wide.webp?tr=${paramsString}`
}
};
export const CustomImage = (
DirtyCajunRice | AppDir
export const CustomImage = (props: ComponentProps<typeof Image>) => {
return <Image {...props} loader={imageKitLoader} />
}
berkserbetOP
Great
Now I am getting a different issue with the
onError={onImageError}
I think I know how to fix
It doesn't like the
onError
: <CustomImage
src={image}
onError={onImageError}
className="w-full rounded-box object-contain"
width={480}
height={480}
alt="Item Picture"
/>
I move the function to the image.tsx
But it didn't work
Fixed!
Now app works
Weird error on left:
Error: Hydration failed because the initial UI does not match what was rendered on the server.
DirtyCajunRice | AppDir
ignore for now
something to address later
berkserbetOP
Cool
So now I gotta update query params
DirtyCajunRice | AppDir
yes
but first click a button
and check the logs
so you know what the formData looks like
berkserbetOP
Form Data: FormData { [Symbol(state)]: [ { name: 'brands', value: 'Adidas' } ] }
Form Data: FormData {
[Symbol(state)]: [
{ name: 'conditions', value: 'New With Tags' },
{ name: 'brands', value: 'Adidas' }
]
}
Form Data: FormData {
[Symbol(state)]: [
{ name: 'conditions', value: 'New With Tags' },
{ name: 'conditions', value: 'Used' },
{ name: 'brands', value: 'Adidas' }
]
}
So it keeps updating
With new objects of clicks
I get it, works great
DirtyCajunRice | AppDir
😄
ok. ill leave you to sort the last bit, as you should have it under control. if you get super stuck though you can ping
berkserbetOP
For sure, thank you! it's just redirect right?
DirtyCajunRice | AppDir
yep!
berkserbetOP
Awesome, will try now. Thank you so much for all the help. Is there anything I can do? I would love to pay for your lunch tomorrow
DirtyCajunRice | AppDir
Nope no need
berkserbetOP
If you do share, where are you based?
DirtyCajunRice | AppDir
Austin TX
berkserbetOP
Oh cool
I'm SF
berkserbetOP
What is
Symbol(state)
?I am trying to understand how to read it:
{ [Symbol(state)]: [ { name: 'sizes', value: '0' } ] }
Something about [FormData Iterator], not sure I undestand that
DirtyCajunRice | AppDir
yeah its not a simple object
it has methods
you can do .get()
and .getAll() or something like that
berkserbetOP
Do I need to move my checkHandler?
But then the initialparams wont be accessible
function checkHandler(checkBoxType: string, checkBoxValue: any) {
if (typeof window !== "undefined") {
if (checkBoxType == 'r' && typeof initialParams.r !== "undefined") {
if (initialParams.r.split(",").includes(checkBoxValue['subreddit_name'])) { return true }
return false
}
if (checkBoxType == 'genders' && typeof initialParams.genders !== "undefined") {
if (initialParams.genders.split(",").includes(checkBoxValue['gender'])) { return true }
return false
}
if (checkBoxType == 'sizes' && typeof initialParams.sizes !== "undefined") {
if (initialParams.sizes.split(",").includes(checkBoxValue['size'])) { return true }
return false
}
if (checkBoxType == 'conditions' && typeof initialParams.conditions !== "undefined") {
if (initialParams.conditions.split(",").includes(checkBoxValue['condition'])) { return true }
return false
}
if (checkBoxType == 'countries' && typeof initialParams.countries !== "undefined") {
if (initialParams.countries.split(",").includes(checkBoxValue['country'])) { return true }
return false
}
if (checkBoxType == 'brands' && typeof initialParams.brands !== "undefined") {
if (initialParams.brands.split(",").includes(checkBoxValue['brand'])) { return true }
return false
}
return false
}
return false;
}
DirtyCajunRice | AppDir
you dont need that anymore
all that data is in the object
berkserbetOP
But they don't stay selected
DirtyCajunRice | AppDir
you need to set the value
from your params
berkserbetOP
Oh so instead of checkHandler
But why wouldnt checkHandler work
DirtyCajunRice | AppDir
because its client side only
which is also why you have hydration errors
berkserbetOP
Should I mark it as client
DirtyCajunRice | AppDir
no
doesnt need to be client
theres no reason to check window at all in that function
berkserbetOP
So how do I mark which checkboxes are checked?
DirtyCajunRice | AppDir
fixing your func. sec.
berkserbetOP
ðŸ™
DirtyCajunRice | AppDir
function checkHandler(param: ProductSearchParam, values: any) {
if (!initialParams[param] || param === 'pages') {
return false;
}
const options = initialParams[param].split(',')
switch (param) {
case "r":
return options.includes(values['subreddit_name']);
case "genders":
return options.includes(values['gender']);
case "sizes":
return options.includes(values['size']);
case "conditions":
return options.includes(values['condition']);
case "countries":
return options.includes(values['country']);
case "brands":
return options.includes(values['brand']);
default:
return false;
}
}
berkserbetOP
Amazing
DirtyCajunRice | AppDir
i have no idea what
values
isbut... i assume you do
berkserbetOP
yeah
It's like sizes: M
value is M
Clothing
Couple errors
Property 'split' does not exist on type 'string | number'.
Property 'split' does not exist on type 'number'
Pages doesn't go to this one ever
Also
Cannot find name 'checkBoxType'. Did you mean 'CheckBox'?ts(2552)
checkBoxType is param now I think
DirtyCajunRice | AppDir
oh. do a type check safey on the first if
fixed above
berkserbetOP
Amazing, now where is checkHandler called, same place?
DirtyCajunRice | AppDir
well, before you had it on default checked
that would be an uncontrolled component
you now want controlled component
so change it to checked instead of defaultChecked
berkserbetOP
Seems to be working for that
When I manually enter a param its checked
DirtyCajunRice | AppDir
berkserbetOP
Now just the submitForm 😄
DirtyCajunRice | AppDir
parse the formData, build your url
redirect to it
ezpz
berkserbetOP
Yeah how do I parse Symbol(state)
DirtyCajunRice | AppDir
you dont need to
berkserbetOP
I just need to iterate and append text
How do I get the object to iterate throuch
DirtyCajunRice | AppDir
go back to your types/product.ts
export your array
see all this stuff that ended up being super useful later
😄
berkserbetOP
const allSearchParams = ['r', 'search', 'genders', 'sizes', 'conditions', 'countries', 'brands', 'pages'] as const;
export type ProductSearchParam = typeof allSearchParams[number];
export type ProductSearchParams = Record<ProductSearchParam, string | string[]>;
export interface Product {
title: string;
brands: string[];
actions: string[];
status: string | null;
genders: string[];
sizes: string[];
categories: string[];
price: number | null;
currency: string | null;
countries: string[];
images: string[];
conditions: string[];
colors: string[];
shipping_info: string | null;
search_keywords: string | null;
timestamp: number; // in seconds
source: string;
reddit_subreddit: string;
reddit_author: string;
reddit_post_id: string | null;
reddit_thread_id?: string | null;
reddit_comment_id: string | null;
listing_number: number;
}
DirtyCajunRice | AppDir
allSearchParams
export it
berkserbetOP
Do I export products = Product[]
Product array
DirtyCajunRice | AppDir
nah thats just lazy lol
berkserbetOP
Then how? 🙂
DirtyCajunRice | AppDir
^
export the array
berkserbetOP
Ah
Alright
And imported
Isn't there like a one line version to loop and create the query text
I would loop, over list and keep manually appending with
queryParams.set(key, value.join(','));
Loop over the formData
Form Data: FormData {
[Symbol(state)]: [
{ name: 'sizes', value: '4' },
{ name: 'sizes', value: '5' },
{ name: 'sizes', value: '6â€' }
]
}
I am trying to grab this part:
[
{ name: 'sizes', value: '4' },
{ name: 'sizes', value: '5' },
{ name: 'sizes', value: '6â€' }
]
DirtyCajunRice | AppDir
then in your server action you can do something along the lines of
const submitForm = async (formData: FormData) => {
const newSearchParams = new URLSearchParams();
allSearchParams.forEach(p => {
const opts = formData.getAll(p)
if (opts.length) {
newSearchParams.append(p, opts.join(","));
}
})
redirect(`/current/url/path?${newSearchParams.toString()}`)
}
berkserbetOP
Nice!
Added 'use client'
DirtyCajunRice | AppDir
...
you mean 'use server'
berkserbetOP
Yeah sorry
DirtyCajunRice | AppDir
ok phew
berkserbetOP
const submitForm = async (formData: FormData) => {
'use server';
const newSearchParams = new URLSearchParams();
allSearchParams.forEach(p => {
const opts = formData.getAll(p)
if (opts.length) {
newSearchParams.append(p, opts.join(","));
}
})
redirect(`/current/url/path?${newSearchParams.toString()}`)
}
But still getting
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
<form action={function} children=...>
How do you color like this?
DirtyCajunRice | AppDir
add ts after the 3 backticks
```ts
berkserbetOP
oh cool
DirtyCajunRice | AppDir
that doesnt really make any sense
you can move it to a server actions file
then move the 'use server' to the top of the file
and import it
see if it still does it
berkserbetOP
Sure
This doesn't throw an error:
async function submitForm(formData: FormData) {
'use server';
const newSearchParams = new URLSearchParams();
allSearchParams.forEach(p => {
const opts = formData.getAll(p)
if (opts.length) {
newSearchParams.append(p, opts.join(","));
}
})
redirect(`/current/url/path?${newSearchParams.toString()}`)
}
DirtyCajunRice | AppDir
they hate my es6
berkserbetOP
But now no matter what I click my params are
Initial Params: { r: 'url', pages: NaN }
r=url
DirtyCajunRice | AppDir
did you change the redirect to your actual path
berkserbetOP
🙂
DirtyCajunRice | AppDir
haha
berkserbetOP
Does redirect allow you to not scroll
DirtyCajunRice | AppDir
i... i actually have no idea
berkserbetOP
The app is acting weird
Like the seach bar moves around after every button click
Then goes to it's normal place
DirtyCajunRice | AppDir
that would be your client components not having proper css
which is a whole different issue
😄
berkserbetOP
But it gets to the right place
DirtyCajunRice | AppDir
prerender vs hydration
prerender is only server known stuff
hydration is client side and final state
berkserbetOP
Are the hydration errors easy to fix?
DirtyCajunRice | AppDir
once you understand what causes hydration errors in the first place, yes absolutely.
berkserbetOP
So how can I get my app to work as well as it used to now?
DirtyCajunRice | AppDir
you have now acended
berkserbetOP
🙂
Seems I need to learn a few more things
DirtyCajunRice | AppDir
this will - for better or worse - make you understand the importance of having the correct data on the server so that it matches and is not modified by the client
search around your code and check for anything that modifies how things look via a useEffect
thats your first "oh shit i see"
berkserbetOP
useEffect adds animation?
DirtyCajunRice | AppDir
useEffect adds client javascript logic
of any kind
berkserbetOP
But why do I want that?
Like my searchbar only has:
flex items-center justify-center
Why does it move around so much
DirtyCajunRice | AppDir
couple things can cause it.
1. you have client css-in-js somewhere.
2. you have values not being populated until client render
3. you have css that does not have stringent bounds causing it to grow or shrink until the item that changes where it flows to appears.
1. you have client css-in-js somewhere.
2. you have values not being populated until client render
3. you have css that does not have stringent bounds causing it to grow or shrink until the item that changes where it flows to appears.
berkserbetOP
css-in-js is a function that returns an item with css?
DirtyCajunRice | AppDir
not necessarily. Its css that is defined dynamically via javascript
vs static classes / properties
berkserbetOP
My search looks like this:
'use client'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
const SearchContainer = () => {
// NextJs Navigation
const router = useRouter()
const searchParams = useSearchParams()
const pathname = usePathname()
let queryParams: URLSearchParams;
// React States
const [search, setSearch] = useState(searchParams.get('search') || '')
// Handle Submitting
const handleSubmit = (e: any) => {
e.preventDefault()
if (typeof window !== "undefined") {
queryParams = new URLSearchParams(window.location.search);
}
queryParams.delete("pages");
if (search === '') {
queryParams.delete('search');
} else {
queryParams.set('search', search);
}
const path = window.location.pathname + "?" + queryParams.toString();
router.push(path);
}
const removeSearch = (e: any) => {
router.push(pathname)
return
}
return (
<form className='flex items-center justify-center'>
<input className='border border-gray-400 rounded-md p-1' placeholder='Search' onChange={(e) => setSearch(e.target.value)} defaultValue={search}/>
<button className='border border-gray-400 rounded-md p-1 m-2 hover:bg-blue-100' type='submit' onClick={handleSubmit}>ðŸ”</button>
<button type='submit' onClick={removeSearch}>✖ï¸</button>
</form>
)
}
export default SearchContainer
Does it just take time to run this one the client side each time
DirtyCajunRice | AppDir
record what its doing. its anyones guess without being able to visualize the CLS
berkserbetOP
CLS is Cumulative Layout Shift?
DirtyCajunRice | AppDir
Exactly 🙂
berkserbetOP
For the button that updates the pages number, do I need to create a new button component like the checkbox?
DirtyCajunRice | AppDir
no
that one you just remove onClick
and instead use type="submit" action={}
(give it its own action so it can do the unique thing)
berkserbetOP
I use to use:
function handlePageClick() {
if (typeof window !== "undefined") {
queryParams = new URLSearchParams(window.location.search);
}
initialParams.pages = parseInt(queryParams.get("pages") || "1");
let nextPage: number = initialParams.pages + 1;
queryParams.set("pages", nextPage.toString());
const path = window.location.pathname + "?" + queryParams.toString();
redirect(path);
}
Is action a function?
DirtyCajunRice | AppDir
action is a server action function
like the form one we made
berkserbetOP
To test this I created:
The button I made:
But now my button doesn't appear. And action shows this error:
async function updatePage() {
'use server';
console.log("Update Page.")
}
The button I made:
<button className="btn btn-accent btn-outline btn-lg" type="submit" action={updatePage()}>Load More...</button>
But now my button doesn't appear. And action shows this error:
Type '{ children: string; className: string; type: "submit"; action: Promise<void>; }' is not assignable to type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'.
Property 'action' does not exist on type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'
DirtyCajunRice | AppDir
you dont add the () for actions
you are passing the definition of the function, not the function execution
berkserbetOP
So like this:
action={updatePage}
It gives the same error
DirtyCajunRice | AppDir
oh i forgot. when on a button its formAction not action
berkserbetOP
Cool!
berkserbetOP
When I manually add a pages query param it just gets deleted
I tried this:
async function updatePage(formData: FormData) {
'use server';
console.log("Update Page.")
console.log("Form Data:", formData)
const newSearchParams = new URLSearchParams();
allSearchParams.forEach(p => {
const opts = formData.getAll(p)
if (opts.length) {
console.log("Form Data:", p, opts.join(","));
newSearchParams.append(p, opts.join(","));
}
})
newSearchParams.set("pages", (parseInt(newSearchParams.get("pages") || "1") + 1).toString())
redirect(`?${newSearchParams.toString()}`)
}
But pages is never present in allSearchParams
DirtyCajunRice | AppDir
mmmm
what do you mean when you manually add it gets delete
berkserbetOP
I add ?pages=2
Then I choose a filter, then it's lost
DirtyCajunRice | AppDir
there it is
you left critical info out 😉
in the other action we are ignoring pages
you need to now handle pages separately
post that other action
berkserbetOP
This one?
async function submitForm(formData: FormData) {
'use server';
const newSearchParams = new URLSearchParams();
allSearchParams.forEach(p => {
const opts = formData.getAll(p)
if (opts.length) {
console.log("Form Data:", p, opts.join(","));
newSearchParams.append(p, opts.join(","));
}
})
redirect(`?${newSearchParams.toString()}`)
}
DirtyCajunRice | AppDir
yeah. so…. the pages part is completely missed here
since there is no “input†for it
This one?
async function submitForm(formData: FormData) {
'use server';
const newSearchParams = new URLSearchParams();
allSearchParams.forEach(p => {
const opts = formData.getAll(p)
if (opts.length) {
console.log("Form Data:", p, opts.join(","));
newSearchParams.append(p, opts.join(","));
}
})
if (initialParams.pages) {
newSearchParams.append("pages", initialParams.pages.toString())
}
redirect(`?${newSearchParams.toString()}`)
}
see what i mean?
berkserbetOP
Yeah unique case
It is still overwritten when I click a checkbox
Actually I want it to be overwritten
DirtyCajunRice | AppDir
so… good… or bad… haha
berkserbetOP
Perfect
Do you know why my build might be failing?
Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
✓ Generating static pages (6/6)
> Export encountered errors on following paths:
/page: /
DirtyCajunRice | AppDir
not with just that info
berkserbetOP
This is all it gives me:
✓ Collecting page data
Generating static pages (4/6) [= ]
⨯ useSearchParams() should be wrapped in a suspense boundary at page "/". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
✓ Generating static pages (6/6)
> Export encountered errors on following paths:
/page: /
DirtyCajunRice | AppDir
may seem like nothing to you… but that is very helpful info
berkserbetOP
🙂
useSearchParams
I see
DirtyCajunRice | AppDir
😉
berkserbetOP
Part of this component
'use client'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
const SearchContainer = () => {
// NextJs Navigation
const router = useRouter()
const searchParams = useSearchParams()
const pathname = usePathname()
let queryParams: URLSearchParams;
// React States
const [search, setSearch] = useState(searchParams.get('search') || '')
// Handle Submitting
const handleSubmit = (e: any) => {
e.preventDefault()
if (typeof window !== "undefined") {
queryParams = new URLSearchParams(window.location.search);
}
queryParams.delete("pages");
if (search === '') {
queryParams.delete('search');
} else {
queryParams.set('search', search);
}
const path = window.location.pathname + "?" + queryParams.toString();
router.push(path);
}
const removeSearch = (e: any) => {
router.push(pathname)
return
}
return (
<form className='flex items-center justify-center'>
<input className='border border-gray-400 rounded-md p-1' placeholder='Search' onChange={(e) => setSearch(e.target.value)} defaultValue={search}/>
<button className='border border-gray-400 rounded-md p-1 m-2 hover:bg-blue-100' type='submit' onClick={handleSubmit}>ðŸ”</button>
<button type='submit' onClick={removeSearch}>✖ï¸</button>
</form>
)
}
export default SearchContainer
DirtyCajunRice | AppDir
show where you are using that
berkserbetOP
I call it in ProductCards.tsx
DirtyCajunRice | AppDir
wrap it in a <Suspense></Suspense>
berkserbetOP
In the return
DirtyCajunRice | AppDir
but you now know you can probably convert that to a server component in the future
berkserbetOP
Fixed! Thank you
Yeah, but I am not sure how to deal with something like a searchbar yet
DirtyCajunRice | AppDir
sokay. something to use to learn in your own. gotta have goals
berkserbetOP
Yup 🙂
Found one last important issue
My search query is deleted when checkbox is clicked
I will try to solve it like the page example
This doesn't work unfortunately:
async function submitForm(formData: FormData) {
'use server';
const newSearchParams = new URLSearchParams();
allSearchParams.forEach(p => {
const opts = formData.getAll(p)
if (opts.length) {
console.log("Form Data:", p, opts.join(","));
newSearchParams.append(p, opts.join(","));
}
})
if (initialParams.search) {
newSearchParams.append("search", initialParams.search.toString())
}
redirect(`?${newSearchParams.toString()}`)
}
Worked without tostring()
DirtyCajunRice | AppDir
that one isnt a number so that would make sense haha
berkserbetOP
Yup
Does it have to rerender the page every time I click a checkbox? I want to make it so the user can just keep clicking
DirtyCajunRice | AppDir
as a server action yes, but it should not be noticeable in production
berkserbetOP
But the dropdown closes
DirtyCajunRice | AppDir
whos fault is that ðŸ˜
manage the state of the dropdown
you can do it in another url param, or extract that to a client component
berkserbetOP
I guess so, but this stuff just worked on the client side
DirtyCajunRice | AppDir
is the ui “just working†any good if the result is that it doesnt do shit? (hence the beginning of this thread)
berkserbetOP
Thats true
Thanks for all the help today, will likely come back for more soon 😄
DirtyCajunRice | AppDir
it may seem like more work at first, but you are getting granular control over your ui
np man. im gonna pass out soon its late
berkserbetOP
So I have one last major issue to tackle. Everything works fine in dev, but in build none of my query parameters do anything. It's like they don't exist and they delete existing query params each time
Also, I was thinking - since I serve users a bunch of images each time, won't I have high bandwidth costs when all of those are server side?
I see a "ReferenceError: window is not defined" in the logs
ReferenceError: window is not defined
at n (/Users/berkserbetcioglu/Code/storefront/.next/server/app/[...r]/page.js:1:2996)
at nM (/Users/berkserbetcioglu/Code/storefront/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:12:47419)
at nN (/Users/berkserbetcioglu/Code/storefront/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:12:64674)
at nI (/Users/berkserbetcioglu/Code/storefront/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:12:46806)
at nM (/Users/berkserbetcioglu/Code/storefront/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:12:47570)
at nM (/Users/berkserbetcioglu/Code/storefront/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:12:61663)
at nN (/Users/berkserbetcioglu/Code/storefront/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:12:64674)
at nB (/Users/berkserbetcioglu/Code/storefront/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:12:67657)
at nF (/Users/berkserbetcioglu/Code/storefront/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:12:66824)
at nN (/Users/berkserbetcioglu/Code/storefront/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:12:64990)
berkserbetOP
Marking this as done to move to a new thread
berkserbetOP
Thanks for the help!
Answer