Transition problem when using with useContext
ziimakc opened this issue ยท 8 comments
I want to use react-nprogress in next.js using shared context.
It's works fine except transition. Sandbox example.
Property progress in style margin-left: ${({ progress }): number => (-1 + progress) * 100}%;
after isFinished became 1 and after second usage line first goes back from then right end, when i need it from the start.
Is there a way to solve this?
P.S. Thank you for the library!
Hey @ziimakc, thanks for the kind words and the issue repro. Looking into it now โ
Hmm yea, when restarting the animation, there's a brief moment where progress
is 1
, which explains the strange bar behaviour.
Looks like either a glitch in the internal code, and/or I need to provide more hook usage examples. Will let you know how I get on.
@ziimakc have played around with your example, and have something working here.
The main idea here is to force a remount of the required components using the key
prop, as per the existing router examples in this repo. See this comment, and this file in the next-router example.
One thing to note is if you click the button furiously things might get into a weird state. I'm not sure if this is going to be a practical issue though once you've actually hooked it up to next routing instead of just that button clicking example.
If this way of doing things isn't suitable, then we may be able to meddle with the CSS animation in order to have it not animate back to the starting state.
Anyway, keen to hear how you get on. Any q's just sing out ๐
Care to elaborate on why the issue was closed without an explanation?
Would be good to know if that solution I proposed didn't work, or if you were looking for another solution, or if you found another library (which is completely fine, I'm more interested in solving problems for users than being the most popular library ๐ )?
@tanem hi, your solution didn't work as i hoped for (after furiously clicking), so i solved it by myself adding state that is changed by useEffect on isFinished change with delay of hide animation. This way transition problem can be seen only if you start another loading very fast, that is fine by me for now. I had no time to dive in more to tottaly solve the problem.
Here is the code if someone will need it:
import { useNProgress } from '@tanem/react-nprogress'
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
const ANIMATION_DURATION = 400
const LoadingProgress = ({ isAnimating }: { isAnimating: boolean }): JSX.Element => {
const { animationDuration, isFinished, progress } = useNProgress({
isAnimating,
animationDuration: ANIMATION_DURATION,
incrementDuration: 60,
minimum: 0.6,
})
const [isFinishedWithDelay, setIsFinishedWithDelay] = useState(isFinished)
useEffect(() => {
let timer: number | undefined
if (isFinished) {
timer = setTimeout(() => setIsFinishedWithDelay(true), ANIMATION_DURATION)
} else {
setIsFinishedWithDelay(false)
}
return () => {
timer && clearTimeout(timer)
}
}, [isFinished])
return (
<Wrapper animationDuration={animationDuration} isFinished={isFinished}>
<BarContainer progress={progress} animationDuration={animationDuration} isFinishedWithDelay={isFinishedWithDelay}>
<Bar />
</BarContainer>
</Wrapper>
)
}
const Wrapper = styled.div<{ animationDuration: number; isFinished: boolean }>`
opacity: ${({ isFinished }): number => (isFinished ? 0 : 1)};
pointer-events: none;
transition: ${({ animationDuration }): string => `opacity ${animationDuration}ms linear`};
`
const BarContainer = styled.div<{ progress: number; animationDuration: number; isFinishedWithDelay: boolean }>`
background: ${({ theme }): string => theme.primaryColor};
height: 2px;
left: 0;
top: 0;
width: 100%;
position: fixed;
margin-left: ${({ isFinishedWithDelay, progress }): number => (-1 + (isFinishedWithDelay ? 0 : progress)) * 100}%;
transition: ${({ animationDuration }): string => `margin-left ${animationDuration}ms linear`};
z-index: 1000;
`
// Shadow at the end of bar
const Bar = styled.div`
box-shadow: ${({ theme }): string => `0 0 10px ${theme.primaryColor}, 0 0 5px ${theme.primaryColor}`};
display: block;
height: 100%;
opacity: 1;
position: absolute;
right: 0;
transform: rotate(3deg) translate(0px, -4px);
width: 100px;
`
LoadingProgress.displayName = 'LoadingProgress'
export { LoadingProgress }
@tanem also i think key solution that you used in example is the way to go, i just need to wrap my head around of how to use it in context for routes and manual triggering.
@tanem ok, here is the working example for next.js NProgress context.
I think this reducer could be included in useNProgress context itself. It's only job is to change key if isAnimating goes from false to true. Idn why i need to do it by hand.
For some reason minimum: 0.6 was working bad for route changes, so for this type of events i used minimum: 0 and for the rest is 0.6 by default.
import React, { useEffect, useReducer } from 'react'
import { LoadingProgress } from 'frontend/components/LoadingProgress'
import { useRouter } from 'next/router'
const LoadingProgressBarContext = React.createContext({} as { loading: React.Dispatch<Action> })
type State = { isLoading: boolean; key: number; minimum: number }
type Action = { type: 'stop' | 'start'; minimum?: number }
const initialState = {
isLoading: false,
key: Date.now(),
minimum: 0.6,
}
function reducer(state: State, { type, minimum = 0.6 }: Action) {
switch (type) {
case 'stop':
return { ...state, isLoading: false }
case 'start':
return { minimum, isLoading: true, key: state.isLoading ? state.key : Date.now() }
default:
break
}
return state
}
const LoadingProgressBarContextProvider = ({ children }: { children: React.ReactNode }): JSX.Element => {
const [state, dispatch] = useReducer(reducer, initialState)
const { events } = useRouter()
useEffect(() => {
const start = () => dispatch({ type: 'start', minimum: 0 })
const stop = () => dispatch({ type: 'stop' })
events.on('routeChangeStart', start)
events.on('routeChangeComplete', stop)
events.on('routeChangeError', stop)
return () => {
events.off('routeChangeStart', start)
events.off('routeChangeComplete', stop)
events.off('routeChangeError', stop)
}
}, [events])
return (
<LoadingProgressBarContext.Provider value={{ loading: dispatch }}>
<LoadingProgress isAnimating={state.isLoading} key={state.key} minimum={state.minimum} />
{children}
</LoadingProgressBarContext.Provider>
)
}
LoadingProgressBarContext.displayName = 'LoadingProgressBarContext'
LoadingProgressBarContextProvider.displayName = 'LoadingProgressBarContextProvider'
export { LoadingProgressBarContextProvider, LoadingProgressBarContext }
import { useNProgress } from '@tanem/react-nprogress'
import React from 'react'
import styled from 'styled-components'
const ANIMATION_DURATION = 400
const LoadingProgress = ({ isAnimating, minimum }: { isAnimating: boolean; minimum: number }): JSX.Element => {
const { animationDuration, isFinished, progress } = useNProgress({
isAnimating,
animationDuration: ANIMATION_DURATION,
incrementDuration: 60,
minimum: minimum,
})
return (
<Wrapper animationDuration={animationDuration} isFinished={isFinished}>
<BarContainer progress={progress} animationDuration={animationDuration}>
<Bar />
</BarContainer>
</Wrapper>
)
}
const Wrapper = styled.div<{ animationDuration: number; isFinished: boolean }>`
opacity: ${({ isFinished }): number => (isFinished ? 0 : 1)};
pointer-events: none;
transition: ${({ animationDuration }): string => `opacity ${animationDuration}ms linear`};
`
const BarContainer = styled.div<{ progress: number; animationDuration: number }>`
background: ${({ theme }): string => theme.primaryColor};
height: 2px;
left: 0;
top: 0;
width: 100%;
position: fixed;
margin-left: ${({ progress }): number => (-1 + progress) * 100}%;
transition: ${({ animationDuration }): string => `margin-left ${animationDuration}ms linear`};
z-index: 1000;
`
// Shadow at the end of bar
const Bar = styled.div`
box-shadow: ${({ theme }): string => `0 0 10px ${theme.primaryColor}, 0 0 5px ${theme.primaryColor}`};
display: block;
height: 100%;
opacity: 1;
position: absolute;
right: 0;
transform: rotate(3deg) translate(0px, -4px);
width: 100px;
`
LoadingProgress.displayName = 'LoadingProgress'
export { LoadingProgress }
Thanks for taking the time to respond, and to post your code ๐ Some notes from me:
- I think it's fine to not try and completely solve the button clicking example if that's not your actual use-case. I'll look at adding such an example to the docs though.
- The reason the key-based method isn't baked into the library itself is because I'm not sure if it's the best way to solve this problem. My preference at the moment is to provide working examples of various solutions, and if one is clearly superior I'll consider adding it into the library.
I'll spend some time sprucing up the hook-based examples in this repo either way ๐