Let me explain my problem I have been solving for all day.
I have a site with header which has of course by react-router links to other pages (home, projects, about, services, contact).
Have a Project component which is in '/projects' page and '/' (home) page.
I want to make a simple animation in Project.js component which depends if there is a 'vertical' or there is not this props. Clearly -> in '/projects' I want to do that animation on scroll - in other pages not.
Tried to do that by add if statement in useEffect but it's not working, get me an error 'cannot read property 'style' of null ref.current.style.transform = `translateY(${window.scrollY * -0.35}px)`;
This problem is showing up when I am changing pages in header i.eg. I am in '/projects' scrolling and is ok animation is working then go to '/' and when scroll got error I have showed above.
It is like my if statement is not working and when I am in '/' which Project component has props vertical={false} is making animation on scroll when I don't want to do that.
What I want? I want do make an animation using useEffect only if component has a props 'vertical' like this:
Project.js component code:
const Project = ({ image, className, vertical }) => {
const ref = useRef(null);
const [isVertical, setIsVertical] = useState(vertical);
useEffect(() => {
console.log('component did mount');
isVertical
? window.addEventListener('scroll', () => {
ref.current.style.transform = `translateY(${window.scrollY * -0.35}px)`;
})
: console.log('non-vertical');
}, [isVertical]);
useEffect(() => {
return () => console.log('unmount');
});
return <StyledProject image={image} className={className} vertical={vertical} ref={ref} />;
};
in home '/':
{images.map(({ image, id }) => (
<Project key={id} image={image} />
))}
in '/projects':
{images.map(({ image, id }) => (
<StyledProject vertical image={image} key={id} />
))}
when I am in the path '/projects' and go to another path got error.
It is like after being in '/projects' it is saving all statements was but I want on every page reset useEffect and ref.current
Please help me, I can't go further since I don't fix this.
Thanks in advance.
Main problem is that you are not removing event listener when component unmounts.
Here you can see an example how to do it.
Related
Problem Overview :
Building personal website with a Homepage.js, a Sidebar.js component, and a Project.js component.
Project components are mapped on Homepage from a project list array of objects.
Sidebar.js component will open and display active project info when a Project component is clicked on.
App.js has activeProject state, with a default set to first project in list so sidebar doesn't throw errors.
End result is that Sidebar displays correct project title, info, and video src (inspect mode) on Project click, but the video itself is wrong and always shows the placeholder's video set on App.js
Props passes right info, wrong video
Details :
In my App.js I set activeProject to the first project in my list as a default, via it's title.
const [activeProject, setActiveProject] = useState(ListOfProjects[0].title);
return (
<div className="App">
<Navigation />
<HomePage
isOpen={isOpen}
setIsOpen={setIsOpen}
activeProject={activeProject}
setActiveProject={setActiveProject}
/>
<Sidebar
isOpen={isOpen}
setIsOpen={setIsOpen}
activeProject={activeProject}
/>
<Footer />
</div>
);
My projects are mapped out on my Homepage.js
<div className="project-list">
{ListOfProjects.map((obj) => (
<ProjectItem
key={obj.id}
title={obj.title}
video={obj.video}
videoAlt={obj.videoAlt}
liveLink={obj.liveLink}
codeLink={obj.codeLink}
isOpen={isOpen}
setIsOpen={setIsOpen}
activeProject={activeProject}
setActiveProject={setActiveProject}
/>
))}
</div>
Each project component has a click event to open the sidebar component with extra project information based on the project title that was clicked on.
const showSidebar = () => {
setActiveProject(title);
setIsOpen(!isOpen);
}
On Sidebar.js I have this list of props being pulled from the project with a title that matches the current active project set state.
const {title, liveLink, codeLink, video, videoAlt, tags, details, challenges, lessons} = ListOfProjects.find((project) => {
return project.title === activeProject;
});
The result is that the sidebar component displays the correct title, project info, and even the right video src when shown in the inspection tool, but the project video is always of the first project in the list. Here is my website for more context (https://bryanfink.dev)
Things I have tried :
I tried changing the App.js activeProject state placeholder to the second project.
const [activeProject, setActiveProject] = useState(ListOfProjects[1].title);
This resulted in the Sidebar showing the correct project title, info, and video src(inspect mode) but now the video always shows the second project's video.
Then I tried changing the App.js activeProject state placeholder to ""
const [activeProject, setActiveProject] = useState("");
But this results in errors in the sidebar.
Sidebar.js:15 Uncaught TypeError: Cannot destructure property 'title' of '_Projects_listOfProjects__WEBPACK_IMPORTED_MODULE_2__.default.find(...)' as it is undefined
Try this, the find may return undefined if it can not find the data. So you can not destruct undefined. Adding || {} will check if ListOfProjects.find((project) => { return project.title === activeProject; }) is undefined, it will change to an empty object {}
const {title, liveLink, codeLink, video, videoAlt, tags, details, challenges, lessons} = ListOfProjects.find((project) => {
return project.title === activeProject;
}) || {};
Simply, create a default project
const defaultProject = {
title: "",
liveLink: "",
}
const {title, liveLink, codeLink, video, videoAlt, tags, details, challenges, lessons} = ListOfProjects.find((project) => {
return project.title === activeProject;
}) || defaultProject;
Also, make sure the title is unique or the .find will be returning wrong project.
I guess the issue is not the way you pass the data to your component. The issue is, when you rerender your component, it changes the src in your video <source /> tag but it does not actually reload the video, thus you see an old or wrong video even with the correct URL. I would suggest load() the video with each call to your child component that shows the video. For example call load in useEffect whenever the video source changed:
useEffect(() => {
document.querySelector("video").load();
}, [])
This will force the video to reload with the new src.
EDIT:
I cloned your project and the issue indeed is re-loading the video with the new source. Here is the solution. In your Sidebar.js add the following useEffect and add className="sidebar-video" to your <video /> tag. That should do it.
useEffect(() => {
document.querySelector(".sidebar-video").load();
}, [title]);
I have a problem I'm not able to solve. The app got a component where a do looping array and making multiple elements off it. Then I want to make buttons in another component that will scroll to a specific element. (something similar to liveuamap.com when you click on a circle).
I tried the below solution, but got "Uncaught TypeError: props.refs is undefined". I could not find any solution to fix it.
The second question: is there a better or different solution to make scrolling work?
In app component I creating refs and function for scrolling:
const refs = DUMMY_DATA.reduce((acc, value) => {
acc[value.id] = React.createRef();
return acc;
}, {});
const handleClick = (id) => {
console.log(refs);
refs[id].current.scrollIntoView({
behavior: "smooth",
block: "start",
});
};
The refs I send to the article component as a prop where I render elements with generated refs from the app component.
{props.data.map((article) => (
<ContentArticlesCard
key={article.id}
ref={props.refs[article.id]}
data={article}
onActiveArticle={props.onActiveArticle}
activeArticle={props.activeArticle}
/>
))}
The function is sent to another component as a prop where I create buttons from the same data with added function to scroll to a specific item in the article component.
{props.data.map((marker) => (
<Marker
position={[marker.location.lat, marker.location.lng]}
icon={
props.activeArticle === marker.id ? iconCircleActive : iconCircle
}
key={marker.id}
eventHandlers={{
click: () => {
props.onActiveArticle(marker.id);
// props.handleClick(marker.id);
},
}}
></Marker>
))}
Thanks for the answers.
Ok so i found the solution in library react-scroll with easy scroll implementation.
I'm currently working on a project using Next JS, where I seem to have encountered an issue with React. Here is the simplified version of my code. I did try to replicate the issue in codesandbox but I couldn't. I'll keep trying though and if I can, I'll update this post with the link.
const Nav = React.forwardRef<
HTMLDivElement,
{ className?: string; disableAnimation?: boolean }
>((props, ref) => {
const navWrapperRef = useRef<HTMLDivElement>(null);
const navItemsRef = useRef<HTMLSpanElement[]>([]);
useEffect(() => {
const path = window.location.pathname;
if (path === "/") navItemsRef.current[0].classList.add("nav-active");
else if (path.includes("/packages"))
navItemsRef.current[1].classList.add("nav-active");
else if (path.includes("/bhutan"))
navItemsRef.current[2].classList.add("nav-active");
}, []);
return (
<nav className={props.className || "nav"} ref={navWrapperRef}>
<div className="nav-container">
<ul className="nav-ul">
{navLinks.map((link) => (
<button
key={uniqueId(`nav-links-${new Date().getUTCDate()}`)}
data-name={link.name}
className="nav-links"
>
<span
ref={(el) => navItemsRef.current.push(el as HTMLSpanElement)}
className="nav-span"
>
<Link href={link.href}>{link.name}</Link>
</span>
</button>
))}
</ul>
</div>
</nav>
);
});
My objective here is to implement a navigation component without the use of states. I'd like to render out the current active navigation link on the initial page load using the empty dependency array as for the useEffect hook. But I can't seem to get it to work.
My desired output is the following on page load:
The output I get:
However, if I remove the dependency array altogether then all seems to work fine as expected. But if I am not wrong I think this causes performance issues as it re-renders each and every time if there are any other state changes. Any help would be greatly appreciated!
The contents of your useEffect hook will run once on mount and whenever its dependencies change.
As this is reliant on what you have defined as path, I'd move it out of the useEffect and add it as a dependency.
Update: you will have to use next/router's useRouter hook instead of the window directly when working with next.
Demo here.
const { asPath } = useRouter();
useEffect(() => {
if (asPath === "/") navItemsRef.current[0].classList.add("nav-active");
else if (asPath.includes("/packages"))
navItemsRef.current[1].classList.add("nav-active");
else if (asPath.includes("/bhutan"))
navItemsRef.current[2].classList.add("nav-active");
}, [asPath]);
It's my first week using the react library and I'm a bit confused with this piece where I have a list of items that are part of a side menu. I can assert their texts identifying them as the child of the parent List tag. But when I try to fire a click event this way it won't navigate me to the next page. It will only work if I target that list item by their text directly, not as the child of the parent list, which I'm trying to avoid because there may be other clickable elements with that same title on the page. If anyone could please point me towards what I may be lacking in understanding here please.
describe('App when it is rendered in a certain state:', () => {
beforeAll(() => {
render(<Root/>);
});
it('should render the dashboard after logging in', async () => {
expect(screen.queryByTestId('dashboard')).toBeInTheDocument();
expect(screen.getByTestId('side-menu')).toBeInTheDocument();
const liArr = screen.queryAllByRole('listitem')
expect(liArr[0].textContent).toBe('Dashboard')
expect(liArr[1].textContent).toBe('Schools')
expect(liArr[2].textContent).toBe('Teachers')
const teachersLink = liArr[2];
expect(teachersLink).toBeInTheDocument();
// await fireEvent.click(screen.getByText('Teachers')) // This navigates me fine
await fireEvent.click(teachersLink); // This does not navigate me
expect(screen.getByText('Teachers page')).toBeInTheDocument();
});
});
This is the SideMenu component where the links come from:
export default function SideMenu() {
const classes = useStyles();
return (
<div className={classes.root} data-testid="side-menu">
<List>
<ListItemLink to="/dashboard" primary="Dashboard"/>
<ListItemLink to="/schools" primary="Schools"/>
<ListItemLink to="/teachers" primary="Teachers"/>
</List>
</div>
);
}
Thanks very much.
It seems it was not working because the link item was actually not clickable, not being an anchor element. Changed it to the below piece and it worked, warranting uniqueness through the role of a button.
async function navigateToTeachersPage() {
await fireEvent.click(screen.getByRole('button', { name: 'Teachers' }));
expect(screen.getByTestId('teachers')).toBeInTheDocument();
}
So I'm trying to update my navbar background based on the page I am on, but when I console.log the window.location.pathname, it doesn't show the pathname properly.
Here is my function to change the navbar state based on the pathname
const [navbar, setNavbar] = useState(false)
const updateNavbar = () => {
if (window.location.pathname !== "/") {
setNavbar(true)
} else {
setNavbar(false)
}
console.log(window.location.pathname)
}
Here's my styled component
const Nav = styled.nav`
background: ${({ navbar }) => (navbar ? "red" : "blue")};
Then I pass this into my JSX
<Nav navbar={navbar}>
{menuData.map((item, index) => (
<NavLink to={item.link} key={index} onClick={updateNavbar}>
{item.title}
</NavLink>
))}
The issue is that if I click on my about menu item, in the console it shows
/
then if I click about again, then it shows
/about
So when I click about it changes the page visually on my screen, but the console logs /, so in order for the navbar to change colors, I have to click about again, which makes no sense. I pretty much have to click it twice to update the color.
Why doesn't the window.location.pathname automatically update to the page I am on instead of showing the previous link I clicked, then only after I click the link again it shows the correct path?
Also, I am using Gatsby JS and have AOS animations, but I don't think that matters.
Update: If I pass it into a useEffect hook, it shows / then /about, but why doesn't it only show me /about instead of showing both the previous and new link I clicked?
useEffect(() => {
updateNavbar()
}, [])
Your console print before your route is rendering!
So you can use:
setTimeout(function(){ console.log(window.location.pathname) }, 3000);
to print or get the path after your route change.
I suspect that code isn't working because you are setting your onClick function at the same time that it navigates to another page. What could work for you could be:
const updateNavbar = (destination) => {
if (typeof window !== 'undefined' && window.location.pathname !== "/") {
setNavbar(true)
navigate("destination")
} else if(typeof window !== 'undefined') {
setNavbar(false)
navigate("/")
}
console.log(window.location.pathname)
}
<NavLink key={index} onClick={updateNavbar(item.link)}>
{item.title}
</NavLink>
Basically, you are preventing the code to navigate to your page before it set the state. Changing the native to prop to navigate function.
Another workaround that may work for you is using the state from Gatsby <Link> component:
<Link
to={`/your/page`}
state={{ navbar: true }}
>
You can retrieve the value using: props.location.state.navbar.
I've added it to give you an approach. On the other hand, the code above will break in the build mode (gatsby build) since you are using window (a global object) and, since gatsby build occurs in the server, where there's no window, it will break the compilation. You should wrap every usage of window inside:
if (typeof window !== `undefined`) {
// your stuff
}
However, there's a more native approach. Since Gatsby extends from React (and from #react/router) it exposes at the top-level component (pages) a prop named location with all your desired information. Of course, you can pass downside from the page component to any atomized component to use it.
After some trials and errors we solved the issue by:
const updateNavbar = () => {
if (window.location.pathname !== "/") {
setNavbar(true)
} else {
setNavbar(false)
}
}
useEffect(() => {
if (window.location.pathname) {
setNavbar(window.location.pathname)
}
console.log(window.location.pathname)
}, [])
background: ${({ navbar }) => (navbar != "/" ? "#000" : "transparent")};