reactjstypescriptreact-routerfeaturetoggle

How to propery genearate routes with approperiate component with React Router


I am trying to generate routes with the appropriate components from "Feature" objects. The idea is to have a collection of site features that can be enabled or disabled, and routes/navigation menu items generated from this collection. Sort of like feature toggle for the routes and navigation items. I got as far as the page runs but any navigation just loads the first item from the list although the url gets updated in the browsers url bar.

Here is the "Feature" object:

  export default class SiteFeature {
    id: string;
    name: string;
    displayName: string;
    path: string;
    icon: string;
    isEnabled: boolean;
    isNavOption: boolean;
    component: () => JSX.Element | undefined;
    constructor(
        id: string,
        compononet: () => JSX.Element | undefined,
        { name = '', displayName = '', path = '', icon = '', isEnabled = false, isNavOpion = false } = {},
    ) {
        this.id = id;
        this.component = compononet;
        this.name = name;
        this.displayName = displayName;
        this.path = path;
        this.icon = icon;
        this.isEnabled = isEnabled;
        this.isNavOption = isNavOpion;
    }
}

Here is the index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import './styles/index.scss';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom';

ReactDOM.render(
    <React.StrictMode>
        <Router>
            <App />
        </Router>
    </React.StrictMode>,
    document.getElementById('root'),
);

And here is the app.tsx

    import React, { useEffect, useState } from 'react';
    import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom';
    import './styles/App.scss';
    import Footer from './components/Footer';
    import Navigation from './components/Navigation';
    import AboutPage from './components/pages/AboutPage';
    import LandingPage from './components/pages/LandingPage';
    import PageNotFound from './components/pages/PageNotFound';
    import background from './images/background.jpg';
    import { createStyles, makeStyles, Theme, ThemeProvider } from '@material-ui/core';
    import siteThemeCollection from './styles/siteThemeCollection';
    import ActualsPage from './components/pages/ActualsPage';
    import EventsPage from './components/pages/EventsPage';
    import DonationPage from './components/pages/DonationPage';
    import ContactPage from './components/pages/ContactPage';
    import getSiteFeatures from './services/featureToggle';
    import SiteFeature from './common/SiteFeature';
    
    const themeCollection = siteThemeCollection();
    
    function App(): JSX.Element {
        const useStyles = makeStyles((theme: Theme) =>
            createStyles({
                root: {
                    flexGrow: 1,
                },
                appContainer: {
                    backgroundImage: `url(${background})`,
                    backgroundPosition: 'center',
                    backgroundRepeat: 'no-repeat',
                    backgroundSize: 'cover',
                    display: 'flex',
                    flexDirection: 'column',
                    minHeight: '100vh',
                    opacity: 1,
                },
            }),
        );
    
        const pageStyle = useStyles();
    
        const [siteFeatures, setSiteFeatures] = useState<SiteFeature[]>([]);
    
        useEffect(() => {
            setSiteFeatures(getSiteFeatures);
        }, []);
    
        return (
            <>
                <ThemeProvider theme={themeCollection.defaultSiteTheme}>
                    <div className={pageStyle.appContainer}>
                        <div className="app-content">
                            <Switch>
                                <Route exact path="/">
                                    <LandingPage />
                                </Route>
                                {siteFeatures.map((feature) => (
                                    <div key={feature.id}>
                                        {feature.isEnabled && <Route path={feature.path}>{feature.component}</Route>}
                                    </div>
                                ))}
                                <Route path="/404">
                                    <PageNotFound />
                                </Route>
                                <Redirect to="/404" />
                            </Switch>
                        </div>
                        <Footer />
                    </div>
                </ThemeProvider>
            </>
        );
    }
    
 export default App;

And here is the feature toggle collection

import SiteFeature from '../common/SiteFeature';
import AboutPage from '../components/pages/AboutPage';
import ActualsPage from '../components/pages/ActualsPage';
import EventsPage from '../components/pages/EventsPage';

const getSiteFeatures = (): SiteFeature[] => {
    const siteEmabledFeatures: SiteFeature[] = [];
    const inactive: SiteFeature[] = [];

    siteEmabledFeatures.push(
        new SiteFeature('null1', ActualsPage, {
            name: 'actual',
            displayName: 'Aktuális',
            path: '/actuals',
            icon: 'new_releases',
            isEnabled: true,
            isNavOpion: true,
        }),
        new SiteFeature('null2', AboutPage, {
            name: 'about',
            displayName: 'Bemutatkozás',
            path: '/about',
            icon: 'info',
            isEnabled: true,
            isNavOpion: true,
        }),
        new SiteFeature('null3', EventsPage, {
            name: 'events',
            displayName: 'Események',
            path: '/events',
            icon: 'events',
            isEnabled: true,
            isNavOpion: true,
        }),

    return siteEmabledFeatures;
};

export default getSiteFeatures;

I am a young padawan when it comes to react so i could be on the wrong track but any advise how to make this work would be greatly appreciated. Thanks.


Solution

  • Issue

    The Switch component really only has two valid children components: Route and Redirect. The Switch will return and render the first "match" it finds, and in this case it is hitting the first child div and rendering that.

    <Switch>
      <Route exact path="/">
        <LandingPage />
      </Route>
      {siteFeatures.map((feature) => (
        <div key={feature.id}> // <-- not a Route or Redirect so gets rendered
          {feature.isEnabled && <Route path={feature.path}>{feature.component}</Route>}
        </div>
      ))}
      <Route path="/404">
        <PageNotFound />
      </Route>
      <Redirect to="/404" />
    </Switch>
    

    Solution

    Filter then map your routes.

    <Switch>
      <Route exact path="/">
        <LandingPage />
      </Route>
      {siteFeatures.filter(({ isEnabled }) => isEnabled).map((feature) => (
        <Route
          key={feature.id}
          path={feature.path}
          component={feature.component}
        />
      ))}
      <Route path="/404">
        <PageNotFound />
      </Route>
      <Redirect to="/404" />
    </Switch>