bradstiff / react-app-location

A package to avoid repetition with Routes and URLs, and reduce boilerplate with location param parsing in React Apps
MIT License
97 stars 8 forks source link

Problem using location.toRoute() in Switch #5

Closed YM-KA closed 5 years ago

YM-KA commented 5 years ago

I am using a custom component "PrivateRoute" to create routes for insertion into a Switch component in a router. The problem is that only the first route in the Switch component gets rendered - none of the following routes ever gets rendered.

In the below example I am pointing my browser to http://localhost:8080/test - the console will only log three routes being rendered (Sidebar, Header and StartLocation). The "TestPage" component will not be rendered. The only page that can be rendered (aside from Sidebar and Header) is StartPage. All other routes are ignored.

//Locations
...
const firebaseId = Yup.string().matches(/-\w{19}/)
const StartLocation = new Location('/v/start')
const SidebarLocation = new Location('')
const HeaderLocation = new Location('')
const LoginLocation = new Location('/')
const CreateLocation = new Location('/v/create')
const ViewLocation= new Location('/v/:id', {id: firebaseId.required()})

//The Router
...
const AppRouter = () => (
  <Router history={history}>
    <div>
      <PrivateRoute myLocation={SidebarLocation} component={Sidebar} history={history} exact={true} />
      <div>
        <PrivateRoute myLocation={HeaderLocation} component={Header} history={history} exact={true} />
        <Switch>
          <PrivateRoute myLocation={StartLocation} component={StartPage} exact={true} />
          <PrivateRoute myLocation={CreateLocation} component={AddPage} exact={true} />
          <PrivateRoute myLocation={ViewLocation} component={ViewPage} exact={true} />
          <PrivateRoute myLocation={TestLocation} component={TestPage} history={history} exact={true} />
          <Route component={NotFoundPage} />
        </Switch>
      </div>
    </div>
  </Router>
);

//The custom component 
...
//stateless func comp
const PrivateRoute = ({
  isAuthenticated, 
  myLocation,
  component: Component,
  history,
  exact,
  unauthPath=LoginLocation.path
}) => {
  const MyRoute = myLocation.toRoute({ render : () => 
    isAuthenticated ? (
      <ErrorBoundary history={history}>
        <Component />
      </ErrorBoundary>
    ) : (
      <Redirect to={unauthorisedPath} /> //redirect if not auth - defaults to login page 
    )
  , invalid: NotFoundPage }, false)

  console.log(exact, MyRoute)
  return MyRoute
}

//don't need dispatch anything but need to check if user is authenticated
const mapStateToProps = (state) => ({
  isAuthenticated: !!state.auth.uid //true, false. if exist we're auth
});

//connected version of comp, used for consumption by approuter
export default connect(mapStateToProps)(PrivateRoute);

Am I doing something wrong? Is this a expected behavior?

Kind regards Kazuyuki

bradstiff commented 5 years ago

I don't see TestLocation defined in your code sample. Can you send a link to a comprehensive repro?

YM-KA commented 5 years ago

Sorry, missed to include the TestLocation - it is defined in my code though.

const TestLocation = new Location('/test')

I will link to a comprehensive repro.

bradstiff commented 5 years ago

Your PrivateRoute component won't work inside react-router's Switch component. Switch expects Routes as children, or components that have the same props as a Route. I would suggest using a helper function that composes Location.toRoute.

YM-KA commented 5 years ago

Ok, that explains why the below code works. :) ( The {...rest} spread is what makes it work. )

export const PrivateRoute = ({
  isAuthenticated, 
  component: Component,
  history,
  exact=false,
  noauthPath='/',
  ...rest 
}) => (
  <Route {...rest} component = {(props) => (
    isAuthenticated ? (
      <ErrorBoundary history={props.history}>
        <Component {...props} /> 
      </ErrorBoundary>
    ) : (
      <Redirect to={noauthPath} /> //redirect if not auth
    )
  )} exact={exact}/> 
);

I am still a bit confused though. Is it correct that the Location.ToRoute do not return a component or a component similar to ? I would really love to use Location.ToRoute() to gain the benefit from Yup validation.

Many thanks!

bradstiff commented 5 years ago

Actually, in this code sample, it is the fact you appear to be providing all of the Route props (path, exact, etc.) to your PrivateRoute component that makes it work. Switch is seeing the props it needs in order to determine what to render.

Location.toRoute does return a Route. When you follow the conventions used by the examples in the README, you end up with Routes as direct children of the Switch.

But in your original code sample, you end up with PrivateRoutes as direct children of the Switch. And your PrivateRoutes don't have the props that Switch is looking for.

If you were to eliminate your PrivateRoute component, and instead, put the logic into a helper function that composes Location.toRoute, it should work.

YM-KA commented 5 years ago

Thank you for clearing that up! 👍 I will go back to calling {Location.toRoute} directly in my Switch.

Many thanks! Kazuyuki

YM-KA commented 5 years ago

Sorry to bug you again. I cant seem to find an example of how to pass down props to the component when using a helper function. In the below code I would like for "EditPage" in "EditLocation" to receive "props.match" for matching of "docid"

//The router
const AppRouter = ({isAuthenticated}) => (
  <Router history={history}>
    <div className="dashboardContainer">
      <div className="dashboard-MainContent">
        {HeaderLocation.toRoute({ render : () => privatePath({isAuthenticated, history, component:Header}), invalid: NotFoundPage }, true)}

        <Switch>
          {/* '/' */}
          {LoginLocation.toRoute({ render : () => publicPath({isAuthenticated, history, component:LoginPage}), invalid: NotFoundPage }, true, true)}

          {/* '/v/start' */}
          {StartLocation.toRoute({ render : () => privatePath({isAuthenticated, history, component:StartPage}), invalid: NotFoundPage }, true)}

          {/* '/v/edit/:docid' */}
          {EditLocation.toRoute({ render : (rest) => priv({isAuthenticated, history, component:EditPage}), invalid: NotFoundPage }, true)}

          <Route component={NotFoundPage} />
        </Switch>
      </div>
    </div>
  </Router>

//Helper function
export const priv = ({
  isAuthenticated=false, 
  history, 
  component:Component,
  ...props
}) => {
  return isAuthenticated ? (
    <ErrorBoundary history={history}>
      <Component {...props}/>
    </ErrorBoundary>
  ):(
    <Redirect to={LoginLocation.path} /> //redirect if not auth
  ) 
}

Kind regards Kazuyuki

YM-KA commented 5 years ago

My bad again ;) Below code works as expected.

//Location 
{EditLocation.toRoute({ render : (props) => priv({isAuthenticated, history, component:EditPage, ...props}), invalid: NotFoundPage }, true)}

//Helper function
export const priv = ({
  isAuthenticated=false, 
  history, 
  component:Component,
  ...props
}) => {
  return isAuthenticated ? (
    <ErrorBoundary history={history}>
      <Component {...props} />
    </ErrorBoundary>
  ):(
    <Redirect to={LoginLocation.path} /> //redirect if not auth
  ) 
}