Search code examples
reactjsreduxreact-router-domuse-statereact-context

Problem referencing redux store, state & context of nested component routes in react router dom v6


There is a Catalogue component that some redux store & react context provider code. Before router v6, they wrapped a nested <BrowserRouter> containing Catalogue child routes like below

BEFORE

client/src/components/Catalogue/index.js

import React, { useCallback, useEffect, useState }  from 'react';
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Provider, connect } from "react-redux";
import { createStore, combineReducers } from "redux";

import { getPeopleBy, getChaptersBy, getPostsBy } from "../../api"
import playlistReducer from "../../store/reducers/playlists";

import SideBar from "../CatalogueSidebar/SideBar";
import CatalogueHeader from "../CatalogueHeader/CatalogueHeader"

import CatalogueHome from "../CatalogueHome/CatalogueHome";
import CatalogueSearch from "../CatalogueSearch";
import People from "../People/People";
import Chapters from "../Chapters/Chapters";
import Posts from "../Posts/Posts";
import PostDetail from "../PostDetail/PostDetail";

import FooterBar from "../FooterBar/FooterBar";
import style from "./Catalogue.module.css"

const Catalogue = () => {

  const [chapters, setChapters] = useState(null)
  const [people, setPeople] = useState(null)
  const [posts, setPosts] = useState(null)

  const fetchCatalogue = async () => {
    let chapters = await getChaptersBy("latest")
    let people = await getPeopleBy("active")
    let posts = await getPostsBy("recently_added")

    return { chapters, people, posts }
  }

  useEffect(() => {
      document.documentElement.className = ""; //<head>
      document.body.className = style.CatalogueBody; //<body>

      fetchCatalogue().then(response => {

        setChapters(response.chapters)
        setPeople(response.people)
        setPosts(response.posts)
      })
      return () => {
        setChapters(null)
        setPeople(null)
        setPosts(null)
      }
  }, [])

  const reducers = combineReducers({
      chapters: playlistReducer,
      people: playlistReducer,
  });
  
  const store = createStore(reducers);

  return (
    <Provider store={store}>
      <div className={style.App}>
        <Router>
          <SideBar />
          <CatalogueHeader account={{display_name: "User"}}/>
          <Route path="/catalogue" exact>
            <CatalogueHome chapters={chapters} people={people} posts={posts}/>
          </Route>   
          <Route path="/catalogue/search">
            <CatalogueSearch />
          </Route>
          <Route path="/catalogue/posts">
            <Posts/>
          </Route>   
          <Route path="/catalogue/posts/:id">
            <PostDetail />
          </Route>
          <Route path="/catalogue/people">
            <People />
          </Route> 
          <Route path="/catalogue/chapters">
            <Chapters />
          </Route> 
        </Router>
        <FooterBar />
      </div>
    </Provider>
  );
};

const mapStateToProps = (state) => {
  return {
    posts: state.posts,
    people: state.people,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    initPosts: (data) => dispatch({ type: "init", posts: data }),
    initPeople: (data) => dispatch({ type: "init", people: data }),
  };
};

export default Catalogue;

client/src/components/App.js

import React, { useState } from "react";
import { Route, Switch, BrowserRouter } from "react-router-dom";
import TierA from "./TierA"
import TierB from "./TierB"
import Catalogue from "./Catalogue"
import Admin from "./Admin"
import NotFound from "./NotFound"

import PlayerContext from "./Player/context"
import SearchContext from "./Search/context"
import CatalogueSearchContext from "./CatalogueSearch/context"
import { useSearch } from "../hooks/useSearch"

const App = () => {

    const searchText = useSearch()
    const [playerItem, setPlayerItem] = useState(null)

    return (
        <React.Fragment>
        <SearchContext.Provider value={searchText}>
        <CatalogueSearchContext.Provider value={searchText}>
        <PlayerContext.Provider value={{ playerItem, setPlayerItem }}>
            <BrowserRouter basename={process.env.PUBLIC_URL}>
                <Switch>
                    <Route component={TierA} exact path="/tier_a" />
                    <Route component={TierB} exact path="/tier_b" />
                    <Route component={Catalogue} path="/catalogue" />
                    <Route component={Admin} path="/admin" />
                    <Route component={NotFound} />
                </Switch>
            </BrowserRouter>
        </PlayerContext.Provider>
        </CatalogueSearchContext.Provider>
        </SearchContext.Provider>
        </React.Fragment>
    )
}


export default Routes;

AFTER

Updated "react-router-dom": "^5.1.2" to "react-router-dom": "^6.3.0", and catalogue routes have a few problems. Moved the Catalogue child components to App but only kept CatalogueHome which will be rendered for catalogue index "/"

client/src/components/Catalogue/index.js

 import React, { useCallback, useEffect, useState }  from 'react';
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import { Provider, connect } from "react-redux";
import { createStore, combineReducers } from "redux";

import { getPeopleBy, getChaptersBy, getPostsBy } from "../../api"
import playlistReducer from "../../store/reducers/playlists";
import playingReducer from "../../store/reducers/playing";

import SideBar from "../CatalogueSidebar/SideBar";
import CatalogueHeader from "../CatalogueHeader/CatalogueHeader"

import CatalogueHome from "../CatalogueHome/CatalogueHome";
import CatalogueSearch from "../CatalogueSearch";
import Posts from "../Posts/Posts";
import PostDetail from "../PostDetail/PostDetail";
import People from "../People/People";
import Chapters from "../Chapters/Chapters";

import FooterBar from "../FooterBar/FooterBar";
import style from "./Catalogue.module.css"

const Catalogue = () => {

  const [chapters, setChapters] = useState(null)
  const [people, setPeople] = useState(null)
  const [posts, setPosts] = useState(null)

  const fetchCatalogue = async () => {
    let chapters = await getChaptersBy("latest")
    let people = await getPeopleBy("active")
    let posts = await getPostsBy("recently_added")

    return { chapters, people, posts }
  }

  useEffect(() => {
      document.documentElement.className = ""; //<head>
      document.body.className = style.CatalogueBody; //<body>

      fetchCatalogue().then(response => {

        setChapters(response.chapters)
        setPeople(response.people)
        setPosts(response.posts)
      })
      return () => {
        setChapters(null)
        setPeople(null)
        setPosts(null)
      }
  }, [])

  const reducers = combineReducers({
      chapters: playlistReducer,
      people: playlistReducer,
  });
  
  const store = createStore(reducers);

  return (
    <Provider store={store}>
      <div className={style.App}>

          <SideBar />
          <CatalogueHeader account={{display_name: "User"}}/>

           <CatalogueHome chapters={chapters} people={people} posts={posts}/>

        <FooterBar />
      </div>
    </Provider>
  );
};

const mapStateToProps = (state) => {
  return {
    posts: state.posts,
    people: state.people,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    initPosts: (data) => dispatch({ type: "init", posts: data }),
    initPeople: (data) => dispatch({ type: "init", people: data }),
  };
};

export default Catalogue;

The new App.js does not have absolute paths for "catalogue", so it is using react router v6 and <Outlet/>, but it is missing all of the redux & context wrappers it needs to reference: client/src/components/App.js

import React, { useState } from "react";
import { BrowserRouter, Route, Routes, Outlet } from "react-router-dom";
import TierA from "./TierA"
import TierB from "./TierB"
import Catalogue from "./Catalogue"

import CatalogueHome from "./CatalogueHome/CatalogueHome";
import CatalogueSearch from "./CatalogueSearch";
import Chapters from "./Chapters/Chapters";
import PostDetail from "./PostDetail/PostDetail";
import People from "./People/People";
import Posts from "./Posts/Posts";

import Admin from "./Admin"
import NotFound from "./NotFound"

import PlayerContext from "./Player/context"
import SearchContext from "./Search/context"
import CatalogueSearchContext from "./CatalogueSearch/context"
import { useSearch } from "../hooks/useSearch"


const App = () => {

    const searchText = useSearch()
    const [playerItem, setPlayerItem] = useState(null)

    return (
        <React.Fragment>
        <SearchContext.Provider value={searchText}>
        <CatalogueSearchContext.Provider value={searchText}>
        <PlayerContext.Provider value={{ playerItem, setPlayerItem }}>
        <BrowserRouter basename={process.env.PUBLIC_URL}>
                <Routes>
                    <Route path="/tier_a" element={<TierA/>}/>
                    <Route path="/tier_b" element={<TierB/>}/>
                    <Route path="catalogue"> 
                        <Route index element={<Catalogue/>}/>
                        <Route path="search" element={<CatalogueSearch />}/>
                        <Route path="posts" element={<Post/>}/>  
                        <Route path="post/:id" element={<PostDetail />}/>
                        <Route path="people" element={<People />}/>
                        <Route path="chapters" element={<Chapters />}/>
                    </Route>
                    <Route path="/admin" element={<Admin/>}/>
                    <Route path="*" element={<NotFound/>}/>
                </Routes>
            </BrowserRouter>
        </PlayerContext.Provider>
        </CatalogueSearchContext.Provider>
        </SearchContext.Provider>
        </React.Fragment>
    )
}


export default App;

Getting the following error when trying to hit any of the catalogue nested routes like /catalogue/posts or /catalogue/people:

Uncaught Error: Could not find "store" in the context of "Connect(PostDetail)". Either wrap the root component in a <Provider>, or pass a custom React context provider to <Provider> and the corresponding React context consumer to Connect(PostDetail) in connect options.
    at ConnectFunction (connect.js:244:1)
    at renderWithHooks (react-dom.development.js:16175:1)
    at updateFunctionComponent (react-dom.development.js:20387:1)
    at updateSimpleMemoComponent (react-dom.development.js:20222:1)
    at updateMemoComponent (react-dom.development.js:20081:1)
    at beginWork (react-dom.development.js:22502:1)
    at HTMLUnknownElement.callCallback (react-dom.development.js:4161:1)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:4210:1)
    at invokeGuardedCallback (react-dom.development.js:4274:1)
    at beginWork$1 (react-dom.development.js:27405:1)

My question is do i need to move the Catalogue code like useEffect(), states, contextProviders, to App.js for the props to work? "store" & etc apply to every catalogue child. I can't put the Provider code in <Routes> </Routes> because of the error

Error: [Provider] is not a <route> component. all component children of <routes> must be a <route> or <react.fragment>

Please advise how to fix. Thanks in advance


Solution

  • The issue is that the Catalogue component is the component rendering the Redux Provider and Catalogue is only rendered when the path is "/catalogue" as the index route. This means that the Redux Provider component and store aren't mounted when rendering any other "/catalogue/XYZ" sub-route. The react-router-dom@6 version of Catalogue isn't a true 1-1 conversion.

    To fix, Catalogue should be converted to a layout route that renders an Outlet for its nested routes. CatalogueHome is the component that should be rendered on the index route. You will also want to move the Redux store declaration outside any React component so is isn't redeclared each render cycle.

    Example:

    Catalogue

    const reducers = combineReducers({
      chapters: playlistReducer,
      people: playlistReducer,
    });
    
    const store = createStore(reducers); // <-- declared once
    
    const Catalogue = () => {
      ...
    
      return (
        <Provider store={store}>
          <div className={style.App}>
            <SideBar />
            <CatalogueHeader account={{display_name: "User"}}/>
            <Outlet /> // <-- nested routes render content here
            <FooterBar />
          </div>
        </Provider>
      );
    };
    
    export default Catalogue;
    

    App

    const App = () => {
      ...
    
      return (
        <React.Fragment>
          <SearchContext.Provider value={searchText}>
            <CatalogueSearchContext.Provider value={searchText}>
              <PlayerContext.Provider value={{ playerItem, setPlayerItem }}>
                <BrowserRouter basename={process.env.PUBLIC_URL}>
                  <Routes>
                    <Route path="/tier_a" element={<TierA />} />
                    <Route path="/tier_b" element={<TierB />} />
                    <Route path="catalogue" element={<Catalogue />}> // <-- Catalogue is layout route 
                      <Route
                        index
                        element={(
                          <CatalogueHome // <-- CatalogueHome is index route
                            chapters={chapters}
                            people={people}
                            posts={posts}
                          />
                        )}
                      />
                      <Route path="search" element={<CatalogueSearch />} />
                      <Route path="posts" element={<Post />} />  
                      <Route path="post/:id" element={<PostDetail />} />
                      <Route path="people" element={<People />} />
                      <Route path="chapters" element={<Chapters />} />
                    </Route>
                    <Route path="/admin" element={<Admin />} />
                    <Route path="*" element={<NotFound />} />
                  </Routes>
                </BrowserRouter>
              </PlayerContext.Provider>
            </CatalogueSearchContext.Provider>
          </SearchContext.Provider>
        </React.Fragment>
      );
    };
    
    export default App;