React Router useLocation() location is not followed to the current page - javascript

I'm using react-router-dom: "^6.2.2" in my project for a long time, but I don't know before that this version is not included useBlocker() and usePrompt(). So I'm found this solution and followed them. Then implemented into React Hook createContext() and useContext(). The dialog is displayed when changing route or refresh the page as expected.
But it has an error that useLocation() get the previous location despite the fact that I'm at the current page.
The NavigationBlocker code.
import React, { useState, useEffect, useContext, useCallback, createContext } from "react"
import { useLocation, useNavigate, UNSAFE_NavigationContext } from "react-router-dom"
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "#mui/material"
const navigationBlockerContext = createContext()
function NavigationBlocker(navigationBlockerHandler,canShowDialogPrompt) {
const navigator = useContext(UNSAFE_NavigationContext).navigator
useEffect(()=>{
console.log("useEffect() in NavigationBlocker")
if (!canShowDialogPrompt) return
// For me, this is the dark part of the code
// maybe because I didn't work with React Router 5,
// and it emulates that
const unblock = navigator.block((tx)=>{
const autoUnblockingTx = {
...tx,
retry() {
unblock()
tx.retry()
}
}
navigationBlockerHandler(autoUnblockingTx)
})
return unblock
})
}
function NavigationBlockerController(canShowDialogPrompt) {
// It's look like this function is being re-rendered before routes done that cause the useLocation() get the previous route page.
const navigate = useNavigate();
const currentLocation = useLocation();
const [showDialogPrompt, setShowDialogPrompt] = useState(false);
const [wantToNavigateTo, setWantToNavigateTo] = useState(null);
const [isNavigationConfirmed, setIsNavigationConfirmed] = useState(false);
const handleNavigationBlocking = useCallback(
(locationToNavigateTo) => {
// currentLocation.pathname is the previous route but locationToNavigateTo.location.pathname is the current route
if (!isNavigationConfirmed && locationToNavigateTo.location.pathname !== currentLocation.pathname) // {
setShowDialogPrompt(true);
setWantToNavigateTo(locationToNavigateTo);
return false;
}
return true;
},
[isNavigationConfirmed]
);
const cancelNavigation = useCallback(() => {
setIsNavigationConfirmed(false);
setShowDialogPrompt(false);
}, []);
const confirmNavigation = useCallback(() => {
setIsNavigationConfirmed(true);
setShowDialogPrompt(false);
}, []);
useEffect(() => {
if (isNavigationConfirmed && wantToNavigateTo) {
navigate(wantToNavigateTo.location.pathname);
setIsNavigationConfirmed(false)
setWantToNavigateTo(null)
}
}, [isNavigationConfirmed, wantToNavigateTo]);
NavigationBlocker(handleNavigationBlocking, canShowDialogPrompt);
return [showDialogPrompt, confirmNavigation, cancelNavigation];
}
function LeavingPageDialog({showDialog,setShowDialog,cancelNavigation,confirmNavigation}) {
const preventDialogClose = (event,reason) => {
if (reason) {
return
}
}
const handleConfirmNavigation = () => {
setShowDialog(false)
confirmNavigation()
}
const handleCancelNavigation = () => {
setShowDialog(true)
cancelNavigation()
}
return (
<Dialog fullWidth open={showDialog} onClose={preventDialogClose}>
<DialogTitle>ต้องการบันทึกการเปลี่ยนแปลงหรือไม่</DialogTitle>
<DialogContent>
<DialogContentText>
ดูเหมือนว่ามีการแก้ไขข้อมูลเกิดขึ้น
ถ้าออกจากหน้านี้โดยที่ไม่มีการบันทึกข้อมูล
การเปลี่ยนแปลงทั้งหมดจะสูญหาย
</DialogContentText>
</DialogContent>
<DialogActions>
<Button variant="outlined" color="error" onClick={handleConfirmNavigation}>
ละทิ้งการเปลี่ยนแปลง
</Button>
<Button variant="contained" onClick={handleCancelNavigation}>
กลับไปบันทึกข้อมูล
</Button>
</DialogActions>
</Dialog>
)
}
export function NavigationBlockerProvider({children}) {
const [showDialogLeavingPage,setShowDialogLeavingPage] = useState(false)
const [showDialogPrompt,confirmNavigation,cancelNavigation] = NavigationBlockerController(showDialogLeavingPage)
return (
<navigationBlockerContext.Provider value={{showDialog:setShowDialogLeavingPage}}>
<LeavingPageDialog showDialog={showDialogPrompt} setShowDialog={setShowDialogLeavingPage} cancelNavigation={cancelNavigation} confirmNavigation={confirmNavigation}/>
{children}
</navigationBlockerContext.Provider>
)
}
export const useNavigationBlocker = () => {
return useContext(navigationBlockerContext)
}
Expected comparison.
"/user_profile" === "/user_profile"
Error in comparison code.
"/user_profile" === "/home"
// locationToNavigateTo and currentLocation variable
The NavigationBlocker consumer code usage example.
function UserProfile() {
const prompt = useNavigatorBlocker()
const enablePrompt = () => {
prompt.showDialog(true)
}
const disablePrompt = () => {
prompt.showDialog(false)
}
}
The dialog image if it works correctly and if I click discard change, then route to the page that I clicked before. (Not pop-up when clicked anything except changing route.)
There is a bug that the dialog is poped-up when clicked at the menu bar button. When I clicked discard change the page is not changed.
Thank you, any help is appreciated.

From what I can see your useNavigationBlockerController hook handleNavigationBlocking memoized callback is missing a dependency on the location.pathname value. In other words, it is closing over and referencing a stale value.
Add the missing dependencies:
const navigationBlockerContext = createContext();
...
function useNavigationBlockerHandler(
navigationBlockerHandler,
canShowDialogPrompt
) {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
if (!canShowDialogPrompt) return;
// For me, this is the dark part of the code
// maybe because I didn't work with React Router 5,
// and it emulates that
const unblock = navigator.block((tx) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
}
};
navigationBlockerHandler(autoUnblockingTx);
});
return unblock;
});
}
...
function useNavigationBlockerController(canShowDialogPrompt) {
// It's look like this function is being re-rendered before routes done that cause the useLocation() get the previous route page.
const navigate = useNavigate();
const currentLocation = useLocation();
const [showDialogPrompt, setShowDialogPrompt] = useState(false);
const [wantToNavigateTo, setWantToNavigateTo] = useState(null);
const [isNavigationConfirmed, setIsNavigationConfirmed] = useState(false);
const handleNavigationBlocking = useCallback(
(locationToNavigateTo) => {
// currentLocation.pathname is the previous route but locationToNavigateTo.location.pathname is the current route
if (
!isNavigationConfirmed &&
locationToNavigateTo.location.pathname !== currentLocation.pathname
) {
setShowDialogPrompt(true);
setWantToNavigateTo(locationToNavigateTo);
return false;
}
return true;
},
[isNavigationConfirmed, currentLocation.pathname] // <-- add current pathname
);
const cancelNavigation = useCallback(() => {
setIsNavigationConfirmed(false);
setShowDialogPrompt(false);
}, []);
const confirmNavigation = useCallback(() => {
setIsNavigationConfirmed(true);
setShowDialogPrompt(false);
}, []);
useEffect(() => {
if (isNavigationConfirmed && wantToNavigateTo) {
navigate(wantToNavigateTo.location.pathname);
setIsNavigationConfirmed(false);
setWantToNavigateTo(null);
}
}, [isNavigationConfirmed, navigate, wantToNavigateTo]); // <-- add navigate
useNavigationBlockerHandler(handleNavigationBlocking, canShowDialogPrompt);
return [showDialogPrompt, confirmNavigation, cancelNavigation];
}
...
export function NavigationBlockerProvider({ children }) {
const [showDialogLeavingPage, setShowDialogLeavingPage] = useState(false);
const [
showDialogPrompt,
confirmNavigation,
cancelNavigation
] = useNavigationBlockerController(showDialogLeavingPage);
return (
<navigationBlockerContext.Provider
value={{ showDialog: setShowDialogLeavingPage }}
>
<LeavingPageDialog
showDialog={showDialogPrompt}
setShowDialog={setShowDialogLeavingPage}
cancelNavigation={cancelNavigation}
confirmNavigation={confirmNavigation}
/>
{children}
</navigationBlockerContext.Provider>
);
}
...
export const useNavigationBlocker = () => {
return useContext(navigationBlockerContext);
};

Related

React useEffect hook will cause an infinite loop if I add tasksMap to the depedency array

If I add tasksMap to the useEffect dependency array below an infinite loop will happen. Can someone point me in the right direction on how to fix this? In order for the user to get an updated view of the tasks that have been added or modified, I need the app to call getProjectTasks and assign the returned map to the tasksMap. I do know that anytime you update state the component rerenders. I just havne't figured out how to do this without creating an infinite loop. Any help is greatly appreciated. Thank you.
import { useContext, useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { UserContext } from "../../contexts/user.context";
import { ProjectsContext } from "../../contexts/projects.context";
import { createProjectTask, getProjectTasks } from "../../utils/firebase/firebase.utils";
import OutlinedCard from "../../components/cards/TaskCard.component";
import { TextField, Button, } from "#mui/material";
import "./project.styles.css";
import "./project.styles.css";
const Project = () => {
const params = useParams();
const projectId = params.id;
const { currentUser } = useContext(UserContext);
const { projectsMap } = useContext(ProjectsContext);
const [taskName, setTaskName] = useState("");
const [tasksMap, setTasksMap] = useState({});
const project = Object.keys(projectsMap)
.filter((id) => id.includes(projectId))
.reduce((obj, id) => {
return Object.assign(obj, {
[id]: projectsMap[id],
});
}, {});
useEffect(() => {
console.log("running")
const getTasksMap = async () => {
const taskMap = await getProjectTasks(currentUser, projectId);
taskMap ? setTasksMap(taskMap) : setTasksMap({});
};
getTasksMap();
}, [projectId])
const handleChange = (event) => {
const { value } = event.target;
setTaskName(value);
};
const handleSubmit = async (event) => {
event.preventDefault();
try {
await createProjectTask(currentUser, projectId, taskName);
setTaskName("");
} catch (error) {
console.log(error);
}
};
return (
<div className="project-container">
{project[projectId] ? <h2>{project[projectId].name}</h2> : ""}
<form onSubmit={handleSubmit} className="task-form">
<TextField label="Project Task" onChange={handleChange} value={taskName}></TextField>
<Button type="submit" variant="contained">
Add Task
</Button>
</form>
<div className="tasks-container">
{Object.keys(tasksMap).map((id) => {
const task = tasksMap[id];
return (
<OutlinedCard key={id} projectId={projectId} taskId={id} name={task.name}></OutlinedCard>
);
})}
</div>
</div>
);
};
export default Project;
This is where the taskMap object comes from. For clarification, I'm using Firebase.
export const getProjectTasks = async(userAuth, projectId) => {
if(!userAuth || !projectId) return;
const tasksCollectionRef = collection(db, "users", userAuth.uid, "projects", projectId, "tasks")
const q = query(tasksCollectionRef);
try {
const querySnapshot = await getDocs(q);
const taskMap = querySnapshot.docs.reduce((acc, docSnapshot) => {
const id = docSnapshot.id;
const { name } = docSnapshot.data();
acc[id] = {id, name};
return acc;
}, {});
return taskMap;
} catch (error) {
console.log("Error getting task docs.");
}
};
The useEffect appears to be setting tasksMap when executed. Because this state is an object its reference will change everytime, which will produce an infinite loop

React Hook useEffect has a missing dependency: 'handleLogout'. Either include it or remove the dependency array react

import { useState, useEffect } from "react";
import LoginModal from "./LoginModal";
import { NavLink, useLocation, useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { userLogout } from "../Features/User/userSlice";
import decode from "jwt-decode";
const Header = ({ toggleModalShow, showModal }) => {
const [burgerAnimation, setBurgerAnimation] = useState(false);
const [user, setUser] = useState();
const location = useLocation();
const dispatch = useDispatch();
const navigate = useNavigate();
// for showing login/sign up modal
const showModalButton = () => {
toggleModalShow();
};
const handleBurgerAnimation = () => {
setBurgerAnimation(!burgerAnimation);
};
const handleLogout = async (id) => {
await dispatch(userLogout({ id, navigate, dispatch }));
setUser(null);
};
const burgerListItemAnimation = ...
const burgerIconAnimation = ...
const guestHeader = (
<ul>
...
</ul>
);
const userHeader = (
<ul>
...
</ul>
);
useEffect(() => {
if (localStorage.getItem("user") && !user) {
setUser(JSON.parse(localStorage.getItem("user")));
}
const accessToken = user?.accessToken;
if (accessToken) {
const decodedAccessToken = decode(accessToken);
if(decodedAccessToken.exp * 1000 < new Date().getTime()){
handleLogout(user.user._id);
}
console.log(decodedAccessToken);
}
}, [location, user]);
return (
<header className="header">
...
</header>
);
};
export default Header;
Hi all.I just wanted to try to log out the user when the expiration date is over. If i put 'handleLogout' to useEffect dependicies warning doesnt change. Why am i getting this warning ? What kind of warning may i get if i dont fix that ? And finally, if you have time to review the repo, would you give feedback ?
repo : https://github.com/UmutPalabiyik/mook
If you keep handleLogout external to the useEffect hook it should be listed as a dependency as it is referenced within the hook's callback.
If i put handleLogout to useEffect dependencies warning doesn't
change.
I doubt the warning is the same. At this point I would expect to you to see the warning change to something like "the dependency handleLogout is redeclared each render cycle, either move it into the useEffect hook or memoize with useCallback..." something to that effect.
From here you've the 2 options.
Move handleLogout into the useEffect so it is no longer an external dependency.
useEffect(() => {
const handleLogout = async (id) => {
await dispatch(userLogout({ id, navigate, dispatch }));
setUser(null);
};
if (localStorage.getItem("user") && !user) {
setUser(JSON.parse(localStorage.getItem("user")));
}
const accessToken = user?.accessToken;
if (accessToken) {
const decodedAccessToken = decode(accessToken);
if (decodedAccessToken.exp * 1000 < new Date().getTime()) {
handleLogout(user.user._id);
}
console.log(decodedAccessToken);
}
}, [location, user, id, navigate, dispatch]);
Memoize handleLogout with useCallback so it's a stable reference and add it to the effect's dependencies.
const handleLogout = useCallback(async (id) => {
await dispatch(userLogout({ id, navigate, dispatch }));
setUser(null);
}, [id, navigate, dispatch]);
...
useEffect(() => {
if (localStorage.getItem("user") && !user) {
setUser(JSON.parse(localStorage.getItem("user")));
}
const accessToken = user?.accessToken;
if (accessToken) {
const decodedAccessToken = decode(accessToken);
if (decodedAccessToken.exp * 1000 < new Date().getTime()) {
handleLogout(user.user._id);
}
console.log(decodedAccessToken);
}
}, [location, user, handleLogout]);

Dont show me again message with react hooks

I have modal which shows when user visit my page now I want a user to be able to hide this modal so that doesn't show the modal again using local storage or cookies
Here is a live demo on code sandbox : dont show me again
Js code
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import Modal from "./Modal";
import useModal from "./useModal";
import "./styles.css";
const App = () => {
const { isShowing, toggle } = useModal();
const [cookieConsent, showCookieConsent] = useState(true);
const [checked, setIsChecked] = useState(0);
const handleOnchange = (e) => {
setIsChecked(e.target.value);
};
let modalStorage = localStorage.setItem("hide", checked);
useEffect(() => {
toggle();
if (modalStorage) {
showCookieConsent(false);
}
}, []);
const clearStorage = () => {
localStorage.clear();
};
return (
<div className="App">
<button onClick={clearStorage}> Clear Storage</button>
{cookieConsent === false && (
<Modal
isShowing={isShowing}
handleOnchange={handleOnchange}
hide={toggle}
/>
)}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Now when I click the checkbox and close the modal and I refresh the page it shows again instead of hiding it
What am I doing wrong here?
I changed a few things
const handleOnchange = (e) => {
setIsChecked(e.target.checked);
localStorage.setItem("hide", e.target.checked)
};
useEffect(() => {
toggle();
let modalStorage = localStorage.getItem("hide", checked);
if (modalStorage) {
showCookieConsent(false);
}
}, []);
I've used a custom hook to solve a similar issue. Sharing the code in case it helps anyone.
// useInfoBanner.jsx
import { useEffect, useState } from "react";
import { getFromLocalStorage, setToLocalStorage } from "utils/storage";
const useInfoBanner = (key) => {
const [showInfoBanner, setShowInfoBanner] = useState(false);
const handleHideInfoBanner = () => {
setShowInfoBanner(false);
setToLocalStorage(key, true);
};
useEffect(() => {
const isHideInfoBanner = getFromLocalStorage(key);
isHideInfoBanner ? setShowInfoBanner(false) : setShowInfoBanner(true);
}, []);
return [showInfoBanner, handleHideInfoBanner];
};
export default useInfoBanner;
// utils/storage.js
export const getFromLocalStorage = key =>JSON.parse(localStorage.getItem(key));
export const setToLocalStorage = (key, value) =>
localStorage.setItem(key, JSON.stringify(value));
// You can use this in your components like so
const [showInfoBanner, handleHideInfoBanner] = useInfoBanner("hideSettingsInfoBanner");
//.. then in return
{showInfoBanner && <InfoBanner />}

history.push() using react-router-dom works in some components but not others

So as the title says. I'm using React-router-dom and so within my App.js file i have my Router set up containing a Switch and multiple Routes. From a couple of smaller components i have no problem using useHisory and history.push() to manipulate the history and navigate my app.
However within my App.js file it doesn't work and i get returned:
"TypeError: Cannot read property 'push' of undefined"
I'm at a loss as to what is the problem and any help would be much appriciated.
import React, { useState, useEffect } from "react";
import {
BrowserRouter as Router,
Route,
Switch,
useHistory,
} from "react-router-dom";
import styled from "styled-components";
import unsplash from "../api/unsplash";
import Header from "./Header";
import Customise from "./Customise";
import LandingPage from "./LandingPage";
import GameBoard from "./GameBoard";
import GameFinished from "./GameFinished";
function App() {
const [searchImageTerm, setSearchImageTerm] = useState("south africa");
const [images, setImages] = useState([]);
const [randomisedImages, setRandomisedImages] = useState([]);
const [roundStarted, setRoundStarted] = useState(false);
const [firstSelectedTile, setFirstSelectedTile] = useState(null);
const [secondSelectedTile, setSecondSelectedTile] = useState(null);
const [matchedTiles, setMatchedTiles] = useState([]);
const [endOfTurn, setEndOfTurn] = useState(false);
const [score, setScore] = useState(0);
const [minutes, setMinutes] = useState(2);
const [seconds, setSeconds] = useState(0);
const [difficulty, setDifficulty] = useState(8);
const history = useHistory();
useEffect(() => {
getImages();
}, [searchImageTerm, difficulty]);
useEffect(() => {
randomiseImagesWithID(images);
}, [images]);
useEffect(() => {
if (minutes === 0 && seconds === 0) {
finishGame();
}
}, [seconds, minutes]);
const finishGame = () => {
history.push(`/gamefinished`);
};
useEffect(() => {
if (roundStarted) {
let myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval);
} else {
setMinutes(minutes - 1);
setSeconds(59);
}
}
}, 1000);
return () => {
clearInterval(myInterval);
};
}
});
useEffect(() => {
if (matchedTiles.length > 0 && matchedTiles.length === images.length / 2) {
alert("YOU WON!");
}
}, [matchedTiles]);
const getImages = async () => {
const response = await unsplash.get("/search/photos", {
params: { query: searchImageTerm, per_page: difficulty },
});
setImages(response.data.results);
};
const generateTileId = () => {
return "tile_id_" + Math.random().toString().substr(2, 8);
};
const randomiseImagesWithID = (images) => {
let duplicateImagesArray = [...images, ...images];
var m = duplicateImagesArray.length,
t,
i;
while (m) {
i = Math.floor(Math.random() * m--);
t = duplicateImagesArray[m];
duplicateImagesArray[m] = duplicateImagesArray[i];
duplicateImagesArray[i] = t;
}
let finalArray = [];
for (let image of duplicateImagesArray) {
finalArray.push({
...image,
tileId: generateTileId(),
});
}
setRandomisedImages([...finalArray]);
};
const startRound = () => {
setRoundStarted(true);
};
const onTileClick = (tileId, id) => {
// is the tile already paired && is the tile selected && is it the end of the turn?
if (
!matchedTiles.includes(id) &&
tileId !== firstSelectedTile &&
!endOfTurn
) {
// find image id for first selcted id for comparrison
const firstSelctedTileId = randomisedImages.find(
(image) => image.tileId === firstSelectedTile
)?.id;
// if there is no selected tile set first selected tile
if (!firstSelectedTile) {
setFirstSelectedTile(tileId);
} else {
// if the second tile matches the first tile set matched tiles to include
if (id === firstSelctedTileId) {
setMatchedTiles([...matchedTiles, id]);
// add points to score
setScore(score + 6);
// reset selected tiles
setFirstSelectedTile(null);
} else {
// deduct points from score
setScore(score - 2);
// set and display second tile choice
setSecondSelectedTile(tileId);
// set end of turn so tiles cannot be continued to be selected
setEndOfTurn(true);
// reset all values after a few seconds
setTimeout(() => {
setFirstSelectedTile(null);
setSecondSelectedTile(null);
setEndOfTurn(false);
}, 1500);
}
}
}
};
const onResetClick = () => {
randomiseImagesWithID(images);
setFirstSelectedTile(null);
setSecondSelectedTile(null);
setMatchedTiles([]);
setScore(0);
setEndOfTurn(false);
};
return (
<div>
<Router>
<Container>
<Header
onResetClick={onResetClick}
score={score}
seconds={seconds}
minutes={minutes}
/>
<Main>
<Switch>
<Route path="/gameboard">
<GameBoard
images={randomisedImages}
onTileClick={onTileClick}
firstSelectedTile={firstSelectedTile}
secondSelectedTile={secondSelectedTile}
matchedTiles={matchedTiles}
/>
</Route>
<Route path="/customise">
<Customise
setSearchImageTerm={setSearchImageTerm}
setDifficulty={setDifficulty}
setMinutes={setMinutes}
startRound={startRound}
/>
</Route>
<Route path="/gamefinished">
<GameFinished />
</Route>
<Route path="/">
<LandingPage startRound={startRound} />
</Route>
</Switch>
</Main>
</Container>
</Router>
</div>
);
}
export default App;
const Container = styled.div`
width: 100%;
height: 100vh;
display: grid;
grid-template-rows: 7rem;
`;
const Main = styled.div`
display: grid;
grid-template-columns: auto;
`;
And to give an example of where my code is working as expected:
import React from "react";
import { useHistory } from "react-router-dom";
import styled from "styled-components";
function LandingPage({ startRound }) {
const history = useHistory();
const startGame = () => {
history.push(`/gameboard`);
startRound();
};
const customiseGame = () => {
history.push("/customise");
};
return (
<Container>
<WelcomeText>
<p>Match the tiles by picking two at a time.</p>
<p>Gain points for a correct match but lose points if they dont.</p>
<p>Good Luck!</p>
</WelcomeText>
<ButtonContainer>
<GameButton onClick={() => startGame()}>Start</GameButton>
<GameButton onClick={() => customiseGame()}>Customise</GameButton>
</ButtonContainer>
</Container>
);
}
The reason why you are getting TypeError: Cannot read property 'push' of undefined is because you have initialized/assigned history before render has returned (hence the router never populated.
const history = useHistory();
Change it to this and everything should be working as expected (Warning: I haven't tested it myself):
const finishGame = () => {
const history = useHistory();
history.push(`/gamefinished`);
};
It will work because finishGame is only called inside useEffect which is called after the page is rendered.
You can pass history prop one component to another component.
like
// First component
import { useHistory } from "react-router-dom";
const firstComponent = () => {
const history = useHistory();
return (
<SecondComponent history=history />
)
}
const SecondComponent = ({history}) => (
....
);
I don't see any problems with your code. Try this
npm uninstall react-router-dom && npm i react-router-dom
Then try again.

useState not updating Grandparent state - data passes from Grandchild to child succesfully, but no further

I have an 'Autocomplete' grandchild React component which takes data from the child component, helps the user autocomplete a form field, and then passes the value right back up to the grandparent component which posts the whole form.
I am able to get data from grandchild to child, but not able to get the data from there up to the grandparent.
Grandparent - AddPost.jsx
import React, { useState } from 'react';
import GetBeerStyle from './GetBeerStyle';
export default function AddPost(props) {
const [parentBeerStyleData, setParentBeerStyleData] = useState("")
const handleSubmit = (e) => {
e.preventDefault();
// There's some code here that pulls the data together and posts to the backend API
}
return (
<div>
<GetBeerStyle
name="beerstyle"
beerStyleData={childData => setParentBeerStyleData(childData)}
onChange={console.log('Parent has changed')}
/>
// More data input fields are here...
</div>
);
}
Child - GetBeerStyle.jsx
import React, {useState, useEffect } from 'react';
import axios from 'axios';
import Autocomplete from '../Autocomplete';
export default function GetBeerStyle(props) {
const [beerStyleData, setBeerStyleData] = useState("")
const [beerStyles, setBeerStyles] = useState(null)
const apiURL = "http://localhost:8080/api/styles/"
// Code to fetch the beer styles from the API and push down to the grandchild
// component to enable autocomplete
const fetchData = async () => {
const response = await axios.get(apiURL)
const fetchedStyles = Object.values(response.data)
const listOfStyles = []
for (let i = 0; i < fetchedStyles.length; i++) {
(listOfStyles[i] = fetchedStyles[i].style_name)
}
setBeerStyles(listOfStyles)
};
// This snippet pulls data from the Posts table via the API when this function is called
useEffect(() => {
fetchData();
}, []);
return (
<div className="one-cols">
<Autocomplete
suggestions={ beerStyles } // sending the data down to gchild
parentUpdate={childData => setBeerStyleData(childData)}// passing data back to gparent
onChange={() => props.beerStyleData(beerStyleData)}
/>
</div>
);
}
Grandchild - Autocomplete.jsx
import React, { Component, Fragment, useState } from 'react';
export default function Autocomplete(props) {
const [activeSuggestion, setActiveSuggestion] = useState(0);
const [filteredSuggestions, setFilteredSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [userInput, setUserInput] = useState("");
const [fieldId, setFieldId] = useState("");
const [parentUpdate, setParentUpdate] = useState("");
const onChange = e => {
const { suggestions } = props;
setActiveSuggestion(0);
setFilteredSuggestions(suggestions.filter(suggestion => suggestion.toLowerCase().indexOf(userInput.toLowerCase()) >-1));
setShowSuggestions(true);
setUserInput(e.currentTarget.value);
setParentUpdate(e.currentTarget.value);
(console.log(parentUpdate));
return props.parentUpdate(parentUpdate);
};
const onClick = e => {
setActiveSuggestion(0);
setFilteredSuggestions([]);
setShowSuggestions(false);
setUserInput(e.currentTarget.innerText);
setFieldId(props.fieldId);
setParentUpdate(e.currentTarget.innerText);
return props.parentUpdate(parentUpdate);
};
const onKeyDown = e => {
// User pressed the ENTER key
if (e.keyCode === 13) {
setActiveSuggestion(0);
setShowSuggestions(false);
setUserInput(filteredSuggestions[activeSuggestion]);
// User pressed the UP arrow
} else if (e.keyCode === 38) {
if (activeSuggestion === 0) {
return;
}
setActiveSuggestion(activeSuggestion - 1);
}
// User pressed the DOWN arrow
else if (e.keyCode === 40) {
if (activeSuggestion - 1 === filteredSuggestions.length) {
return;
}
setActiveSuggestion(activeSuggestion + 1);
}
};
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul class="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className;
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion} onClick={onClick}>
{suggestion}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div class="no-suggestions">
<em>No Suggestions Available.</em>
</div>
);
}
}
return (
<Fragment>
<input
type="text"
value={userInput}
onChange={onChange}
onKeyDown={onKeyDown}
id={fieldId}
/>
<div>
{suggestionsListComponent}
</div>
</Fragment>
);
}
While I certainly accept there may be other issues with the code, overriding problem that I seem to have spent an inordinate amount of time googling and researching, is that I can't get the data being entered in the form input, to pull through to the grandparent component!
What have I missed?

Categories

Resources