3

Problem

I'm trying to make an SPA with routing (ideally with React hooks) in React, but all the examples, descriptions i find are about displaying different components based on the URL. What i want is something like Youtube or Google docs, where the page structure/components are (mostly) the same and only the content changes.

Context

(Edit: adding a bit more context.)

This is going to be a document editor/presenter.

Page structure: after login, there is always a toolbar(blue color) on the top, for menus, notifications, etc. The rest of the screen will be mostly like the two examples below:

Example1: enter image description here Example2: enter image description here

The search pane(orange) could be switched on/off by a button on the toolbar or by a user session variable. The document will be presented in the document section(grey) based on either a user session variable, doc ID provided in URL or selecting a document in the search pane.

Planned URLs

(Added in edit.)

  • Landing page: /login , login page.

  • Landing page: / , here the toolbar and a preconfigured, user session based default doc would be presented.

  • Document page: /doc?id=oys2OPkfOwQ , same as landing page but document section would contain the document with ID provided as query param.

  • Anything else: /something , toolbar and something under it.

Idea

(Added in edit.)

The layout is defined by CSS grid and page structure changes based on a variable. So this is going to be a prop for the App component coming from default value and user session configured variable and could change later.

This is the functionality i imagine for the App component (pseudo code-like thing):

<Router>
    <Route path='/login'>
        <Login/>
        // Components: Toolbar and something under it
    </Route>
    <Route path='/'>
        <DocApp/> 
        // Components: Toolbar, Document or Toolbar, Search and Document
        // Default document loaded for default, not logged in user
        // Default document loaded from stored user session
    </Route>
    <Route path='/doc'>
        <DocApp/> 
        // Components: Toolbar, Document or Toolbar, Search and Document
        // Same as for '/' except document with ID set as query param is displayed
        // This could be called/triggered from search and document component as well
    </Route>
    <Route path='/somethingelse'>
        <SomethingElse/> 
    </Route>
</Router>

Question

(Edit: rephrased, original question was how to implement a solution where different documents loaded based on URL query parameter.)

What i'm mostly interested in if there is a simpler way to draw the landing layout '/' and specific doc presenter /doc?id=oys2OPkfOwQ layout? In both cases the same components get displayed, only the provided parameter(doc to present) is different.

Solution

(Added in edit.)

By reading the answers and feedback and re-thinking my problem i realized that i have a multiple URLs same content problem.

inspiral
  • 611
  • 1
  • 7
  • 15
  • Can you use a library like Next? That makes SPA nav super easy – k8xian Sep 25 '22 at 23:47
  • Sounds like you just want a static page with dynamic content, no routing involved. Can you edit the post to include a [mcve] for what you are trying to accomplish? – Drew Reese Sep 26 '22 at 00:25
  • @DrewReese i'll try to add add some pseudo-code and more context. – inspiral Sep 26 '22 at 11:08
  • I don't believe you have a "multiple URLs same content" issue, in fact, I think it's quite the opposite. You've a single path, i.e. `"/doc"` and you want to parameterize some part of it, i.e. the queryString for an `id` query parameter. David's answer below is on the right track IMO. – Drew Reese Sep 26 '22 at 17:06

2 Answers2

2

Using React Router to render components based on UrlParams.

First of all, edit your routes to render DocumentLoader component under the route /doc

// file: app.js

import React from "react";
import { BrowserRouter, Route } from "react-router-dom";
import DocumentLoader from "./DocumentLoader";

const App = (props) => {

  return <BrowserRouter>
    <Routes>
      <Route path="/doc" element={<DocumentLoader />}>
    </Routes>
  </BrowserRouter>
}

Create custom hooks for loading documents

You need two custom hooks, one for loading new document by changing the docId query parameter, and another hook to listen to docId changes to reload new document from your backend.

NOTE: Edit loadDocumentData to load from your backend

// file: hooks.js

import { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';

/**
 * TODO:// Refactor this function to call your backend to get
 * Document data by docId
 */
const loadDocumentData = (docId) =>
  new Promise((resolve, reject) => {
    // this setTimeout for demonstration porpuse only
    setTimeout(() => {
      resolve({ id: docId, name: `Document name for ${docId}` });
    }, 3000);
  });

export const useDocument = () => {
  const [loading, setLoading] = useState(true);
  const { docId, loadDocument } = useDocumentParam();
  const [document, setDocument] = useState(null);

  useEffect(() => {
    setLoading(true);

    // Load your document data based on docID
    loadDocumentData(docId)
      .then((doc) => {
        setDocument(doc);
        setLoading(false);
      })
      .catch((e) => {
        console.error('Failed to load doc', docId);
      });
  }, [docId, setLoading]);

  return { document, loading, loadDocument };
};

export const useDocumentParam = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  const docId = searchParams.get('d');

  const loadDocument = useCallback(
    (newDocId) => {
      setSearchParams({ d: newDocId });
    },
    [setSearchParams]
  );

  return { docId, loadDocument };
};


Create DocumentLoader component

To listen on query param changes, load document from server-side, display loading indicator and render the "DocPresenter" component.

// file: DocumentLoader.js

import * as React from 'react';
import DocPresenter from './DocPresenter';
import { useDocument } from './hooks';

const DocumentLoader = (props) => {
  const { loading, document, loadDocument } = useDocument();

  if (loading) {
    return <div>Display loading indicator while loading the document</div>;
  }

  return (
    <div className="document-container">
      <div className="toolbar">NavBar</div>
      <div className="searchbox">search component</div>
      <div className="editor">
        <DocPresenter document={document} setParentstate={loadDocument} />
      </div>
    </div>
  );
};

export default DocumentLoader;


Checkout Live Example on StackBlitz.

Helper Links:

David Antoon
  • 815
  • 6
  • 18
  • Thanks @David Antoon a lot for the detailed answer. I had to heavily re-edit my question since i realized i didn't give enough context and it was quite vague. Sorry for the re-edit. The StackBlitz example is still useful, but not exactly for my problem. – inspiral Sep 26 '22 at 14:01
-1

Here's how I would do it. Notice that the URL will remain the same.

const DynamicComponent = () => {
  const components = {
    Component1: <Component1 />,
    Component2: <Component2 />,
    Component3: <Component3 />,
  };

  const [component, setComponent] = useState(components["Component1"]);

  return (
    <div>
      <div id="nav">
        <span onClick={(e) => setComponent(components["Component1"])}>
          Set to component 1
        </span>
        <span onClick={(e) => setComponent(components["Component2"])}>
          Set to component 2
        </span>
        <span onClick={(e) => setComponent(components["Component3"])}>
          Set to component 3
        </span>
      </div>
      <div>{component}</div>
    </div>
  );
};

export default DynamicComponent;
Shane Sepac
  • 806
  • 10
  • 20
  • I suggest to manage component rendering based on query param as @inspiral mentioned. /doc?d=[ID]. It's not DynamicComponent, it's SwitchComponent, You have to support rendering the same component with document content based on query param DocumentID – David Antoon Sep 26 '22 at 07:20
  • @Shn_Android_Dev sorry, my question was vaguely formulated, i've corrected it. The downvote is not from me. :) – inspiral Sep 26 '22 at 13:53