sanfilippopablo / react-swipeable-routes

Utility for integrating react-swipeable-views with react-router.
MIT License
75 stars 14 forks source link

Support for react-router v6? #40

Open Haltarys opened 2 years ago

Haltarys commented 2 years ago

I am making a small react project with Material UI and React Router.
I want to implement a tab system with transitions following the documentation. To do so, I use react-swipeable-views.

I have two <Route /> and I would like to render them based on the current location AND have a sliding transition when switching from one to the other.

My code is as follows but it doesn't work because the <Route /> elements are not rendered inside a <Routes /> element.

import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Container from '@mui/material/Container';
import SwipeableViews from 'react-swipeable-views';

function App() {
  const location = useLocation();
  const [value, setValue] = useState(location.pathname);

  return (
    <>
      <Tabs
        value={value}
        onChange={(e, val) => setValue(val)}
        indicatorColor="secondary"
        variant="fullWidth"
        textColor="inherit"
      >
        <Tab value="/" label="Reference" component={RouterLink} to="/" />
        <Tab value="/quiz" label="Quiz" component={RouterLink} to="/quiz" />
      </Tabs>
      <Container>
        {/* React wants this to be a <Routes /> but that would mean no transitions :( */}
        <SwipeableViews>
          <Route path="/" element={<div>reference</div>} />
          <Route path="/quiz" element={<div>quiz</div>} />
        </SwipeableViews>
      </Container>
    </>
  );
}

To fix this issue I tried using react-swipeable-routes with provides a <SwipeableRoutes/> element which is a merge of <Routes /> and <SwipeableViews />. Unfortunately, this component is not compatible with v6 of React Router.

I tried to implement my own <SwipeableRoutes /> by inspecting the code of React Router v6, react-swipeable-views, and react-swipeable-routes. Here is the result I have arrived to:

import { Fragment } from 'react';
import {
  createRoutesFromChildren,
  matchRoutes,
  RoutesProps,
  useLocation,
} from 'react-router-dom';
import SwipeableViews from 'react-swipeable-views';

function SwipeableRoutes({ children, location }: RoutesProps) {
  location = location || useLocation(); // Location
  const routes = createRoutesFromChildren(children); // RouteObject[]
  const matches = matchRoutes(routes, location); // RouteMatch[]
  const index = routes.findIndex((route) => route === matches?.[0].route);

  // index + 1 because index equals -1 in case of no matching route.
  return (
    <SwipeableViews index={index + 1}>
      <div>No match!</div>
      {routes.map((route) => (
        <Fragment key={route.path}>{route.element}</Fragment>
      ))}
    </SwipeableViews>
  );
}

export default SwipeableRoutes;

My code works but I am not statisfied with it. How can I improve my component/rewrite it entirely to make react-swipeable-views compatible with React Router v6?

My problems are:
First, matchRoutes returns an array of matches, whereas <Routes/> only matches and renders one component. Is this safe?
Second, I use .findIndex() which has O(n) time complexity. Can I do better? Third, will my component behave correctly when containing or by contained by nested <Route/> or <Routes/>?
Fourth, I fear my component does not fully implement all the lesser known behaviors and features of regular <Routes/> and is overall a hack, instead of a well thought-out React component.

Thank you in advance.

flying-sheep commented 2 years ago

IDK what made it work for you, but for me it didn’t work at all, constantly rerendering until it broke.

With https://github.com/remix-run/react-router/issues/8470 merged this should be actually possible.