User UI feedback after clicking on a react-router-dom <Link/>? - javascript

Here's what I'm dealing with:
Single page app built with react + react-router-dom
User clicks on a <Link to={"/new-page-route"}/>
URL changes and my <Route/> starts rendering a new <Component/>
I mean, React is fast, but my new component takes a while to render because it's a whole new page. So it should take something between 200 and 400ms. And if I get no UI feedback, it feels that my click has not worked.
I need some kind of UI feedback so my user's know their click has been fired and something is going on. I don't think I need loaders or anything, but something to show that the click has been "accepted" by the UI.
In theory that could be handled by CSS using:
-webkit-tap-highlight-color: #SOMECOLOR
active: #SOMECOLOR
But somehow, when the URL changes and the new render begins, the browser is not being able to paint those CSS results to the screen, at least this is the behavior on Chrome and Firefox. It gets kind of weird, sometimes I see the tap-highlight and the active-change but almost always I don't see it.
NOTE: This is not the 300ms default delay on mobile that waits for the double tap. I've dealt with that using the appropriate tags.
What I thought about doing is:
Stop using the <Link/> component and use a normal <a/>.
Create a clicked state to be update after the click event
Call event.preventDefault() to prevent normal behavior of the <a/> navigation
Use the clicked state to render some new styles for the UI feedback
Fire history.push("/new-page-route") on a useEffect after clicked state has become true
Something like:
const newUrl = "/new-page-route";
const [clicked,setClicked] = useState(false);
const history = useHistory(); // HOOK FROM react-router-dom
function handleLinkClick(event) {
event.preventDefault();
setClicked(true);
}
useEffect(() => {
if (clicked === true) {
history.push(newUrl);
// OR MAYBE ADD A TIMEOUT TO BE EXTRA SURE THAT THE FEEDBACK WILL BE PAINTED
// BECAUSE I'VE SEEN THE BROWSER NOT BEING ABLE TO PAINT IF I DON'T GIVE IT SOME EXTRA TIME
setTimeout(() => history.push(newUrl),100);
}
},[clicked,history]);
// USE THE clicked STATE TO RENDER THE UI FEEDBACK (CHANGE TEXT COLOR, WHATEVER I NEED);
QUESTION
Has anyone had this issue before? What is a good way of solving this? I guess that in theory the browser should be able to paint before the new render begins, but this is not what I'm getting.
SANDBOX WITH THE ISSUE
https://codesandbox.io/s/nice-monad-4fwoc <=== CLICK HERE TO SEE THE CODE
https://4fwoc.csb.app <=== CLICK HERE TO SEE THE RESULT ONLY
On desktop: I'm able to see the BLUE background for the active state when clicking the link
On mobile: I don't see any CSS change when clicking the links. Unless I tap AND HOLD
Comments: Imagine that your component takes a while to render. Without any UI feedback, it feels that you haven't clicked at all, even though it is rendering on background.
import React from "react";
import { Link, Switch, Route } from "react-router-dom";
import styled from "styled-components";
import "./styles.css";
const HOME = "/";
const ROUTE1 = "/route1";
const ROUTE2 = "/route2";
const LS = {};
// REGULAR CSS RULES FOR THE className="link" ARE ON "./styles.css"
LS.Link_LINK = styled(Link)`
color: black;
-webkit-tap-highlight-color: red;
&:active {
background-color: blue;
}
`;
export default function App() {
return (
<React.Fragment>
<div>
<Switch>
<Route exact path={HOME} component={Home} />
<Route exact path={ROUTE1} component={Component1} />
<Route exact path={ROUTE2} component={Component2} />
</Switch>
</div>
</React.Fragment>
);
}
function Home() {
return (
<React.Fragment>
<div>I am Home</div>
<LS.Link_LINK to={ROUTE1}>Route 1 (using styled-components)</LS.Link_LINK>
<br />
<Link className={"link"} to={ROUTE1}>
Route 1 (using regular CSS)
</Link>
</React.Fragment>
);
}
function Component1() {
return (
<React.Fragment>
<div>I am Component1</div>
<LS.Link_LINK to={ROUTE2}>Route 2 (using styled-components)</LS.Link_LINK>
<br />
<Link className={"link"} to={ROUTE2}>
Route 2 (using regular CSS)
</Link>
</React.Fragment>
);
}
function Component2() {
return (
<React.Fragment>
<div>I am Component2</div>
<LS.Link_LINK to={HOME}>Home (using styled-components)</LS.Link_LINK>
<br />
<Link className={"link"} to={HOME}>
Home (using regular CSS)
</Link>
</React.Fragment>
);
}

You should use two loading phases.
Use lazy
loading
when loading the "heavy" page and display a
skeleton
component as a fallback to show the user that something is loading.
Use a component/redux state loading indicator for when your page is
loading data from apis
Trying to achive that with react-router links is not the best way because routing happends instantly, it is not the thing that stalls your application. Api calls and multiple re-renders cause an application to behave slowly.

Related

Is there a way to integrate framer motion into my astro project? successfully rendered the react component but the animation doesnt seem to be working

The animate property doesnt seem to be working even though I don't receive any error and the component is being rendered without any problem
import { motion } from "framer-motion";
const Card1 = () => {
return (
<>
<motion.div
className="bg-red-400 w-[10px] h-[10px]"
animate={{ x: 100 }}
/>
<h1>yo bros</h1>
</>
);
}
export default Card1;
just make sure you add the client:load attribute onto your react component when you are adding it into your astro page. Just remember, by default, Astro won't load your JS code.
<Header client:load />
That may be your issue.

How do I implement initial page load animations?

How do I implement initial page load animations, bunch of blocks in gray color with loading animation till the actual content renders in Next Js. I there any library for it?
With React 18, use the React.Suspense API to get to a fallback
https://reactjs.org/docs/react-api.html#reactsuspense
// This component is loaded dynamically
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
// Displays <Spinner> until OtherComponent loads
<React.Suspense fallback={<Spinner />}>
<div>
<OtherComponent />
</div>
</React.Suspense>
);
}
Where spinner could be a component that is built from a great resource like this
https://skeletonreact.com/
More Options
https://github.com/buildo/react-placeholder

How do I add a website loading screen in ReactJS?

I'm really new to ReactJs, JS and basically all of web development. I trying to create a single page portfolio website using ReactJS and wanted to try some more advanced techniques such as using hooks. I wanted to create a simple effect where an animation plays once (when a user first logs in to my website), then they are brought to the main site. All the resources i've found online relate to loading screens whilst fetching data from an API. Here is my code for the loading screen:
import Typical from 'react-typical';
import "./Loading.scss";
import {useState, useEffect} from 'react';
const Loading = function Loading() {
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true)
setTimeout(() => {
setLoading(false)
},18000);
}, [])
return (
<div>
<h1 id="loading">
{ loading ? <Typical oop={1} wrapper = 'p'
steps={[
"Hello",
2000,
"My name is Leonard.",
3000,
"I am an aspiring developer",
3000,
"Welcome to my website!",
3000,
]} /> : null}
</h1>
</div>
);
}
I'm using a very simple react package called typical which gives a nice animation of words being typed on the screen like a typewriter, then gets deleted, then the next bit of text gets shown etc.. this only loops once. I've used the useState and useEffect hooks with a timer to setLoading to false in 18s which is when the animation stops. As you can see I render the animation only if loading is true, using a ternary operator, then once loading gets set to false then null gets displayed. My app.js looks like this:
import Nav from './Components/Nav';
import "./App.scss";
import Loading from './Components/Loading';
function App() {
return (
<div>
<Loading />
<main>
<Nav/>
</main>
</div>
);
}
export default App;
(Apologies for the awful JSX). The issue i'm having is that both navbar and the loading screen loads at the same time. I'm unsure how to hide all my other components until the animation is finished. Everything I try is waaay to complicated and really doesnt seem very efficient at all. I appreciate any help thank you!
The reason why they're both showing at the same time because in the app.js. You have both the <Loading /> component and the <Nav /> component being rendered at the same time.
There are two solutions you can go for
You can style the loading component to take the full screen and cover everything. This can be done by giving the loading screen an id. Say, loading-screen for example and do the following in css:
#loading-screen {
position: absolute;
width: 100%;
height: 100%;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
}
BUT make sure you after the loading time passes to hide the loading screen otherwise, it'll keep blocking the view.
You can put the loading logic in the app.js instead of it being inside the Loading component. You can have it render the Loading components as long as the loading state is true otherwise load the rest of the app (The navbar, etc..). You can do something like:
function App() {
const [loading, setLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setLoading(false)
}, 18000);
}, []);
return (
<div>
{loading ? (<Loading />) :
(
<main>
<Nav />
</main>
)
}
</div>
);
}

How to capture paste event anywhere on page in React?

Having looked at other stack overflow questions and on Google, I haven't been able to find an answer to this.
Background:
I'm currently re-writing a jquery application in React. Previously I was just adding a paste event listener to the body of the page. That seemed to allow me to capture any paste event the user would do.
Questions:
In React, with the code below in my App.js file, on initial load of the page it doesn't allow me to capture paste events. Only after clicking somewhere on the page does it then work when I paste. I realise I could just have an input and an onPaste attribute, but I need to be able to capture automatically.
In terms of passing the pasted text down the component tree, with the ability to edit the text from a lower component, am I right in thinking it's best to pass a callback as a prop down from the top-level component so the state is always updated using a function in the top-level component?
Currently this is what I have:
// App.js
import TopBar from "./components/TopBar/TopBar";
import Wrapper from "./components/Wrapper/Wrapper";
import AppContainer from "./components/AppContainer/AppContainer";
function App() {
const [inputText, setInputText] = useState(false);
return (
<div
onPaste={(e) => setInputText(e.clipboardData.getData("Text"))}
className="App"
>
<TopBar />
<Wrapper>
<h2>New Flashcard</h2>
<AppContainer inputText={inputText} setInputText={setInputText} />
</Wrapper>
</div>
);
}
export default App;
Your listener will only work when the specific div is focused, therefore it works on the second attempt. Instead, add the listener to body element or window object:
function Component() {
const [inputText, setInputText] = useState(false);
useEffect(() => {
window.addEventListener("paste", (e) => {
setInputText(e.clipboardData.getData("text"));
});
return () => {
window.removeEventListener("paste");
};
}, []);
return <div className="App">{inputText}</div>;
}
am I right in thinking it's best to pass a callback
No, it depends on the depth, please read about Context API and research on an anti-pattern called "props-drilling".

Testing `img.onLoad`/`img.onError` using Jest and React Testing Library

Out team ran into a scenario this afternoon when writing a React Testing Library (RTL) test with Jest for our <Avatar /> component. Specifically, we wanted to test that the <img /> tag was removed from the DOM when it fails to load (onError is triggered) to match the expected final view of component. For some reason, using fireEvent on the <img /> DOM Element was not immediately obvious to us and we didn't find this explicit solution online so we wanted share. As you can imagine, this will work for other events such as onLoad as well. More about RTL Events.
it('should render with only initials when avatar image is NOT found', async() => {
const { container } = render(<Avatar {...defaultMocks} />);
const avatarImg = container.querySelector('img');
expect(avatarImg).toBeInTheDocument();
fireEvent(avatarImg, new Event('error'));
expect(avatarImg).not.toBeInTheDocument();
});
get the image using testId or role then use fireEvent to call onLoad or OnError functions respectively
const image = getByRole('img')
fireEvent.load(image);
for onError
fireEvent.error(image)
Explanation
<Avatar /> is rendered with default props, and in our case, contains an <img /> tag pointing to an optional profile image endpoint for the user.
We then use fireEvent on that DOM element, triggering the onError function that was bound at render time mocking a failed/404 case where a user avatar was not set.
Finally we can now expect the <img /> was removed based on the logic within <Avatar />

Categories

Resources