Search code examples
reactjsreact-router-domnetlify

React Router for dynamic routing doesnt work live


I have sample React blog with dummy data, but when I host it on netlify the dynamic url routing doesnt work. It is designed so there is two js pages, one lists all posts (index) and other is detail page for post (single post). Because I use headless CMS the single post template is one for all and it changes the data depending on the slug selected, the single post page is "generated" depending on which slug you choose.

The build is successful and index works, you can also click on a post and it will be generated, but when you click on "next post" (to generate the same template with different data from slug), it doesnt recognizes the URL and shows error:

Page Not Found Looks like you've followed a broken link or entered a URL that doesn't exist on this site.

"going back" also doesnt work and same error persist. you can only go back to blog by entering the index URL in browser. from index you can access all posts when you click on them, but the dynamic routing from each post doest work.

# index.js
import React from 'react';
import { createRoot } from 'react-dom/client'; 
import { BrowserRouter, Route, Routes } from 'react-router-dom'; // dyanmic URL routing

import AllPosts from './pages/AllPosts';
import SinglePost from './pages/SinglePost';
import Footer from './Footer';
import Header from './Header';

// environment variables
require('dotenv').config();

// Use the createRoot API to render your app
const rootElement = document.getElementById('root');
const root = createRoot(rootElement); 

root.render(
  <React.StrictMode>
  <BrowserRouter>
  <Header />
    <Routes> {/*AllPosts is basically index.html */}
      <Route exact path="/" element={<AllPosts />} />
      <Route path='/posts/:slug' element={<SinglePost />} />
    </Routes>
  <Footer />
  </BrowserRouter>
  </React.StrictMode>

);

# SinglePost.js 
import React, { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';

const graphqlToken = process.env.GRAPHQL_TOKEN;
const endpoint = graphqlToken;

const SinglePost = () => {
  const { slug } = useParams(); // obtained from AllPost when clicking, slug is variable for dynamic rendering
  const [post, setPost] = useState(null); // fetch post data
  const [pcedges, setPcedges] = useState([]); // Create a state for PostConnect edges (next and prev posts)
  const [currentPostIndex, setCurrentPostIndex] = useState(0); // Initialize currentPostIndex to 0

  // Your GraphQL query, slug is variable when fetching
  // Query also PostConnection for suggesting other posts.
  const query = `query MyQuery($slug: String!) {
    posts(where: {slug: $slug}) {
      title
      excerpt
      id
      date
      slug
      content {
        markdown
      }
      coverImage {
        url
      }
      author {
        name
        title
        picture {
          url
        }
      }
    }
    postsConnection (orderBy: date_DESC){
      edges {
        node {
          title
          slug
          date
        }
      }
    }
  }
  `;
    const variables = { //this const 'variables' is not used because dynamic variable { slug } defined below is for dynamic content
      "slug": slug,
    };

    const formatDate = (dateString) => { //Date formatting
      const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
      const formattedDate = new Date(dateString).toLocaleDateString(undefined, options);
      return formattedDate;
    };
  
  // fetchData defined outside useEffect hook, for dynamic calling for every different slug
  const fetchData = async (slug) => {
    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
        body: JSON.stringify({
          query,
          variables: { slug }, // Pass the slug as the variable
        }),
      });
  
      const responseData = await response.json();
      const edges = responseData.data.posts; // define post data
      const pcEdges = responseData.data.postsConnection.edges; // define next and prev posts from PostConnect (in query)
  
      setPcedges(pcEdges);
      // next and prev posts routing by setting the post slug
      setCurrentPostIndex(pcEdges.findIndex((pcedge) => pcedge.node.slug === slug));
  
      if (edges && edges.length > 0) {
        setPost(edges[0]); //first value([0]) of query always is post data
      } else {
        setPost(null);
      }
    } catch (error) {
      console.error('Error fetching data:', error);
      setPost(null);
    }
  };
  
  // React hook
  useEffect(() => {
    fetchData(slug); //call fetchData with post content depending on slug
    // second arg for useEffect is slug to check for changes.
  }, [slug]);

  if (!post) {
    return <div>Loading...</div>;
  }

  // set next and prev post; (orderBy: date_DESC) in graphQL query is necessary for this order to work, otherwise links are random
  const nextPost = pcedges[currentPostIndex + 1]?.node;
  const previousPost = pcedges[currentPostIndex -1]?.node;

  //set slug in order for post content to fetch
  const handleNextPostClick = () => {
    if (currentPostIndex + 1 < pcedges.length) {
      const nextPostSlug = pcedges[currentPostIndex + 1]?.node.slug;
      if (nextPostSlug) {
        fetchData(nextPostSlug); 
      }
    }
  };
  
  const handlePreviousPostClick = () => {
    if (currentPostIndex - 1 >= 0) {
      const previousPostSlug = pcedges[currentPostIndex - 1]?.node.slug;
      if (previousPostSlug) {
        fetchData(previousPostSlug);
      }
    }
  };
  

  return (
    <main class='post'>

        <section class='post__header'>
          <div class="formatedDate">
            <dl>{formatDate(post.date)}</dl>
          </div>

        <h2>{post.title}</h2>
        </section>

        <section class='post__content'>

          <div class='post__content__column'>

            <div class="description">


              <div class="detail">

                <div class="img-container">
                  {/* if no picture from query use hardcoded placeholder */}
                  {post.author.picture && post.author.picture.url ? ( <img src={post.author.picture.url}/> ) : ( <img src='https://26159260.fs1.hubspotusercontent-eu1.net/hubfs/26159260/personalBlog/cv-big-photo2.png'/> )}
                </div>

                <div>
                  <p class='author'>{post.author.name}</p>
                  <p class='title'>{post.author.title}</p>
                </div>

              </div>

            </div>
            <hr />
            
            <div class="footer">

              <div class='footer__other-posts'>
                {/* if there is no nextPost or prevPost, dont show link option */}
                {nextPost && (
                  <div>
                    <p>NEXT POST</p>
                    <a href={`/posts/${nextPost.slug}`} onClick={handleNextPostClick}>{nextPost.title}</a>
                  </div>
                )}

                {previousPost && (
                  <div>
                    <p>PREVIOUS POST</p>
                    <a href={`/posts/${previousPost.slug}`} onClick={handlePreviousPostClick}>{previousPost.title}</a>
                  </div>
                )}

              </div>
              <hr />

              <div class='footer__backlink'>
                <a href='/'>← Back to the blog</a>
              </div>

            </div>

          </div>

          <div className="post__content__post-content">
            <div class='post__content__post-content__img-container'>
              {/* if no picture from query use hardcoded placeholder */}
              {post.coverImage && post.coverImage.url ? (<img id='post-img' src={post.coverImage.url} /> ) : ( <img id='post-img' src='https://26159260.fs1.hubspotusercontent-eu1.net/hubfs/26159260/personalBlog/cover-img2.jpg' />)}
            </div>
            <hr />
            <ReactMarkdown>{post.content.markdown}</ReactMarkdown> {/* render MarkDown content (post body) */}
          </div>

        </section>
    </main>
  );
};

export default SinglePost;


// AllPosts.js
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Link } from 'react-router-dom';

// Access the GraphQL token using the environment variable
const graphqlToken = process.env.GRAPHQL_TOKEN;

// GraphQL endpoint URL
const endpoint = graphqlToken;

const AllPosts = () => {
  // get title and ID when clicking on a post (see below)
  const handleClick = (id, title) => {
      console.log('Title:', title);
      console.log('ID:', id);
  };
  
  const [data, setData] = useState([]);

  // Your GraphQL query and variables
  const query = `query MyQuery {
      posts (orderBy: date_DESC){
        title
        excerpt
        id
        date
        slug
      }
    }`;
  const variables = {
  // "first": 25,
  };

  // format date from 15.07.2020 to weekday, Month dayNum, year.
  const formatDate = (dateString) => {
      const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
      const formattedDate = new Date(dateString).toLocaleDateString(undefined, options);
      return formattedDate;
  };
    
  // React hook   
  useEffect(() => {
    // fetchData: asynchronous function to fetch data
    async function fetchData() {
      try {
        // fetch: send POST request to endpoint
        const response = await fetch(endpoint, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
          },
          body: JSON.stringify({
            // include query and variables
            query,
            variables,
          }),
        });
        // Response from server is parsed as JSON
        const responseData = await response.json();
        // assuming response is object with data, page property and edge array
        const edges = responseData.data.posts;
        // console.log(edges)

        // new response state
        setData(edges);

      } catch (error) {
        console.error('Error fetching data:', error);
      }
    }

    fetchData();
    // By passing an empty dependency array (second argument of useEffect), 
    // you tell React not to watch for any changes in specific dependencies 
    // and only run the effect once. 
    // For example see dynamic routing depending on slug (see SinglePost.js )

  }, []); // Run the effect only once on component mount

  return (
    // rendering the fetched data

    <main> {/* top-level container */}

      {/* Display the fetched data here */}
      <section class="title">
        <h1>Latest</h1>
        <p>Our latest blog posts.</p>
      </section>

      <section>

        <ul>

          {data.map((edge, index) => (
          <li key={index} onClick={() => handleClick(edge.id, edge.title)}> {/* handle click for post title and ID*/}

            <div class="article">

              <div class="formatedDate">
                  <dl>{formatDate(edge.date)}</dl> {/* formatted date */}
              </div>

              <div class="article__text">
                <div>
                  <h2><Link to={`/posts/${edge.slug}`}>{edge.title}</Link></h2> {/* use Link for dynamic routing */}
                  <p>{edge.excerpt}</p>
                </div>
                <span><a href={`/posts/${edge.slug}`}>Read more →</a></span> 
              </div>

            </div>
          
          </li>
          ))}

        </ul>

      </section>
    </main>
  );
};

export default AllPosts;

this config works in localhost

I think the issue may be in SinglePost.js:

 // React hook
  useEffect(() => {
    fetchData(slug); //call fetchData with post content depending on slug
    // second arg for useEffect is slug to check for changes.
  }, [slug]);

or with the const variables from my query:

const variables = { //this const 'variables' is not used because dynamic variable { slug } defined below is for dynamic content
      "slug": slug,
    };

Solution

  • react-router-dom uses Link component for SPA routing. SPA routing means your app only has 1 single HTML file, routing should be done with JS by handling states, values in the memory (in React it stored on the virtual dom). In your code, you are using <a> (anchor tag) which will change the window.location, load the whole new page, and the React virtual dom gets cleared meaning all data will be lost.

    <a href={`/posts/${nextPost.slug}`} onClick={handleNextPostClick}>{nextPost.title}</a>
    

    You might want to try to replace a tag with Link element.