metafizzy / isotope

:revolving_hearts: Filter & sort magical layouts
https://isotope.metafizzy.co
11.03k stars 1.42k forks source link

"window is not defined" when importing in next.js #1639

Open ilyador opened 1 year ago

ilyador commented 1 year ago

I am using isotope-layout on a next.js project (not sure if related) Whenever I refresh ht page that calls new Isotope I get this:

index.js?46cb:594 Uncaught ReferenceError: window is not defined
    at Object.<anonymous> (file:///.../node_modules/isotope-layout/js/isotope.js:59:4)

The strange thing is that when I comment out new Isotope and uncomment, it works because the hot reload of next.js only reloads part of the page.

Any ideas why this is happening?

thesublimeobject commented 1 year ago

@ilyador — Unfortunately I haven't used Isotope with Next.js, and it's been quite awhile since I used it with React at all. I checked my current webpack configuration with Isotope, and it looks like I removed the old "window" hack, but looking at an older site, I was using something like this...

{
        loader: 'imports-loader?define=>false&this=>window',
        test: /isotope\-|fizzy\-ui\-utils|desandro\-|masonry|outlayer|get\-size|doc\-ready|eventie|eventemitter/,
}

Now, again, this is an older site and that config doesn't work well with the current version of webpack. You might have to re-formulate it to something like:

{
    test: /isotope\-|fizzy\-ui\-utils|desandro\-|masonry|outlayer|get\-size|doc\-ready|eventie|eventemitter/,
    use: [{
        loader: 'imports-loader',
        options: {
            imports: {
                wrapper: {
                    thisArg: "window",
                    args: ["myVariable", "myOtherVariable"],
                },
            },
        }
    }]
}

I have no tested this, and don't often use the imports-loader, so I'm not guaranteeing this, but you should able to convert the above into the newer v5 config (or whatever version) using the wrapper and window args to make sure that the browser window is correctly set since Next.js is probably running through a more standard Node environment.

Let me know if you can get this working at all. This is the best answer I have off the top of my head, and I'm not sure that this has actually been configured to work with Next.js (doesn't mean it won't work though). Happy to help you debug a bit if you can't get it working.

ilyador commented 1 year ago

Thank you @thesublimeobject but is there any way to do it without touching webpack? I believe I'll have to eject the next.js project to gain access to the webpack config.

thesublimeobject commented 1 year ago

@ilyador — I don't believe so, unfortunately. You have to remember that this library was originally built years ago, and was designed for pretty basic HTML applications. Honestly, I'm not even sure this will work for you in react...for some reason I thought I remembered using it once, but going back and checking a few references, I'm doubting that I ever did.

This is just the tradeoff of using something like Next.js or create-react-app...most things will work for you very easily if you are using a fairly standard setup, but for something like this that wasn't built for that framework, in order to use it you'll need to dig into some of the minutia.

That said, I would probably recommend just not using this library for that project, as strange as it might sound. I think you'll end up running into a lot of trouble that probably won't be worth it. React, in itself, has very natural filtering and sorting applications. Given the way it works, you can actually implement something like Isotope without needing an external library by just tracking the state of your filters in the state and then filtering the items on re-render.

I actually have an example of a very recent prototype I built in Next.js for a client wanting to convert their site. I will post the code and some comments below. If you have any further questions, just follow up!

Below are four separate code blocks. The first is the main component for the grid. The second two are helper functions, which you can see used in the main component (used to get products/categories and form filter groups, etc.). The last block is the component for the filters menu, for which I just used a material-ui form. To hopefully make this a little easier to read, I removed a lot of the extraneous information/imports, so there might be some references missing (like styled components, etc.), but I didn't think these would be helpful to actually understanding what's going on here.

So, on load, we build the filter groups from the categories, and then we use those groups via useState to track which filters are active. Then, we use useMemo to reset the filtered products state on refresh (this may not be the best way to do this, but like I said, it was a prototype, so you are welcome to change how this is done). As a whole, this will give you several select menus that can be filtered by group, and therefore gives you the option of multiple types of filter groups, etc., etc. Hopefully this is helpful!

import React, { useState, useMemo } from 'react'
import { ProductFilters } from "../index";
import { getFilterGroups, getFilteredProducts } from '../../utils/Products/product-utils'

interface Props {
    products: Product[];
    categories: ProductCategory[];
}

const ProductGrid = (props: Props) => {
    let { products, categories } = props
    let filterGroups = getFilterGroups(categories)
    const [filters, setFilters] = useState(filterGroups)
    const handleFilters = (filters: any) => {
        setFilters(filters)
    }
    const filteredProducts = useMemo(() => getFilteredProducts(products, filters), [filters, products])

    return (
        <Block>
            <ProductContainer>
                <ProductFilters
                    onFilterUpdate={handleFilters}
                    categories={categories}
                    groups={filterGroups}
                />
                <ProductCardContainer products={filteredProducts} />
            </ProductContainer>

        </Block>
    );
};

export default ProductGrid;
/**
 * Get Filter Group Entities for Default Filtering Object
 *
 * @param {ProductCategory[]} categories
 */
export function getFilterGroups(categories: ProductCategory[]): FilterGroups {
    return categories.reduce((groups: FilterGroups, category: ProductCategory) => ({
        ...groups,
        [category.slug]: '*'
    }), {})
}
/**
 * Filter Products by Current Filters
 *
 * @param {Product[]} products
 * @param {FilterGroups} filters
 */
export function getFilteredProducts(products: Product[], filters: FilterGroups) {
    let all = _.every(filters, (value: string, group: string) => value === '*')
    if (all) {
        return products
    }

    else {
        let groups = _.reduce(filters, (groups: FilterGroups, filter: string, group: string): FilterGroups => {
            return (filter !== '*') ? { ...groups, [group]: filter } : groups
        }, {})

        return _.filter(products, (product: Product) => {
            return Object.entries(groups).every(([ key, value ]) => 
                _.find(product.categories, (category: ProductCategory) => category.slug === value)
            })
        })
    }
}
import React, { useState } from "react";
import FormControl from '@mui/material/FormControl';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import { FilterGroups, getMenuItemValue } from '../../utils/Products/product-utils'

interface Props {
    categories: any;
    onFilterUpdate: any;
    groups: FilterGroups;
}

const ProductFilters = (props: Props) => {
    const [filters, setFilter] = useState(props.groups);
    const handleChange = (event: SelectChangeEvent, child: any) => {
        let value = event.target.value
        let parent = child.props.parent
        let currentFilters = { ...filters, [parent]: value }
        setFilter(currentFilters);
        props.onFilterUpdate(currentFilters)
    };

    return (
        <Block>
            {
                props.categories.map((category: ProductCategory) => (
                    <FormControl fullWidth key={category.id}>
                        <InputLabel id="demo-simple-select-label">{category.name}</InputLabel>
                            <Select
                                labelId="demo-simple-select-label"
                                id="demo-simple-select"
                                defaultValue="*"
                                value={props.categories[category.slug]}
                                label={category.name}
                                onChange={handleChange}
                        >
                            {
                                category.children.map((item: ProductCategory) => (
                                    <MenuItem
                                        parent={category.slug}
                                        value={getMenuItemValue(item)}
                                        key={item.id}
                                    >
                                        {item.name}
                                    </MenuItem>
                                ))
                            }
                        </Select>
                    </FormControl>
                ))
            }
        </Block>
    );
};

export default ProductFilters;
thanhtutzaw commented 1 year ago

you need to check before calling isotope if(typeof window !== 'undefined'){ //client stuff }

p0zi commented 1 year ago

Do not import statically in module header, instead dynamically in useEffect() which always run in client.

const isotope = React.useRef(null);

React.useEffect(() => {
    (async () => {
        // Dynamically load Isotope
        const Isotope = (await import('isotope-layout')).default;

        isotope.current = new Isotope(".filter-container", {
            itemSelector: ".filter-item",
            layoutMode: "fitRows"
        });
    })();

    // cleanup
    return () => isotope.current?.destroy();
}, []);