react-router-dom <Link> Not Updating Page

China☆狼群 提交于 2020-05-31 06:01:17

问题


Description of problem:

Changing the id (numbers only) of this url via the link tag does not update the page (but does change the url in the adress bar). Hitting refresh afterward will show the updated page.

http://localhost:8080/video/id/7564

Right clicking to open the link in a new tab, or changing the link path to a completely different page works as expected.

My app.js file

import React from 'react'
import { Router, Route, Switch } from 'react-router-dom'
import RenderHomepage from '../components/homePage/RenderHomepage'
import RenderChannelPage from '../components/channelPage/RenderChannelPage'
import RenderVideoPage from '../components/videoPage/RenderVideoPage'
import RenderSearchPage from '../components/searchPage/RenderSearchPage'
import PageNotFound from '../components/PageNotFound'
import history from '../history'

const App = () => {
  return ( 
    <div>
      <Router history={history}>
        <Switch>
          <Route path="/" exact component={RenderHomepage} /> 
          <Route path="/channel" component={RenderChannelPage} /> 
          <Route path="/video/id" component={RenderVideoPage} /> 
          <Route path="/search" component={RenderSearchPage} /> 
          <Route path="/404" exact component={PageNotFound} />
          <Route component={PageNotFound} />
        </Switch>
      </Router>
    </div>
  )
}

export default App

Link tag in UpNextVideos component:

import React from 'react'
import { Link } from 'react-router-dom'

...
  <Link to={{pathname: vid.id}}> 
    <h3 className={`${p}-sidebar-grid-video-title`}>{capitalizeFirstLetter(vid.tags)}</h3>
  </Link>
...

How the components in question are nested:

<RenderVideoPage>
  <VideoPage>
    <UpNextVideos>

RenderVideoPage component:

import React from 'react'
import VideoPage from './VideoPage'
import Header from '../Header'
import HeaderMobile from '../HeaderMobile'
import FooterMobile from '../FooterMobile'
import ActivityFeed from '../ActivityFeed'

const RenderVideoPage = () => {
  return (
    <div className="videoPage-body">
      <HeaderMobile />
      <Header />
      <ActivityFeed page={'home'} />
      <VideoPage />
      <FooterMobile page={'video'} />
    </div>
  )
}

export default RenderVideoPage

VideoPage component:

import React, { useEffect, useState } from 'react'
import axios from 'axios'
import history from '../../history'
import handleMediaQueries from './containers/handleMediaQueries'
import setDislikes from './containers/setDislikes'

import NewSubscribers from './NewSubscribers'
import CommentSection from './CommentSection'
import UpNextVideos from './UpNextVideos'
import DescriptionBox from './DescriptionBox'
import VideoNotFound from './VideoNotFound'

import { fetchVideoFromID, fetchPictureFromID } from '../../containers/api'
import { thumbsUp, thumbsDown } from '../svgs'

import { 
  abbreviateNumber, 
  capitalizeFirstLetter, 
  randomDate } from '../../containers/helperFunctions'

const VideoPage = () => {
  const [p, setPrefix] = useState("videoPage")
  const [state, setState] = useState({
    loading: true,
    error: false
  })

  useEffect(() => {
    if (state.loading) extractDataFromUrl()
    else handleMediaQueries()
  }, [state.loading])

  const fetchVideo = async (id, picAuthorID) => {
    let response = await fetchVideoFromID(id)
    if (!response) setState(prevState => ({...prevState, error: true}))
    else mapVideoResponseToHTML(response.data.hits, picAuthorID)
  }

  const mapVideoResponseToHTML = (response, picAuthorID) => {
    let responseAsHtml = response.map(vid => {
      return {
        video: 
        <div className={`${p}-video-wrapper posRelative`} key={vid.id}>
          <a className={`${p}-pixabay-src`} href={vid.pageURL}>?</a>
          <video 
            poster="https://i.imgur.com/Us5ckqm.jpg" 
            className={`${p}-video clickable`} 
            src={vid.videos.large.url} 
            controls autoPlay>
          </video>
          <div className={`${p}-video-info-wrapper`}>  
            <div className={`${p}-video-title-box`}>
              <h1 className={`${p}-video-title`}>{capitalizeFirstLetter(vid.tags)}</h1>
              <span className={`${p}-video-views`}>{abbreviateNumber(Number(vid.downloads).toLocaleString())} views</span>
              <span className={`${p}-video-date`}>{randomDate()}</span>
            </div>
            <div className={`${p}-video-options`}>
              <div className="thumbs">
                <div className={`${p}-video-options-thumbsUp`}>{thumbsUp(20)} &nbsp; 
                  <span className={`${p}-video-options-thumbsUp-text`}>{abbreviateNumber(vid.likes)}</span>
                </div>
                <div className={`${p}-video-options-thumbsDown`}>{thumbsDown(20)} &nbsp; 
                  <span className={`${p}-video-options-thumbsDown-text`}>{setDislikes(vid.likes)}</span>
                </div>
                <div className={`${p}-video-options-likebar`}></div>
              </div>
              <span className={`${p}-video-options-share`}>Share</span>
              <span className={`${p}-video-options-save`}>Save</span>
              <span className={`${p}-video-options-ellipses`}>...</span>
            </div>
          </div>
        </div>,
        authorFollowers: vid.views,
        vidAuthorID: vid.id,
        author: picAuthorID ? 'Loading' : vid.user,
        authorAvatar: picAuthorID ? null : vid.userImageURL,
        views: vid.downloads
      }
    })
    responseAsHtml = responseAsHtml[0]
    setState(prevState => ({...prevState, ...responseAsHtml, loading: false}))
    if (picAuthorID) fetchAuthorAvatar(picAuthorID)
  }

  const extractDataFromUrl = () => {
    const currentURL = window.location.href
    const urlAsArray = currentURL.split('/')
    const urlID = urlAsArray[5].split('-')
    const videoID = urlID[0]
    const picAuthorID = urlID[1]

    // Author avatars are random except on the home page. 
    // if url isnt from homepage, then use videoID
    // if url is from homepage, send that avatarID
    if (urlID.includes('000')) {
      fetchVideo(videoID)
    } else {
      setState(prevState => ({...prevState, picAuthorID: picAuthorID}))
      fetchVideo(videoID, picAuthorID)
    }
  }

  const fetchAuthorAvatar = async (id) => {
    const response = await fetchPictureFromID(id)
    const authorName = response.data.hits[0].user
    const authorAvatar = response.data.hits[0].previewURL
    setState(prevState => ({
      ...prevState, 
      authorAvatar: authorAvatar, 
      author: capitalizeFirstLetter(authorName)
    }))
  }

  return (
    <div>
      { state.error ? <VideoNotFound /> : null}
      { state.loading === true ? null
        : 
        <div className={`${p}-page-wrapper`}>
          <main className={`${p}-main`}>
            {state.video}
            <DescriptionBox props={state} />
            <div className={`${p}-suggested-videos-mobile`}></div>

            <div className={`${p}-new-subscribers-wrapper`}>
              <h2 className={`${p}-new-subscribers-text`}>{`New Subscribers to ${state.author}`}</h2>
              <NewSubscribers />
            </div>
            <div className={`${p}-comment-section`}>
              <CommentSection views={state.views}/>
            </div>
          </main>
          <aside className={`${p}-sidebar`}>
           <UpNextVideos />
          </aside>
        </div>
      }
    </div>
  )
}

export default VideoPage

UpNextVideos component:

import React, { useEffect, useState, useRef, useCallback } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import { videoQuery } from '../../words'
import { fetchVideos } from '../../containers/api'
import { 
  capitalizeFirstLetter, 
  uuid,
  getRandom,
  abbreviateNumber
} from '../../containers/helperFunctions'

const UpNextVideos = () => {
  const [p, setPrefix] = useState("videoPage")
  const [nextVideos, setNextVideos] = useState([])

  useEffect(() => {
    fetchUpNextVideos(15, getRandom(videoQuery))
  }, [])

  // INFINITE SCROLL
  const observer = useRef()
  const lastUpNextVideo = useCallback(lastVideoNode => {

    // Re-hookup observer to last post, to include fetch data callback
    if (observer.current) observer.current.disconnect()
    observer.current = new IntersectionObserver(entries => {
      const lastVideo = entries[0]
        if (lastVideo.isIntersecting && window.innerWidth <= 1000) {
          document.querySelector('.videoPage-show-more-button').classList.add('show')
        }
        else if (lastVideo.isIntersecting && window.innerWidth > 1000) {
          document.querySelector('.videoPage-show-more-button').classList.remove('show')
          fetchUpNextVideos(20, getRandom(videoQuery))
      }
    })
    if (lastVideoNode) observer.current.observe(lastVideoNode)
  })

  const fetchUpNextVideos = async (amount, query) => {
    let response = await fetchVideos(amount, ...Array(2), query)
    response = response.data.hits

    const responseAsHtml = response.map((vid, index) => {
      return (
        <div className={`${p}-sidebar-grid-video-wrapper`} key={uuid()} ref={response.length === index + 1 ? lastUpNextVideo : null}>
          <div className={`${p}-sidebar-grid-video`}>
            <a href={`/video/id/${vid.id}-000`}>
              <video 
                className={`${p}-upnext-video`} 
                onMouseOver={event => event.target.play()}
                onMouseOut={event => event.target.pause()}
                src={`${vid.videos.tiny.url}#t=1`}
                muted >
              </video>
            </a>
          </div>
          <a href={`/video/id/${vid.id}`}>
            <h3 className={`${p}-sidebar-grid-video-title`}>{capitalizeFirstLetter(vid.tags)}</h3>
          </a>
          <a href={`/channel/000${vid.id}`}>
            <p className={`${p}-sidebar-grid-video-author`}>{vid.user}</p>
          </a>
          <p className={`${p}-sidebar-grid-video-views-text`}>{abbreviateNumber(vid.downloads)} views</p>
        </div>
      )
    })
    setNextVideos(prevState => ([...prevState, ...responseAsHtml]))
  }

  return (
    <div>
      <div className={`${p}-sidebar-text-top`}>
        <span className={`${p}-sidebar-text-upnext`}>Up next</span>
        <span className={`${p}-sidebar-text-autoplay`}>Autoplay</span>
      </div>
      <div className={`${p}-sidebar-grid-wrapper`}>
        {nextVideos}
      </div> 
      <button 
        className={`${p}-show-more-button`} 
        onMouseDown={() => fetchUpNextVideos(15, getRandom(videoQuery))}>
        Show More
      </button>
    </div>
  )
}

export default UpNextVideos

What I've tried:

  • Wrapping the <Link> tag with <Router history={history} />
  • Wrapping the <Link> tag with <BrowserRouter>
  • Wrapping the export statement withRouter(UpNextVideos)
  • Using a plain string instead of an object, as described in react-router-docs

回答1:


Ok, I believe this issue lies in your VideoPage component.

useEffect(() => {
  if (state.loading) extractDataFromUrl()
  else handleMediaQueries()
}, [state.loading]);

You only ever have state.loading true once, when the component mounts. This only processes your URL once, so when the URL changes this component isn't aware of it.

This is your route currently

<Route path="/video/id" component={RenderVideoPage} />

now assuming your URLs are shaped "/video/id/" then you can define your route to have a parameter

 <Route path="/video/id/:videoId" component={RenderVideoPage} /> 

If you wrap this component with react-router-dom's withRouter HOC you can easily get the id path param and add it to an effect to recompute all the video data.

export default withRouter(VideoPage)

withRouter injects the location, match, and history props from the closest Route ancestor. Here's an example of getting the id param and triggering an effect when its value updates.

const VideoPage = ({ match }) => {
  const { params } = match;

  useEffect(() => { /* do something with new id */ }, [params.videoId]);

}


来源:https://stackoverflow.com/questions/59707853/react-router-dom-link-not-updating-page

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!