Open ilyador opened 2 years 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.
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.
@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;
you need to check before calling isotope if(typeof window !== 'undefined'){ //client stuff }
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();
}, []);
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: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?