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 />}
Related
I Can't render my events. Its showing this error -
"Cannot update a component (App) while rendering a different component (EventList). To locate the bad setState() call inside EventList, follow the stack trace as described in https://reactjs.org/link/setstate-in-render"
Here is EventList Component code -
import { useEffect, useState } from "react";
import EventList from "../../event-list";
import EventForm from "../event-form";
const EventAction = ({
getEventsByClockID,
addEvent,
updateEvent,
clockID,
deleteEvent,
deleteEventsByClockID,
}) => {
const [isCreate, setIsCreate] = useState(false);
const [isToggle, setIsToggle] = useState(false);
const [eventState, setEventState] = useState(null)
const handleCreate = () => {
setIsCreate(!isCreate);
}
useEffect(() => {
setEventState(getEventsByClockID(clockID, true));
}, [isToggle])
const handleToggle = () => {
setIsToggle(!isToggle);
}
return (
<div>
<div>
<button onClick={handleCreate}>Create Event</button>
<button onClick={handleToggle}>Toggle Events</button>
</div>
{isCreate && (
<>
<h3>Create Event</h3>
<EventForm
clockID={clockID}
handleEvent={addEvent}
/>
</>
)}
{isToggle && (
<>
<h3>Events of this clock</h3>
<EventList
clockID={clockID}
eventState={eventState}
deleteEvent={deleteEvent}
updateEvent={updateEvent}
deleteEventsByClockID={deleteEventsByClockID}
/>
</>
)}
</div>
)
}
export default EventAction;
Here is my App Component Code -
import ClockList from "./components/clock-list";
import LocalClock from "./components/local-clock";
import useApp from "./hooks/useApp";
import { localClockInitState } from "./initialStates/clockInitState";
const App = () => {
const {
localClock,
clocks,
updateLocalClock,
createClock,
updateClock,
deleteClock,
getEventsByClockID,
addEvent,
deleteEvent,
updateEvent,
deleteEventsByClockID,
} = useApp(localClockInitState);
return (
<div>
<LocalClock
clock={localClock}
updateClock={updateLocalClock}
createClock={createClock}
/>
<ClockList
clocks={clocks}
localClock={localClock.date}
updateClock={updateClock}
deleteClock={deleteClock}
getEventsByClockID={getEventsByClockID}
addEvent={addEvent}
deleteEvent={deleteEvent}
updateEvent={updateEvent}
deleteEventsByClockID={deleteEventsByClockID}
/>
</div>
)
}
export default App;
and Here is my useApp hook -
import { useState } from "react";
import deepClone from "../utils/deepClone";
import generateID from "../utils/generateId";
import useEvents from "./useEvents";
const getID = generateID('clock');
const useApp = (initValue) => {
const [localClock, setLocalClock] = useState(deepClone(initValue));
const [clocks, setClocks] = useState([]);
const {
// events,
// getEvents,
getEventsByClockID,
addEvent,
deleteEvent,
deleteEventsByClockID,
updateEvent,
} = useEvents();
const updateLocalClock = (data) => {
setLocalClock({
...localClock,
...data,
})
}
const createClock = (clock) => {
clock.id = getID.next().value;
setClocks((prev) => ([
...prev, clock
]))
}
const updateClock = (updatedClock) => {
setClocks(clocks.map(clock => {
if(clock.id === updatedClock.id) return updatedClock;
return clock;
}));
}
const deleteClock = (id) => {
setClocks(clocks.filter(clock => clock.id !== id));
}
return {
localClock,
clocks,
updateLocalClock,
createClock,
updateClock,
deleteClock,
getEventsByClockID,
addEvent,
deleteEvent,
updateEvent,
deleteEventsByClockID,
}
}
export default useApp;
I want to show all events incorporated with each individual clock.
I have a pagination made with a react-paginate package called react-paginate here is the link to the doc. https://www.npmjs.com/package/react-paginate
I have implemented it in my App which is a notes diary, the user creates notes and these are dynamically saved in the localStorage and displayed on screen, well, I have established that there are 6 notes per page, that is, when there is a seventh note, it should not be displayed unless the user goes to page 2, when there are 13 notes page 3 and so ...
The functionality of my component that I have called Pagination works correctly, it is dynamic, I have right now testing 13 notes, so it shows me 3 pages, if I had 12, it would show me 2.
The problem is that although my pagination is correct, the 13 notes are being shown on the screen, when it should be 6 - 6 - 1.
I leave you the code to see if we can find the error, greetings and thanks in advance.
The prop that Pagination receives called data, are basically the notes that are created dynamically in App.js. const [notes, setNotes] = useState([]);
Component Pagination
import React, { useEffect, useState } from 'react'
import ReactPaginate from 'react-paginate';
import '../styles/Pagination.css';
const Pagination = (props) => {
const { data } = props;
// We start with an empty list of items.
const [currentItems, setCurrentItems] = useState([]);
const [pageCount, setPageCount] = useState(0);
// Here we use item offsets; we could also use page offsets
// following the API or data you're working with.
const [itemOffset, setItemOffset] = useState(0);
const itemsPerPage = 6;
useEffect(() => {
// Fetch items from another resources.
const endOffset = itemOffset + itemsPerPage;
console.log(`Loading items from ${itemOffset} to ${endOffset}`);
setCurrentItems(data.slice(itemOffset, endOffset));
setPageCount(Math.ceil(data.length / itemsPerPage));
}, [itemOffset, itemsPerPage, data]);
// Invoke when user click to request another page.
const handlePageClick = (event) => {
const newOffset = (event.selected * itemsPerPage) % data.length;
console.log(
`User requested page number ${event.selected}, which is offset ${newOffset}`
);
setItemOffset(newOffset);
};
return (
<>
<ReactPaginate
breakLabel="..."
nextLabel="next >"
onPageChange={handlePageClick}
pageRangeDisplayed={3}
pageCount={pageCount}
previousLabel="< previous"
renderOnZeroPageCount={null}
containerClassName="pagination"
pageLinkClassName="page-num"
previousLinkClassName="page-num"
nextLinkClassName="page-num"
activeLinkClassName="activee boxx"
/>
</>
);
}
export default Pagination;
Component App
import { useState, useEffect } from "react";
import { nanoid } from 'nanoid';
import NoteList from "./components/NoteList";
import './App.css';
import Search from "./components/Search";
import Header from "./components/Header";
import Pagination from "./components/Pagination";
const App = () => {
const [notes, setNotes] = useState([]);
const [searchText, setSearchText] = useState('');
const [darkMode, setDarkMode] = useState(false);
//Se encarga de mostrar la nota para escribir
const [showNote, setShowNote] = useState(true); //eslint-disable-line
useEffect(() => {
const saveNotes = JSON.parse(localStorage.getItem('notes-data'));
if (saveNotes){
setNotes(saveNotes);
}
}, []);
useEffect(() => {
localStorage.setItem('notes-data', JSON.stringify(notes))
},[notes])
const addNote = (inputText, text) => {
const date = new Date();
const newNote = {
id: nanoid(),
title: inputText,
text: text,
date: date.toLocaleString()
}
const newNotes = [newNote, ...notes];
setNotes(newNotes)
}
const deleteNote = (id) => {
var response = window.confirm("Are you sure that you want to remove the note?");
if (response){
const notesUpdated = notes.filter((note) => note.id !== id)
setNotes(notesUpdated);
}
}
return (
<div className={darkMode ? 'dark-mode' : ''}>
<div className="container">
<Header
handleToggleTheme={setDarkMode}
/>
<Search
handleSearchNote={setSearchText}
setShowNote={setShowNote}
/>
<NoteList
notes={notes.filter((noteText) =>
noteText.title.toLowerCase().includes(searchText)
)}
handleAddNote={addNote}
handleDeleteNote={deleteNote}
/>
<Pagination data={notes}/>
</div>
</div>
)
}
export default App;
The problem is you are not using currentItems and the paginated data is stored in that state.
Codesandbox: https://codesandbox.io/s/sweet-keldysh-2u72vd
Pagination.js
import React, { useEffect, useState } from 'react'
import ReactPaginate from 'react-paginate';
import NoteList from "./components/NoteList";
import '../styles/Pagination.css';
const Pagination = (props) => {
const { data, searchText, handleAddNote, handleDeleteNote } = props;
// We start with an empty list of items.
const [currentItems, setCurrentItems] = useState([]);
const [pageCount, setPageCount] = useState(0);
// Here we use item offsets; we could also use page offsets
// following the API or data you're working with.
const [itemOffset, setItemOffset] = useState(0);
const itemsPerPage = 6;
useEffect(() => {
// Fetch items from another resources.
const endOffset = itemOffset + itemsPerPage;
console.log(`Loading items from ${itemOffset} to ${endOffset}`);
setCurrentItems(data.slice(itemOffset, endOffset));
setPageCount(Math.ceil(data.length / itemsPerPage));
}, [itemOffset, itemsPerPage, data]);
// Invoke when user click to request another page.
const handlePageClick = (event) => {
const newOffset = (event.selected * itemsPerPage) % data.length;
console.log(
`User requested page number ${event.selected}, which is offset ${newOffset}`
);
setItemOffset(newOffset);
};
return (
<>
<NoteList
notes={currentItems.filter((noteText) =>
noteText.title.toLowerCase().includes(searchText)
)}
handleAddNote={handleAddNote}
handleDeleteNote={handleDeleteNote}
/>
<ReactPaginate
breakLabel="..."
nextLabel="next >"
onPageChange={handlePageClick}
pageRangeDisplayed={3}
pageCount={pageCount}
previousLabel="< previous"
renderOnZeroPageCount={null}
containerClassName="pagination"
pageLinkClassName="page-num"
previousLinkClassName="page-num"
nextLinkClassName="page-num"
activeLinkClassName="activee boxx"
/>
</>
);
}
export default Pagination;
App.js
import { useState, useEffect } from "react";
import { nanoid } from 'nanoid';
import './App.css';
import Search from "./components/Search";
import Header from "./components/Header";
import Pagination from "./components/Pagination";
const App = () => {
const [notes, setNotes] = useState([]);
const [searchText, setSearchText] = useState('');
const [darkMode, setDarkMode] = useState(false);
//Se encarga de mostrar la nota para escribir
const [showNote, setShowNote] = useState(true); //eslint-disable-line
useEffect(() => {
const saveNotes = JSON.parse(localStorage.getItem('notes-data'));
if (saveNotes){
setNotes(saveNotes);
}
}, []);
useEffect(() => {
localStorage.setItem('notes-data', JSON.stringify(notes))
},[notes])
const addNote = (inputText, text) => {
const date = new Date();
const newNote = {
id: nanoid(),
title: inputText,
text: text,
date: date.toLocaleString()
}
const newNotes = [newNote, ...notes];
setNotes(newNotes)
}
const deleteNote = (id) => {
var response = window.confirm("Are you sure that you want to remove the note?");
if (response){
const notesUpdated = notes.filter((note) => note.id !== id)
setNotes(notesUpdated);
}
}
return (
<div className={darkMode ? 'dark-mode' : ''}>
<div className="container">
<Header
handleToggleTheme={setDarkMode}
/>
<Search
handleSearchNote={setSearchText}
setShowNote={setShowNote}
/>
<Pagination data={notes} handleAddNote={addNote}
handleDeleteNote={deleteNote} searchText={searchText} />
</div>
</div>
)
}
export default App;
I've got a bug with LocalStorage on react.js. I try to set a todo into it, but it doesn't load. This is the code:
import React, { useState, useRef, useEffect } from 'react';
import './App.css';
import TodoList from './TodoList';
const { v4: uuidv4 } = require('uuid');
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useState([]);
const TodoNameRef = useRef()
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if (storedTodos) setTodos(storedTodos)
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
function HandleAddTodo(e){
const name = TodoNameRef.current.value
if (name==='') return
setTodos(prevTodos => {
return[...prevTodos, { id:uuidv4(), name:name, complete:false}]
})
TodoNameRef.current.value = null
}
return (
<>
<TodoList todos={todos}/>
<input ref={TodoNameRef} type="text" />
<button onClick={HandleAddTodo}>Add todo</button>
<button>clear todo</button>
<p>0 left todo</p>
</>
)
}
export default App;
This is TodoList.js
import React from 'react'
import Todo from './Todo';
export default function TodoList({ todos }) {
return (
todos.map(todo =>{
return <Todo key ={todo.id} todo={todo} />
})
)
}
And as last Todo.js:
import React from 'react'
export default function Todo({ todo }) {
return (
<div>
<label>
<input type="checkbox" checked={todo.complete}/>
{todo.name}
</label>
</div>
)
}
What the code has to do is load a todo into the local storage, and after refreshing the page reload it into the document. The code I implemented
I just started with react but I hope anyone can pass me the right code to make it work. If anyone need extra explenation, say it to me.
Kind regards, anonymous
Try to decouple your local storage logic into it's own react hook. That way you can handle getting and setting the state and updating the local storage along the way, and more importantly, reuse it over multiple components.
The example below is way to implement this with a custom hook.
const useLocalStorage = (storageKey, defaultValue = null) => {
const [storage, setStorage] = useState(() => {
const storedData = localStorage.getItem(storageKey);
if (storedData === null) {
return defaultValue;
}
try {
const parsedStoredData = JSON.parse(storedData);
return parsedStoredData;
} catch(error) {
console.error(error);
return defaultValue;
}
});
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(storage));
}, [storage]);
return [storage, setStorage];
};
export default useLocalStorage;
And you'll use it just like how you would use a useState hook. (Under the surface it is not really more than a state with some side effects.)
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useLocalStorage(LOCAL_STORAGE_KEY, []);
const handleAddTodo = event => {
setTodos(prevTodos => {
return[...prevTodos, {
id: uuidv4(),
name,
complete: false
}]
})
};
return (
<button onClick={HandleAddTodo}>Add todo</button>
);
}
You added the getItem and setItem methods of localStorage in two useEffect hooks.
The following code intializes the todo value in localStorage when reloading the page.
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
So you need to set the todo value in HandleAddTodo event.
I edited your code and look forward it will help you.
import React, { useState, useRef, useEffect } from 'react';
import './App.css';
import TodoList from './TodoList';
const { v4: uuidv4 } = require('uuid');
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useState([]);
const TodoNameRef = useRef()
useEffect(() => {
const storageItem = localStorage.getItem(LOCAL_STORAGE_KEY);
const storedTodos = storageItem ? JSON.parse(storageItem) : [];
if (storedTodos) setTodos(storedTodos)
}, []);
function HandleAddTodo(e){
const name = TodoNameRef.current.value;
if (name==='') return;
const nextTodos = [...todos, { id:uuidv4(), name:name, complete:false}];
setTodos(nextTodos);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(nextTodos));//replace todos to nextTodos
TodoNameRef.current.value = null
}
return (
<>
<TodoList todos={todos}/>
<input ref={TodoNameRef} type="text" />
<button onClick={HandleAddTodo}>Add todo</button>
<button>clear todo</button>
<p>0 left todo</p>
</>
)
}
export default App;
There is no need of adding the second useEffect.
You can set your local Storage while submitting in the handleTodo function.
Things you need to add or remove :
Remove the Second useEffect.
Modify your handleTodo function :
const nextTodos = [...todos, { id:uuidv4(), name:name,complete:false}];
setTodos(nextTodos);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(nextTodos));
Note: Make sure you won't pass todos instead of nextTodos as we know setTodos is an async function There might be a chance we are setting a previous copy of todos
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);
};
I need to get a list of repositories using GitHub API, search has to work on button click and on change selectBox with licences
import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import moment from "moment";
import { Layout } from "./../Layout";
import { List } from "./../List";
import { Loader } from "./../Loader";
import { Header } from "./../Header";
import { Search } from "./../Search";
import { Licenses } from "./../Licenses";
import "./App.css";
export const App = () => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [nameSearch, setNameSearch] = useState("");
const [license, setLicense] = useState({});
const fetchData = useCallback(async () => {
setHasError(false);
setIsLoading(true);
try {
const prevMonth = moment()
.subtract(30, "days")
.format("YYYY-MM-DD");
const licenseKey = (license && license.key) || "";
const response = await axios(
`https://api.github.com/search/repositories?q=${nameSearch}+in:name+language:javascript+created:${prevMonth}${
licenseKey ? `+license:${licenseKey}` : ""
}&sort=stars&order=desc`
);
setData(response.data.items);
} catch (error) {
setHasError(true);
setData([]);
}
setIsLoading(false);
}, [license]);
useEffect(() => {
fetchData();
}, [fetchData]);
return (
<Layout>
<Header>
<Search
handleSearchChange={setNameSearch}
nameSearch={nameSearch}
isLoading={isLoading}
onSearch={fetchData}
/>
<Licenses license={license} handleLicenseChange={setLicense} />
</Header>
<main>
{hasError && <div>Error</div>}
{isLoading ? <Loader /> : <List data={data} />}
</main>
</Layout>
);
};
First of all, I get warning
Compiled with warnings.
./src/components/App/App.js
Line 42:6: React Hook useCallback has a missing dependency: 'nameSearch'. Either include it or remove the dependency array react-hooks/exhaustive-deps
And my search is not working because nameSearch is always empty in the query string.
How to make search work?
Try adding nameSearch to the list of dependencies for useCallback:
const fetchData = useCallback(async () => {
...
}, [license, nameSearch]);
and make sure setNameSearch is actually used inside Search.js so that it will have a value.