Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sidebaritem): fix sidebar item with next link #438

Merged
merged 1 commit into from
Dec 22, 2022

Conversation

myabeaver
Copy link
Contributor

@myabeaver myabeaver commented Nov 24, 2022

Description

This PR fixes the next/link component for the SidebarItem.

Fixes #430

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

@codecov
Copy link

codecov bot commented Nov 24, 2022

Codecov Report

Base: 97.41% // Head: 97.42% // Increases project coverage by +0.00% 🎉

Coverage data is based on head (bb26a5d) compared to base (4934a89).
Patch coverage: 100.00% of modified lines in pull request are covered.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #438   +/-   ##
=======================================
  Coverage   97.41%   97.42%           
=======================================
  Files         127      127           
  Lines        5460     5471   +11     
  Branches      443      443           
=======================================
+ Hits         5319     5330   +11     
  Misses        141      141           
Impacted Files Coverage Δ
src/lib/components/Sidebar/SidebarItem.tsx 100.00% <100.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@rluders rluders requested a review from tulup-conner November 26, 2022 13:20
@rluders
Copy link
Collaborator

rluders commented Nov 26, 2022

I'm not sure about this one... maybe @tulup-conner could give it a look.

@tulup-conner
Copy link
Collaborator

I don't totally understand what this does TBH. I don't really use Next. I don't think it makes sense for the library to have Next-specific patches.

@rluders
Copy link
Collaborator

rluders commented Nov 27, 2022

Yes. I agree with you one this one @tulup-conner, I don't want to see specific-nextjs or any specific stuff landing at the flowbite-react. I have the same feeling that this is too specific. I'm wondering if #430 can't be solved in some more generic approach.

@rluders
Copy link
Collaborator

rluders commented Nov 27, 2022

Also, I'm seeing the "Legacy" word there... So, is this for new Next.js version or just to support oldest versions? I mean, if this is a legacy support, I'll say: we don't care.

src/lib/components/Footer/FooterLink.tsx Outdated Show resolved Hide resolved
src/lib/components/Navbar/NavbarLink.tsx Outdated Show resolved Hide resolved
@@ -0,0 +1,15 @@
import { useCallback, useState, type FC, type ReactElement } from 'react';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component seems to be like a "documented solution" not a flowbite-react component. So, maybe we could add it to the documentation insted? Some "Known Issues" section?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component is only to test the behavior. But testing it is important, because the anti-pattern (component in component) is not obvious to new React devs.

return (
<ListItem>
<ListItem id={id} isCollapsed={isCollapsed} contentChildren={children}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt like having the same id there and at line 144 will be a problem. I mean, the id should be always unique. Right? Maybe adding some suffix to the one at 144 line, something like ${id}-children.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id != id. This will be passed down to Children not printed as html. But i also feel like it's complicated to follow the code. I haven't changed the code, just moved it out of the component.

I changed:

const ComponentA: FC = (props) => {
  const ComponentB: FC = () => {
    // ...
  };
   // ...
};

to

const ComponentB: FC = () => {
  // ...
};

const ComponentA: FC = (props) => {
   // ...
};

But yes, now it looks weird because of id and contentChildren etc.. But i guess, let's not make to much changes and concentrate us on fixing the bug first. 😄

@myabeaver
Copy link
Contributor Author

myabeaver commented Nov 27, 2022

Also, I'm seeing the "Legacy" word there... So, is this for new Next.js version or just to support oldest versions? I mean, if this is a legacy support, I'll say: we don't care.

@rluders @tulup-conner We don't need it to be named NextLinkLegacyBehavior, but currently it's happening to next's Link component. The reason i added this test, was to ensure, no one, creates components inside components, like this:

const ComponentA: FC = (props) => {
  const ComponentB: FC = () => {
    // ...
  };
   // ...
};

Because it's an anti-pattern, but it's not that obvious. Currently it causes this infinite rerenderings with the Next Link button. But it can also happen to other frameworks like Gatsby if they use same approaches.

I see, that NextLinkLegacyBehavior is not the best naming. This can be renamed (UnstableReferenceTest ?), but my concerns are, that without testing it, someone will add this same component-in-component pattern again.

My suggestion would be something like this:

import { useState, useCallback, type FC } from 'react';

export interface UnstableReferenceTestProps {
	maxUpdateDepth?: number;
	children: (setRef: (ref: unknown) => void) => JSX.Element;
}

export const UnstableReferenceTest: FC<UnstableReferenceTestProps> = ({
	maxUpdateDepth = 10,
	children
}) => {
	const [count, setCount] = useState(0);
	const setRef = useCallback(
		() => {
			if (count >= maxUpdateDepth) {
				throw new Error('Maximum update depth exceeded.');
			} else {
				setCount((x) => x + 1);
			}
		},
		// eslint-disable-next-line react-hooks/exhaustive-deps
		[]
	);

	return children(setRef);
};

@esko22
Copy link

esko22 commented Nov 28, 2022

Been following this a bit as I am having the same issue reported in #397 using the vanilla { Link } from "react-router-dom" producing the <a> cannot appear as a descendant of <a>. using 0.3.5.

Some of this conversation is pushing my boundaries of component rendering :) but just wanted to add a note that the issue doesn't appear to be isolated to the NextJs Link component. Not sure if there are any nuances with how NextJs extends from the core React lib or if it even does at all but I've noticed that the NextJs link shows to use href in the docs where the react router specifies to

        <Sidebar.Items className="pt-8 pr-12">
          <Sidebar.ItemGroup>
                  <Sidebar.Item className="mb-2" icon={IoMdPin}>
                      <Link to="/" >
                      Map
                      </Link>
                  </Sidebar.Item>
            </Sidebar.ItemGroup>
        </Sidebar.Items>

@myabeaver
Copy link
Contributor Author

@rluders @tulup-conner So how shall we proceed?

@tulup-conner
Copy link
Collaborator

tulup-conner commented Dec 1, 2022

Well, I've brought this up somewhere on this repo before, but the universal standard for component libraries as far as I understand is to provide a prop, generally called as, that lets you essentially "cast" any flowbite-react elements that are/can be links as any custom link component.

<Sidebar.Item className="mb-2" icon={IoMdPin} as={Link} to="/">Map</Sidebar.Item>

I tried to do this a few months ago but I'm too stupid. Or, perhaps I just need to try again. Anyway, I think this is probably the most familiar and ergonomic solution.

@rluders
Copy link
Collaborator

rluders commented Dec 1, 2022

I'm just wondering what would happen if the Sitebar.Item became a simple div if there is no href property attached to it, and if there is a href it should become an a otherwise, it will use the children. So, in this case, could be a Link or a Button or whatever fits to the desired action.

And all that I'm saying right now could be really dumb... I'm just wondering what could happen. 🤔

@tulup-conner
Copy link
Collaborator

That makes sense to me, too. I have a theory if we could figure out how to do the as={} it might be less code/more readable, but like I said I failed before. I'll look into this.

@myabeaver
Copy link
Contributor Author

That makes sense to me, too. I have a theory if we could figure out how to do the as={} it might be less code/more readable, but like I said I failed before. I'll look into this.

@tulup-conner The as approach is not that hard. It could be simply done like the icon prop on some components. Like this:

interface FlowbiteLinkProps<P = {}> {
	as: FC<P>;
	props: P & JSX.IntrinsicAttributes;
}

function FlowbiteLink<P = {}>({ as: Component, props }: FlowbiteLinkProps<P>) {
	return <Component {...props} />;
}

And then be used like this:

<FlowbiteLink as={CustomLink} props={{ href: '#' }} />

But i would prefer @rluders approach but with a children prop.

@rluders
Copy link
Collaborator

rluders commented Dec 2, 2022

I'm not sure if I would take my suggestion seriously. I have no idea how it is going to work and if it is a good practice at all. It was just a crazy idea. 🤣

I think that also using the as={} could be interesting for Button component and so many other components that may need nome "tag mutation".

@tulup-conner
Copy link
Collaborator

Ok, I tried again and the issue I was having with Sidebar.Item is that you can't override className, which is how Remix facilitates self-aware links via NavLink. That's because we designate it ourselves in Sidebar.Item on L62. It looks like all other properties work just fine.

I'm not sure how to work around this - it might be that I will just need to patch Remix on my apps. I'm not sure if it is something that will come up with other users/use cases where they need to override className for the as={} component and can't.

@rluders
Copy link
Collaborator

rluders commented Dec 12, 2022

Ok, I tried again and the issue I was having with Sidebar.Item is that you can't override className, which is how Remix facilitates self-aware links via NavLink. That's because we designate it ourselves in Sidebar.Item on L62. It looks like all other properties work just fine.

I'm not sure how to work around this - it might be that I will just need to patch Remix on my apps. I'm not sure if it is something that will come up with other users/use cases where they need to override className for the as={} component and can't.

I'm wondering if my crazy idea to use the Sidebar.Item as a wrapper to do any children component wouldn't solve this. We probably gonna have to change the styling and the children's component will not receive any style directly, just from its parent.

Maybe we could give this crazy idea a try? To see what happens. WDYT?

@tulup-conner
Copy link
Collaborator

tulup-conner commented Dec 12, 2022

Ok, I tried again and the issue I was having with Sidebar.Item is that you can't override className, which is how Remix facilitates self-aware links via NavLink. That's because we designate it ourselves in Sidebar.Item on L62. It looks like all other properties work just fine.

I'm not sure how to work around this - it might be that I will just need to patch Remix on my apps. I'm not sure if it is something that will come up with other users/use cases where they need to override className for the as={} component and can't.

The workaround looks like...

Patch to flowbite-react

-          className={classNames(
-            theme.base,
-            isActive && theme.active,
-            !isCollapsed && isInsideCollapse && theme.collapsed.insideCollapse,
-            className,
-          )}
+          className={
+            typeof className === 'string' || typeof className === 'undefined'
+              ? classNames(
+                  theme.base,
+                  isActive && theme.active,
+                  !isCollapsed && isInsideCollapse && theme.collapsed.insideCollapse,
+                  className,
+                )
+              : className
+          }

Custom component for ease of use

export const SidebarNavLink: FC<NavLinkProps & SidebarItemProps> = function ({
  children,
  ...props
}) {
  const theme = useTheme().theme.sidebar.item;

  return (
    <FlowbiteSidebar.Item
      as={NavLink}
      className={({ isActive }: { isActive: boolean }) =>
        classNames(theme.base, isActive && theme.active)
      }
      {...props}
    >
      {children}
    </FlowbiteSidebar.Item>
  );
};

Actual links

<Sidebar.NavLink to="/brand/components/tooltip">Tooltip</Sidebar.NavLink>

The end product in the final snippet here is an ideal DX from my perspective, but I think the pieces in between can be better. Overall, if this is all I have to write to accomplish this, it isn't really that bad. The custom component I made is for convenience. You can easily just write:

<Sidebar.Item 
  as={NavLink} 
  to="/brand/components/tooltip" 
  className={({ isActive }) => 
    classNames(theme.base, isActive && theme.active)
  }
>
  Tooltip
</Sidebar.NavLink>

For example.

@myabeaver
Copy link
Contributor Author

@tulup-conner I appreciate your efforts, but I think we are currently doing too much effort for a small fix.

Removing the React anti-pattern and moving the components outside the rendering cycle will solve the problem with Next.js.

If the problem persists for other libraries, we should look at that separately.

@myabeaver
Copy link
Contributor Author

@tulup-conner @rluders I removed most of the code. Now it only contains the relevant part. I moved Children , ListItem and TooltipContent outside of the Sidebar.Item component. This fixes the issue with Next.js.

@rluders
Copy link
Collaborator

rluders commented Dec 22, 2022

@myasteiner Well, I guess that this now changes the PR general goal. So, maybe we could update the title and the description.

@myabeaver myabeaver changed the title fix(nextlink): fix next link for link components fix(nextlink): fix sidebar item with next link Dec 22, 2022
@myabeaver myabeaver changed the title fix(nextlink): fix sidebar item with next link fix(sidebaritem): fix sidebar item with next link Dec 22, 2022
@myabeaver
Copy link
Contributor Author

@myasteiner Well, I guess that this now changes the PR general goal. So, maybe we could update the title and the description.

@rluders I've updated the title and description of the PR.

@rluders rluders merged commit 32f337b into themesberg:main Dec 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Maximum update depth exceeded with Next/Link and SidebarItem
4 participants