React Router Dom - v6 - useBlocker - reactjs

As https://github.com/remix-run/react-router/issues/8139 is finished and we got useBlocker in v6, did anyone got it to work?
This is what I got so far and pretty much I'm stuck with error I quite don't understand
in App.js I have my BrowserRouter and everything is wrapped inside
Also I used example from implementer's gists (I copy pasted)
import * as React from "react";
import { useBeforeUnload, unstable_useBlocker as useBlocker } from "react-router-dom";
function usePrompt(message, { beforeUnload } = {}) {
let blocker = useBlocker(
React.useCallback(
() => (typeof message === "string" ? !window.confirm(message) : false),
[message]
)
);
let prevState = React.useRef(blocker.state);
React.useEffect(() => {
if (blocker.state === "blocked") {
blocker.reset();
}
prevState.current = blocker.state;
}, [blocker]);
useBeforeUnload(
React.useCallback(
(event) => {
if (beforeUnload && typeof message === "string") {
event.preventDefault();
event.returnValue = message;
}
},
[message, beforeUnload]
),
{ capture: true }
);
}
function Prompt({ when, message, ...props }) {
usePrompt(when ? message : false, props);
return null;
}
And then within my component I called Prompt like this
const MyComponent = (props) => {
const [showPrompt, setShowPrompt] = useState(false)
...
return (
...
<Prompt when={showPrompt}
message="Unsaved changes detected, continue?"
beforeUnload={true}
/>
)
}
And on page load of MyComponent I keep getting error
Error: useBlocker must be used within a data router. See
https://reactrouter.com/routers/picking-a-router.
at invariant (history.ts:308:1)
at useDataRouterContext (hooks.tsx:523:1)
at useBlocker (hooks.tsx:723:1)
at usePrompt (routerCustomPrompt.js:8:1)
at Prompt (routerCustomPrompt.js:37:1)
Did anyone got useBlocker in new version to work?

The error message is rather clear. In order to use the useBlocker hook it must be used within a component rendered by a Data router. See Picking a Router.
Example:
const MyComponent = (props) => {
const [showPrompt, setShowPrompt] = useState(false);
...
return (
...
<Prompt
when={showPrompt}
message="Unsaved changes detected, continue?"
beforeUnload={true}
/>
);
}
import {
createBrowserRouter,
createRoutesFromElements,
Route,
RouterProvider,
} from "react-router-dom";
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Root />}>
{/* ... etc. */}
<Route path="myComponent" element={<MyComponent />} />
{/* ... etc. */}
</Route>
)
);
const App = () => <RouterProvider router={router} />;

Related

React Hooks - Invalid Hook Call

I am getting an error
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
This is my hook useParking:
import { Strings } from "..";
import { get } from "../HTTPProvider";
import { Lot } from ".";
import { Moment } from "moment";
import L from "leaflet";
interface ApiResponse {
id: string;
fields: Lot;
createdTime: Moment | string;
}
const { apiUrl, apiKey } = Strings;
const [error, setError] = useState(false);
const getParkingLots = (setParkingLots: (l: Lot[]) => void) => {
get<{ records: ApiResponse[] }>(`${apiUrl}Parking%20Lots`, { apiKey })
.then((response) => {
console.log(response);
const data: Lot[] = [];
response.data.records.forEach((record) => {
const lat = record.fields.latitude;
const lon = record.fields.longitude;
if (lat && lon) record.fields.coordinates = L.latLng([lat, lon]);
data.push(record.fields);
});
setParkingLots(data);
})
.catch((error) => setError(error));
console.log(error);
};
export const useParkingLot = (): Lot[] => {
const [parkingLots, setParkingLots] = useState<Lot[]>([]);
useEffect(() => {
getParkingLots(setParkingLots);
}, [parkingLots]);
return parkingLots;
};
I am trying to use the hook in my MainTabs component here:
import {
IonTabs,
IonTabBar,
IonTabButton,
IonIcon,
IonLabel,
IonRouterOutlet,
} from "#ionic/react";
import { map, business, calendarOutline, carOutline } from "ionicons/icons";
import { Route, Redirect } from "react-router";
import { CampusMap, Events, Buildings, ParkingLots } from "../../pages";
import { useFakeEvent } from "../../DataProviders";
import { useBuilding } from "../../DataProviders";
import { useParkingLot } from "../../DataProviders";
export const MainTabs: React.FC = () => {
const buildings = useBuilding();
const parkingLots = useParkingLot();
const [events, setEvents] = useState(useFakeEvent());
const [showName, setShowName] = useState(true);
const toggleName = () => {
console.log("resetName called");
setShowName(false);
return setTimeout(() => {
setShowName(true);
});
};
return (
<IonTabs>
<IonRouterOutlet>
<Route path="/:tab(Events)" render={() => <Events />} exact={true} />
<Route
path="/:tab(Map)"
render={() => (
<CampusMap
buildings={buildings}
showName={showName}
parkingLots={parkingLots}
events={events}
/>
)}
exact={true}
/>
<Route
path="/:tab(BuildingList)"
render={() => <Buildings buildings={buildings} />}
exact={true}
/>
<Route
path="/:tab(ParkingLotList)"
render={() => <ParkingLots parkingLots={parkingLots} />}
exact={true}
/>
<Route exact path="/" render={() => <Redirect to="/Map" />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="Map" href="/Map" onClick={toggleName}>
<IonIcon icon={map} />
<IonLabel>Map</IonLabel>
</IonTabButton>
<IonTabButton tab="BuildingList" href="/BuildingList">
<IonIcon icon={business} />
<IonLabel>Buildings</IonLabel>
</IonTabButton>
<IonTabButton tab="Events" href="/Events">
<IonIcon icon={calendarOutline} />
<IonLabel>Events</IonLabel>
</IonTabButton>
<IonTabButton tab="ParkingList" href="/ParkingLotList">
<IonIcon icon={carOutline} />
<IonLabel>Parking Lots</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
I have checked my code against the Rules of Hook documentation and it doesn't seem like I am breaking any. I have also checked my dependencies and they all check out. So I'm not sure why I'm getting the error. Can anyone see what I am doing wrong here?
Use useState inside getParkingLots function because you defined useState outside of functional component which causes Invalid Hook Call. Try initializing inside like
const getParkingLots = (setParkingLots: (l: Lot[]) => void) => {
const [error, setError] = useState(false); // here
get<{ records: ApiResponse[] }>(`${apiUrl}Parking%20Lots`, { apiKey })
.then((response) => {
....
})
.catch((error) => setError(error));
console.log(error);
};
The const [error, setError] = useState(false); needs to be within a functional component body or custom hook. – Drew Reese
This fixed the error.

In React, is there an elegant way of using the id in a RESTful edit url and loading the corresponding object into the initial state of my component?

I'm building a React 16.13 application. I have a search component, src/components/Search.jsx, that constructs search results and then builds a URL to edit those results ...
renderSearchResults = () => {
const { searchResults } = this.state;
if (searchResults && searchResults.length) {
return (
<div>
<div>Results</div>
<ListGroup variant="flush">
{searchResults.map((item) => (
<ListGroupItem key={item.id} value={item.name}>
{item.name}
<span className="float-right">
<Link to={"/edit/"+item.id}>
<PencilSquare color="royalblue" size={26} />
</Link>
</span>
</ListGroupItem>
))}
</ListGroup>
</div>
);
}
};
render() {
return (
<div className="searchForm">
<input
type="text"
placeholder="Search"
value={this.state.searchTerm}
onChange={this.handleChange}
/>
{this.renderSearchResults()}
</div>
);
}
Is there a more elegant way to load/pass the object I want to edit? Below I'm deconstructing the URL and launching an AJAX call but what I'm doing seems kind of sloppy. I'm familiar with Angular resolvers and that seems a cleaner way of decoupling the logic of parsing the URL and finding the appropriate objects but the below is all I could come up with ...
src/components/Edit.jsx
import React, { Component } from "react";
import FormContainer from "../containers/FormContainer";
export default class Edit extends Component {
render() {
return <FormContainer />;
}
}
src/containers/FormContainer.jsx
class FormContainer extends Component {
...
componentDidMount() {
let initialCountries = [];
let initialProvinces = [];
let coopTypes = [];
// Load form object, if present in URL
const url = window.location.href;
const id = url.split("/").pop();
fetch(FormContainer.REACT_APP_PROXY + "/coops/" + id)
.then((response) => {
return response.json();
})
.then((data) => {
const coop = data;
coop.addresses.map(address => {
address.country = FormContainer.DEFAULT_COUNTRY_CODE; // address.locality.state.country.id;
});
this.setState({
newCoop: coop,
});
});
You aren't posting all the relevant code but I know what you are trying to accomplish (correct me if I'm wrong). You want to use the id from the url parameters to fetch data. I think you are using react-router. You can use this example to refactor your code:
import React, { useState, useEffect } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
useParams
} from "react-router-dom";
const REACT_APP_PROXY = "api";
const DEFAULT_COUNTRY_CODE = "20";
// You can use functional components and react hooks in React 16.13 to do everything
// No need for class components any more
function FormContainer() {
// useState hook to handle state in functional components
const [newCoop, setNewCoop] = useState({});
// useParams returns an object of key/value pairs of URL parameters. Use it to access match.params of the current <Route>.
const { id } = useParams();
// This will be called whenever one of the values in the dependencies array (second argument) changes
// but you can pass an empty array to make it run once
useEffect(() => {
fetch(REACT_APP_PROXY + "/coops/" + id)
.then(response => {
return response.json();
})
.then(data => {
const coop = data;
coop.addresses.map(address => {
address.country = DEFAULT_COUNTRY_CODE; // address.locality.state.country.id;
});
setNewCoop(coop);
});
// use an empty array as the second argument to run this effect on the first render only
// it will give a similar effect to componentDidMount
}, []);
return <div>Editing {id}</div>;
}
const Edit = () => <FormContainer />;
function App() {
return (
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/edit/:id">
<Edit />
</Route>
</Switch>
</Router>
);
}

Uncaught (in promise) TypeError: Cannot read property 'fields' of undefined (React + Contentful)

I'm trying to create a "portfolio" website to learn react. I've plugged content from Contentul, but i'm getting an error : Uncaught (in promise) TypeError: Cannot read property 'fields' of undefined when trying to display my content.
Here's what i've done so far to get content from Contentful into my React app :
I've creacted a contentful.js file
## contentful.js
const client = require('contentful').createClient({
space: 'MYSPACEID',
accessToken: 'MYACCESSTOKEN',
});
const getProjectItems = () => client.getEntries().then((response) => response.items);
const getSingleProject = (slug) =>
client
.getEntries({
'fields.slug': slug,
content_type: 'project',
})
.then((response) => response.items);
export { getProjectItems, getSingleProject };
Then, i've created 2 custom Hooks for getting my content :
## UseProjects.js
import { useEffect, useState } from 'react';
import { getProjectItems } from '../contentful';
const promise = getProjectItems();
export default function useProjects() {
const [projects, setProjects] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
promise.then((project) => {
setProjects(project);
setLoading(false);
});
}, []);
return [projects, isLoading];
}
## useSingleProject.js
import { useEffect, useState } from 'react';
import { getSingleProject } from '../contentful';
export default function useSingleProject(slug) {
const promise = getSingleProject(slug);
const [project, setProject] = useState(null);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
promise.then((result) => {
setProject(result[0].fields);
setLoading(false);
});
}, [promise]);
return [project, isLoading];
}
I can add my components code if needed but i feel like my error comes from here.. What's weird is that if i close the error, i see all the items properly rendered (so..they're properly pulled from Contentful) and if i click on it i've got the correct informations displayed (title, image, etc.). But the error makes weird layout things.
The error comes from my useSingleProject.js file (useSingleProject.js:13)
Now here i feel it can also come from my App.js file, i'm not sure about how i configured the routing for single project pages (i'm still new to react..). If i disable the following line from the routes array : { path: '/:id', name: ':id', Component: SingleProject }, then the error disapears. I can see all the projects on my projects page, but if i click on one of them the slug changes but nothing shows on the single project pages, since i've disabled it.
## App.js
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import { gsap } from 'gsap';
import './styles/App.scss';
import Header from './components/header';
import Navigation from './components/navigation';
import CaseStudies from './pages/caseStudies';
import Approach from './pages/approach';
import Services from './pages/services';
import About from './pages/about';
import Home from './pages/home';
import Projects from './pages/projects';
import SingleProject from './pages/SingleProject';
const routes = [
{ path: '/', name: 'Home', Component: Home },
{ path: '/case-studies', name: 'caseStudies', Component: CaseStudies },
{ path: '/approach', name: 'approach', Component: Approach },
{ path: '/services', name: 'services', Component: Services },
{ path: '/about-us', name: 'about', Component: About },
{ path: '/projects', name: 'projects', Component: Projects },
{ path: '/:id', name: ':id', Component: SingleProject },
];
function debounce(fn, ms) {
let timer;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, arguments);
}, ms);
};
}
function App() {
const [dimensions, setDimensions] = React.useState({
height: window.innerHeight,
width: window.innerWidth,
});
useEffect(() => {
// prevents flashing
gsap.to('body', 0, { css: { visibility: 'visible' } });
const debouncedHandleResize = debounce(function handleResize() {
setDimensions({
height: window.innerHeight,
width: window.innerWidth,
});
}, 1000);
window.addEventListener('resize', debouncedHandleResize);
return () => {
window.removeEventListener('resize', debouncedHandleResize);
};
});
return (
<>
<Header dimensions={dimensions} />
<div className="App">
{routes.map(({ path, Component }) => (
<Route key={path} exact path={path}>
<Component dimensions={dimensions} />
</Route>
))}
</div>
<Navigation />
</>
);
}
export default App;
EDIT :
So i've tried to console.log(response.items) in my getSingleProject function. It returns the correct array of object (so here containing only one object).
I've also tried tu console.log(result) in my useProjects function (inside the useEffect). It still logs the correct object, and it has the fields property i need to get. When console logging in my useEffect, it logs the object every second or so by the way. Is this a normal behavior?
Your hook useSingleProject is coded in such a way that it triggers an infinite re-render loop. Here is a reproduction of the behaviour I describe: https://codesandbox.io/s/confident-banach-tr609?file=/src/index.js. Observe how the console logs forever.
This happens because the hook useSingleProject is going to be called on each re-render, creating a new promise each time. In turn useEffect will trigger when promise is changed, causing an eventual state update, which also means an eventual re-render. This causes an infinite loop.
The solution would be to make your effect to depend on slug instead of promise. In fact, you don't even need to assign the promise to a variable first.
Also, just in case, it makes sense to only set isLoading to true only if you actually had to fetch data. So:
export default function useSingleProject(slug) {
const [project, setProject] = useState(null);
const [isLoading, setLoading] = useState(false);
useEffect(() => {
setLoading(true)
getSingleProject(slug).then((result) => {
setProject(result[0].fields);
setLoading(false);
});
}, [slug]);
return [project, isLoading];
}
So after another day trying to understand where the error comes from, it looks like it's coming from the way i've setup my Router in App.js. By modifying it, the error disappeared and now all content is pulled and displayed correctly. For those interested, here is my new App.js file and how i've setup routes for now to correct the issue :
import React, { useEffect } from 'react';
import { Switch, Route } from 'react-router-dom';
import { gsap } from 'gsap';
import './styles/App.scss';
import Header from './components/header';
import Navigation from './components/navigation';
import Approach from './pages/approach';
import Services from './pages/services';
import About from './pages/about';
import Home from './pages/home';
import Projects from './pages/projects';
import SingleProject from './pages/SingleProject';
function debounce(fn, ms) {
let timer;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, arguments);
}, ms);
};
}
function App() {
const [dimensions, setDimensions] = React.useState({
height: window.innerHeight,
width: window.innerWidth,
});
useEffect(() => {
// prevents flashing
gsap.to('body', 0, { css: { visibility: 'visible' } });
const debouncedHandleResize = debounce(function handleResize() {
setDimensions({
height: window.innerHeight,
width: window.innerWidth,
});
}, 1000);
window.addEventListener('resize', debouncedHandleResize);
return () => {
window.removeEventListener('resize', debouncedHandleResize);
};
});
return (
<>
<Header dimensions={dimensions} />
<div className="App">
<Switch>
<Route path="/" exact render={(props) => <Home {...props} dimensions={dimensions} />} />
<Route
path="/projects"
exact
render={(props) => <Projects {...props} dimensions={dimensions} />}
/>
<Route
path="/approach"
exact
render={(props) => <Approach {...props} dimensions={dimensions} />}
/>
<Route
path="/services"
exact
render={(props) => <Services {...props} dimensions={dimensions} />}
/>
<Route
path="/about-us"
exact
render={(props) => <About {...props} dimensions={dimensions} />}
/>
<Route
path="/:id"
render={(props) => <SingleProject {...props} dimensions={dimensions} />}
/>
</Switch>
</div>
<Navigation />
</>
);
}
export default App;
I'll refactor to make it less repetitive but here's how i've fixed it for now.

React Router: Test a component that programatically pushes to react-router history on form submit

New to react-router and having issues with Jest/Enzyme tests failing due to react-routers history prop.
The idea:
On submit of the form component call handleSubmit.
handleSubmit calls validate to do some simple validation on the form.
If validated history.push('/new-url-to-navigate-to')
Else if not validated pushes error message to state.
The idea works when running the project - a valid form submit navigates to the /sent component, as expected.
So far so good.
The Issue
My issue comes from my jest/enzyme unit test to test whether the component renders an error message when the validation fails. The test code:
import React from 'react'
import { mount } from 'enzyme'
import { MemoryRouter } from 'react-router-dom'
import Form from '../Form'
describe('<Form />', () => {
it('validates invalid form', () => {
const component = mount(
<MemoryRouter
initialEntries={["/", "/form", "/saved"]}
initialIndex={1}
>
<Form />
</MemoryRouter>
)
const input = component.find('input')
input.value = ""
const form = component.find('form')
form.simulate('submit', { preventDefault: () => {},
target: {
routingTextbox: input
}});
expect(component.find('h3').length).toBe(1)
})
})
Which fails with:
<Form /> › validates invalid form
TypeError: Cannot read property 'push' of undefined
48 |
49 | if (isValid) {
> 50 | history.push('/sent')
The error prevents the test from running and fails.
If I remove/comment the history.push('/sent') line, the test passes, but I obviously need this line to navigate to the next page (as I say this works when running the project - it is just the test that is failing.
I seem to not have access to the history prop in my component.
Any help on how to move forward with this would be greatly appreciated.
Form component for reference
I don't think the issue is in here, since it's working perfectly when running the project, but would be happy to be proved wrong if I can get this test working!
// Form component
import React, { useState } from 'react';
const Form = ({ history }) => {
const [state, setState] = useState({
error: {
isValid: true,
message: ''
}
})
const handleClick = () => {
history.goBack()
}
const renderError = () => {
if (state.error.isValid === false) {
return(
<div>
<h3>{state.error.message}</h3>
</div>
)
}
return null
}
const validate = (value) => {
if (value === "") {
setState({
error: {
isValid: false,
message: 'Please enter a value in the textbox'
}
})
return false
}
return true
}
const handleSubmit = (e) => {
e.preventDefault()
const isValid = validate(e.target.routingTextbox.value)
if (isValid) {
history.push('/sent')
} else {
console.log('textbox is empty')
}
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="routingTextbox" />
<button type="submit">Submit</button>
<button type="button" onClick={handleClick}>Go Back</button>
{renderError()}
</form>
)
}
export default Form
Router for reference
const App = () => (
<Router>
<Switch>
<Route path="/" exact component={Index} />
<Route path="/form" component={Form} />
<Route path="/sent" component={Sent} />
</Switch>
</Router>
)
export default App;

HOC as an App Wrapper for React Redux

I wanted to have an app HOC that wraps each component view.
This HOC authenticates user and sets Google Analytics tracking.
I'm upgrading to Router 4, and having an issue with making it work.
It's giving me the following error -
TypeError: (0 , _AppWrapper2.default) is not a function
Which is likely related to how I am creating the HOC.
Any ideas?
routes.js
export default (
<Switch>
<Route exact path="/" component={AppWrapper(Home)} />
<Route exact path="/channels" component={AppWrapper(Channels)} />
</Switch>
);
const AppWrapper = (WrappedComponent) => {
return class AppWrapperComponent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const page = this.props.location.pathname;
this.trackPage(page);
}
componentWillReceiveProps(nextProps) {
const currentPage = this.props.location.pathname;
const nextPage = nextProps.location.pathname;
if (currentPage !== nextPage) {
this.trackPage(nextPage);
}
}
trackPage = page => {
GoogleAnalytics.set({
page,
...options,
});
GoogleAnalytics.pageview(page);
};
render() {
return (
<div>
{this.state.isMounted && !window.devToolsExtension && process.env.NODE_ENV === 'development' && <DevTools />}
<WrappedComponent {...this.props.chidren} />
</div>
);
}
}
Looks like you're not exporting AppWrapper. If you import it with import AppWrapper from .., add this line at the end of AppWrapper.js:
export default AppWrapper;
or replace the const declaration with
export default (WrappedComponent) => { ..
If you import it with import {AppWrapper} from .., you can insert an export before the const:
export const AppWrapper = (WrappedComponent) => {

Resources