Next.js Discord

Discord Forum

Query params update but app doesn't (sometimes)

Answered
berkserbet posted this in #help-forum
Open in Discord
Avatar
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.
Answered by berkserbet
Thanks for the help!
View full answer

898 Replies

Avatar
a link to your site isnt relevant. Please post code showing where you get the params, and where you consume them
Avatar
Sure, I just didn't want to post a ton of code. Thank you
Avatar
It's just one page.

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
Avatar
um
so…
Avatar
I was about to share the last file
Avatar
first off
Avatar
Do you already see an issue?
Avatar
there are some silly coding habits im gonna point out
so you can improve
Avatar
Please do! I'm new to this
Avatar
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} />
Avatar
I think I get what you're saying
Avatar
secondly
Avatar
Let me quickly try that
Avatar
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
Avatar
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
Avatar
well… you can do it that way too… but you dont need to
Avatar
Can I access search params directly from components without passing it around?
Avatar
  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
Avatar
Sorry I'm a bit confused - what's the first thing I need to do from my existing codebase?
Avatar
interface Props {
  searchParams: ProductSearchParams
}

export default function Home({searchParams}: Props)
copy that type into your product browser file
Avatar
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)
Avatar
yeah
im on mobile
Avatar
Errors gone
Avatar
show me your product browser page again now updated
Avatar
I haven't been able to het it to work yet, trying to understand the page.tsx changes
Avatar
nono
ignore page.tsx
ignore getting it working
just show it
its 3 steps
Avatar
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
Avatar
nice. delete Props
Avatar
Cool, app still works
Avatar
change the : Props in the component to ProductSearchParams
remove all the selected_ prefix nonsense
Avatar
Then what do I pass to ProductCards?
Avatar
the props
Avatar
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
Avatar
just named properly
nono
leave the props
just remove the prefix
you added a selected_ prefix for no reason
to all the keys
Avatar
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
Avatar
yes exactly
and when we are done you can do the same thing to ProductCards
but 1 thing at a time
Avatar
Seeing a lot of
Type 'string | string[]' is not assignable to type 'string'.
  Type 'string[]' is not assignable to type 'string'.
Avatar
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
Avatar
Done
import { ProductSearchParams } from './ProductBrowser';
Avatar
make your searchParams: ProductSearchParams
in your page props def
Avatar
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>
  );
}
Avatar
then where you have your ProductBrowser in page.tsx just make it <ProductBrowser {...searchParams } />
nono
Avatar
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)
Avatar
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
Avatar
Done!
Works
No errors
Avatar
see how the page has no errors?
magic 😉
now
Avatar
Magic
Avatar
now before we move on
where are you going to host this thing
Avatar
Vercel I host it on
Avatar
ok. then your fs read is silly and wasteful
and the component doesnt need async nor suspense
Avatar
🙂
Avatar
just import the json
Avatar
The json can get big
Avatar
doesnt matter
Avatar
And changes often
Cool
Avatar
it doesnt change without redeploying
Avatar
Correct
Avatar
so its literally the same thing either way
Avatar
Trying
import products from '../json/listings.json'?
Avatar
you use cwd
so 1 dot
Avatar
VScode autofilled
Avatar
1 dot == same folder 2 dots == parent folder
vscode is caca
😂
Avatar
Got it
🙂
Avatar
ok so remove async and just pass that along
also remove suspense from parent as the compnent isnt async so it doesnt do anything
Avatar
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>
  );
}
Avatar
so clean
Avatar
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
Avatar
less clean haha
Avatar
I don't even need the productbrowser now I think
Avatar
you do not 🙂
Avatar
I can just pass directly
Avatar
so update your Product card params to use the same logic you used for the browser
Avatar
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'
Avatar
do you wanna make the ProductSearchParams cleaner?
how comfortable are you with typescript
Avatar
I do
Not super
Up to you
Avatar
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
Avatar
Amazing!
Avatar
if any of that is confusing, ask
copying without understanding is the worst sin lol
Avatar
What is "as const"
Avatar
coding on phone is such a pain lol
Avatar
What is it for I mean
Avatar
means its read only and cant be edited
so its “static” basically
you cant infer a type from a mutable array
Avatar
What is allSearchParams?
Avatar
only a static (immutable) arrat
Avatar
I don't have that variable
Avatar
just a list of the keys we will use, separated for organization purposes
its the first thing
oh
whoops
all should be product
Avatar
Got it
Avatar
again. phone -,/
so the type is saying “this type is all the keys of this array”
Avatar
So what does the second line do type ProductSearchParam = typeof allSearchParams[number]
Avatar
what i just said
Avatar
Is it looping somehow
Avatar
knew you would ask haha
its the typescript way to say “all the items in the array as type options”
Avatar
Sorry I don't get it
" as type options"
This part
Avatar
when you hover over it
does it show you the options?
Avatar
Image
Like that?
Avatar
yes but hover over the left side
the type itself
Avatar
Image
This one?
Avatar
see what it did?
it plucked all the items out of the array and made them “this or this or this” as a type
Avatar
But how does it know the type?
We haven't given any
Avatar
we did
we said it is the items in the array
its a new type
of those strings
Avatar
Got it
Avatar
“just typescript things”
😂
Avatar
🙂
Avatar
and you know what a Record is?
Avatar
And I get the third line
Avatar
ok cool
Avatar
I don't actually
What is Record
I assumed but not sure
Avatar
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
Avatar
Which is string or array of strings
Right?
Avatar
thats the value type
the key type is the array of productSearchParams
so string…. but “specific”
Avatar
Very cool
Avatar
so record says “for each item in the key type, the value type is”
Avatar
The next thing I did was import the json
Avatar
yes.
do you have a type for your json?
Avatar
import listings from './json/listings.json'

But giving error
Avatar
what error
screenshot your folder/file window
Avatar
'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
Avatar
^
Avatar
Image
I think it's in the right spot
Avatar
wait
is app and json at the same level?
Avatar
No
Its a level up 🙂
I see
..
Avatar
also
do you have type aliases in tsconfig?
it would help you
not type
path aliases
Avatar
{
  "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
Avatar
yeah
so you can import smarter
Avatar
"baseUrl": "./src",
"paths": {
    "@modules/*": ["rest/modules/*"],
    "@services/*": ["services/*"]
}
Found this online
Like that?
Avatar
nono
yours is good
leave it
Avatar
Cool
No more error in the import
My json is a list of objects which I define in Cards
Avatar
import Listings from “@/json/listings.json"
you can import this way
with the aliases
much prettier imo anyway haha
Avatar
Yup!
No error
Avatar
whats your listings json look like
just show me like the first 20-30 lines
Avatar
[
    {
        "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
Avatar
wheres that at
Avatar
In ProductCards.tsx
Avatar
make yourself a types folder
so you keep things organized
Avatar
But that is the only type
No other one
Avatar
is it? we have already made another type
😉
Avatar
allSearchParams?
Yeah
Avatar
mhm
and you will make more.
Avatar
Where should I put the types folder?
Avatar
same level as json
outside app
Avatar
Created it
Avatar
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
Avatar
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
Avatar
dont forget to export Product
Avatar
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;
}
Avatar
ok. very nice
Avatar
Now trying import
Avatar
@/types/products
import { Product } from
something like that
Avatar
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?
Avatar
good. if you couldnt figure that one out i might have given up on you
Avatar
🙂
Avatar
means an export in the file
since you can export many things
Avatar
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
Image
Avatar
does it need to be client?
Avatar
It has a bunch of product listings that update based on filters
So I think so
Avatar
arent your filters in the url…?
Avatar
Yeah
Avatar
so…
Avatar
Ah
Avatar
not client
😉
Avatar
App broke, but I think I'm on the right track 🙂
I use useRouter()
Avatar
for what
to update the url?
Avatar
Yeah
router.push(path, { scroll: false });
Avatar
instead use redirect()
same thing and no use
Avatar
Cool
import redirect from 'nextjs-redirect' this one?
Avatar
nono
nextjs/navigation iiirc
this
helpers gotta know the docs
or we would be lost
Avatar
Updated
Avatar
whats your productcards look like now
Avatar
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.
Avatar
yeah you will change those to a form
with server action
Avatar
Cool
Avatar
pause for a sec
lets clean up the search params
Avatar
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
Avatar
right
in this case tho, we wanna sanitize your props
Avatar
const ProductCards = ({r, search, genders, sizes, conditions, countries, brands, pages}: ProductSearchParams) => {
I did this
Avatar
so lets make a helper function above ProductCards
Avatar
function sanitize(params: ProductSearchParams) {
  return params
}
?
With like if's in there
Avatar
we can be fancy though
sec
Avatar
I like that
Avatar
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?
Avatar
For some of them I just use comma seperated strings
Then split by the comma to form an array
Is that ok?
Avatar
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
Avatar
I can have longish ones
Avatar
thats totally fine
Avatar
I don't quite understand
Avatar
so lets work from inside to outside
you know what Object.entries does?
Avatar
I don't
Avatar
takes an object, and turns it into an array
each item in the array is an array of [key, value]
Avatar
Got it
Avatar
so it would be like
[["pages", "2"], ["r", "poo"]]
Avatar
Perfect
Avatar
we then map those values
Avatar
Yup
Avatar
and keep the key but check the value
Avatar
If array then v[0]
Why is that?
Avatar
yeah. basically say “if you manually put more than 1 key on my site i dont care i want the first one”
Avatar
Cool
Avatar
never trust the user
Avatar
stringsOnly has an array of arrays now
Avatar
or a fart after age 30
jkjk
Avatar
With the first val
Of each key
Avatar
and each value as a string
Avatar
Yup, then the last one is noing number conversion - not sure how
Avatar
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
Avatar
Oh cool
So little code to accomplish that
Avatar
you could also just do
stringsOnly.pages = Number(stringsOnly.pages)
but i like clean short code
Avatar
Yeah for sure
Where do I call sanitize?
Avatar
so heres the fun part
your object spread you have in ProductCards definition
move that INSIDE the component
Avatar
🙂 crazy
Avatar
as like
const {put it here} = sanitize(props)
Avatar
Got it
Avatar
and your func should just be (props: ProductSearchParams
then you get the same functionality, but clean
Avatar
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)
Avatar
nono
inside {}
Avatar
Oh
Avatar
its still an object
Avatar
Like this?
const ProductCards = (props: ProductSearchParams) => {
  const { r, search, genders, sizes, conditions, countries, brands, pages } = sanitize(props)
Avatar
nice
no errors right?
Avatar
I have errors
Property 'r' does not exist on type '{ pages: number; }'.ts(2339)
Avatar
i have emotional errors
Avatar
🙂
Avatar
hahahaha
Avatar
I appreciate you so greatly
Avatar
show me your sanitize func
Avatar
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)};
}
Avatar
ok. we will do this the verbose way
go to your types
and export ProductSearchParam
so we have access to it
Avatar
I do that
export type ProductSearchParams = Record<ProductSearchParam, string | string[]>;
Avatar
nope
that is a different type
🙂
hint: no s at the end
Avatar
Ah
export type ProductSearchParam = typeof allSearchParams[number];
Avatar
yes
ok so now at the end of your sanitize where you return the object
Avatar
I imported it
Current: return {... stringsOnly, pages: Number(stringsOnly.pages)};
Avatar
add this at the end
as Record<Exclude<ProductSearchParam, 'pages'>, string> & { pages: number };
Avatar
Addind
Avatar
im ready for the questions
😂
Avatar
I get it
Avatar
oh? very good
Avatar
Exclude pages mark string
Pages number
Avatar
my man
ok. is the other error now gone?
Avatar
Why couldn't we use ProductSearchParams
Error gone
Avatar
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
😅
Avatar
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
Avatar
are you using those variable names anywhere else?
inside that component
Avatar
I likely am
Avatar
if you want to be safe
Avatar
I will update
Avatar
you can not destructure it
and save as like. initialParams
then get them via initialParams.whatever
Avatar
Ah
Yeah sounds good
const { initialParams } = sanitize(props)
Like that?
  const initialParams = sanitize(props)
Like that
Avatar
yeah exactly
Avatar
Cool, worked
Updating vars in file
Avatar
once you change your router.push to redirects, your problem should be gone
Avatar
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)
Avatar
ah yeah
if the key isnt there, you dont put null
you put ?:
Avatar
I either put an empty array if array type or null
Avatar
Types of property 'reddit_thread_id' are incompatible.
        Type 'undefined' is not assignable to type 'string | null'.ts(2345)
Avatar
Hmm
Avatar
according to this, you have an undefined somewhere in that json, but dont have it in your type
Avatar
Let me check
Avatar
show me your Project interface
Avatar
--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;
}
Avatar
this is what i meant
change your reddit_thread_id
to add a ? before:
Avatar
?reddit_thread_id: string | null;
Like that?
Avatar
reddit_thread_id?: string | null;
Avatar
Done
Avatar
error gone?
Avatar
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[] is
But shipping_info is null often
Avatar
basically, you need to match up your json with your type
so go through those 1 by 1 and fix
or fix the type
Avatar
But why is it broken now?
Avatar
for that one its adding [] to the shipping_info
Avatar
It's the same json
Avatar
yeah your json is wonky haha
doesnt adhere like you thought it did
Avatar
Got it, fixing
Done!
🙂
Avatar
so you are set?
Avatar
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.
Avatar
On it
Avatar
Do functions that do redirects need to be client side?
Avatar
nope
redirect() is designed to work in server actions
Avatar
Cool, I'm just trying to figure out how to turn my 6 <input>'s into <form>'s
I think
Avatar
dont
turn all into 1 form
Avatar
Hmm
Avatar
then you can build the url the same way regardless
Avatar
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)}
                  />
Avatar
find the closest parent that contains all the inputs
and add a <form> to wrap it
Avatar
But I have 6 seperate dropdowns
Avatar
yes
Avatar
Each dropdown has on input for many options
Avatar
i understand 🤣 Its fine
trust.
Avatar
I trust 🙂
Doing it now
It will basically be
return (
  <form>
Is that ok?
Avatar
thats fine
what is your handleClick doing btw
just rebuilding the url?
Avatar
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
Avatar
sokay. we can clean it up.
leave the function, but remove the onClick for all of them
Avatar
Sure
Done
Avatar
one question... does the user need to submit at all?
or are you immediately changing the value
Avatar
Immediate change
I also have a "Load More..." button that increments page
All others are checkboxes
Avatar
ok. we are gonna make input (JUST INPUT) a client component
make a new file
like... checkbox.tsx
Avatar
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
Avatar
yeah thats fine
Avatar
Awesome
What do I put in the file?
Avatar
'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
Avatar
Amazing thanks!
Can't find InputHTMLAttributes and DetailedHTMLProps
so trying to figure out import
Avatar
import { DetailedHTMLProps, InputHTMLAttributes } from "react";
Avatar
Awesome worked
Avatar
should be a drop-in replacement for your <input>
Avatar
Testing!
Avatar
well you still need to make your server action
Avatar
So I replace the input with CheckBox?
Avatar
yes
keep all the props the same
just change the tag
Avatar
Yup
The onclick is what I need to do now
Avatar
  function submitForm(formData: FormData) {
    'use server';
    console.log("Form Data:", formData);
  }
start with this
<form action={submitForm}>
Avatar
Where should I have form?
Avatar
^
Avatar
I see
Avatar
you are just adding the action prop
Avatar
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
Avatar
O.o
are you using an old version of next...?
Avatar
I didn't think so
Let me check
Next.js v13.5.6
Avatar
yeah old
14.1.0
Avatar
I'll update
It's because my node was older
Avatar
XD
Avatar
I have an issue when trying to update
Working on it
I seemingly have multiple versions
But next is pointing at an old one
Avatar
let me know when you sort that XD
Avatar
I think it worked now
Sorry about that
Avatar
np
multitasking
Avatar
Seeing:
× Server actions must be async functions
Should I make it
Avatar
yeah thats true
yes
Avatar
Is this wrong?
  async function submitForm(formData: FormData) {
    'use server';
    console.log("Form Data:", formData);
  }
Big red error on browser
Same error
Avatar
yeah thats fine
Avatar
Image
Avatar
thats an old error
doesnt show your change
stop and start the dev env
Avatar
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".
Avatar
did you leave onClick somewhere
Avatar
I had left the button
Commenting out
Still same error
Avatar
gotta find where its happening
Avatar
 ⨯ 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?
Avatar
well...
i cant see your file
try commenting out whats in the CheckBox component first
to make sure its not that
Avatar
Doesn't seem to be that
Can I send the file?
Avatar
sure
Avatar
Image
Avatar
you have stuff that matches
ctrl+f for loader
thats where it is
Avatar
Matches what?
Avatar
matches the error
the error is because you are using an image loader
Avatar
Ah
Yeah I am
Should I make it client
Avatar
if you want to keep doing that, you need to wrap your image component in a client component file
Avatar
Yeah I have to I think
Lots of images in my app
On it
I assume I need to move the loader out too
Avatar
you would move it with the component
ImageKitImage
or whatever
Avatar
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 = (
Avatar
export const CustomImage = (props: ComponentProps<typeof Image>) => {
  return <Image {...props} loader={imageKitLoader} />
}
Avatar
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.
Avatar
ignore for now
something to address later
Avatar
Cool
So now I gotta update query params
Avatar
yes
but first click a button
and check the logs
so you know what the formData looks like
Avatar
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
Avatar
😄
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
Avatar
For sure, thank you! it's just redirect right?
Avatar
yep!
Avatar
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
Avatar
Nope no need
Avatar
If you do share, where are you based?
Avatar
Austin TX
Avatar
Oh cool
I'm SF
Avatar
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
Avatar
yeah its not a simple object
it has methods
you can do .get()
and .getAll() or something like that
Avatar
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;
  }
Avatar
you dont need that anymore
all that data is in the object
Avatar
But they don't stay selected
Avatar
you need to set the value
from your params
Avatar
Oh so instead of checkHandler
But why wouldnt checkHandler work
Avatar
because its client side only
which is also why you have hydration errors
Avatar
Should I mark it as client
Avatar
no
doesnt need to be client
theres no reason to check window at all in that function
Avatar
So how do I mark which checkboxes are checked?
Avatar
fixing your func. sec.
Avatar
🙏
Avatar
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;
  }
}
Avatar
Amazing
Avatar
i have no idea what values is
but... i assume you do
Avatar
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
Avatar
oh. do a type check safey on the first if
fixed above
Avatar
Amazing, now where is checkHandler called, same place?
Avatar
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
Avatar
Seems to be working for that
When I manually enter a param its checked
Avatar
Now just the submitForm 😄
Avatar
parse the formData, build your url
redirect to it
ezpz
Avatar
Yeah how do I parse Symbol(state)
Avatar
you dont need to
Avatar
I just need to iterate and append text
How do I get the object to iterate throuch
Avatar
go back to your types/product.ts
export your array
see all this stuff that ended up being super useful later
😄
Avatar
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; }
Avatar
allSearchParams
export it
Avatar
Do I export products = Product[]
Product array
Avatar
nah thats just lazy lol
Avatar
Then how? 🙂
Avatar
^
export the array
Avatar
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”' }
  ]
Avatar
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()}`)
  }
Avatar
Nice!
Added 'use client'
Avatar
...
you mean 'use server'
Avatar
Yeah sorry
Avatar
ok phew
Avatar
  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?
Avatar
add ts after the 3 backticks
```ts
Avatar
oh cool
Avatar
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
Avatar
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()}`)
  }
Avatar
:pepeCry:
they hate my es6
Avatar
But now no matter what I click my params are Initial Params: { r: 'url', pages: NaN }
r=url
Avatar
did you change the redirect to your actual path
Avatar
🙂
Avatar
:facepalm:
haha
Avatar
Does redirect allow you to not scroll
Avatar
i... i actually have no idea
Avatar
The app is acting weird
Like the seach bar moves around after every button click
Then goes to it's normal place
Avatar
that would be your client components not having proper css
which is a whole different issue
😄
Avatar
But it gets to the right place
Avatar
prerender vs hydration
prerender is only server known stuff
hydration is client side and final state
Avatar
Are the hydration errors easy to fix?
Avatar
once you understand what causes hydration errors in the first place, yes absolutely.
Avatar
So how can I get my app to work as well as it used to now?
Avatar
you have now acended
Avatar
🙂
Seems I need to learn a few more things
Avatar
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"
Avatar
useEffect adds animation?
Avatar
useEffect adds client javascript logic
of any kind
Avatar
But why do I want that?
Like my searchbar only has: flex items-center justify-center
Why does it move around so much
Avatar
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.
Avatar
css-in-js is a function that returns an item with css?
Avatar
not necessarily. Its css that is defined dynamically via javascript
vs static classes / properties
Avatar
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
Avatar
record what its doing. its anyones guess without being able to visualize the CLS
Avatar
CLS is Cumulative Layout Shift?
Avatar
Exactly 🙂
Avatar
For the button that updates the pages number, do I need to create a new button component like the checkbox?
Avatar
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)
Avatar
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?
Avatar
action is a server action function
like the form one we made
Avatar
To test this I created:
  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>'
Image
Avatar
you dont add the () for actions
you are passing the definition of the function, not the function execution
Avatar
So like this: action={updatePage}
It gives the same error
Avatar
oh i forgot. when on a button its formAction not action
Avatar
Cool!
Avatar
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
Avatar
mmmm
what do you mean when you manually add it gets delete
Avatar
I add ?pages=2
Then I choose a filter, then it's lost
Avatar
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
Avatar
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()}`)
  }
Avatar
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?
Avatar
Yeah unique case
It is still overwritten when I click a checkbox
Actually I want it to be overwritten
Avatar
so… good… or bad… haha
Avatar
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: /
Avatar
not with just that info
Avatar
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: /
Avatar
may seem like nothing to you… but that is very helpful info
Avatar
🙂
useSearchParams
I see
Avatar
😉
Avatar
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
Avatar
show where you are using that
Avatar
I call it in ProductCards.tsx
Avatar
wrap it in a <Suspense></Suspense>
Avatar
In the return
Avatar
but you now know you can probably convert that to a server component in the future
Avatar
Fixed! Thank you
Yeah, but I am not sure how to deal with something like a searchbar yet
Avatar
sokay. something to use to learn in your own. gotta have goals
Avatar
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()
Avatar
that one isnt a number so that would make sense haha
Avatar
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
Avatar
as a server action yes, but it should not be noticeable in production
Avatar
But the dropdown closes
Avatar
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
Avatar
I guess so, but this stuff just worked on the client side
Avatar
is the ui “just working” any good if the result is that it doesnt do shit? (hence the beginning of this thread)
Avatar
Thats true
Thanks for all the help today, will likely come back for more soon 😄
Avatar
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
Avatar
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)
Avatar
Marking this as done to move to a new thread
Avatar
Thanks for the help!
Answer