How can i access the history object inside of this promise call? - javascript

In the React component, A button submission takes the values of text fields on the page and passes them into mockFetch. Then, If the promise of mockFetch is successful, It causes a history.push('/newaccount') to fire.
I set up my test to click on the button and then attempt to detect the history.push('/newaccount') call with what i passed into it, but my mock history.push doesn't get called. Does anyone know what i can do to get this test to pass?
EDIT: It turns out replacing the current jest.mock with:
const history = createMemoryHistory();
const pushSpy = await jest.spyOn(history, "push");
allows me to call the mocked history when the history.push is OUTSIDE of the mockFetch function... but not when it is inside of it. That's what i'm trying to figure out now.
Main app routing:
function App() {
const classes = useStyles();
return (
<div className="App">
<Banner />
<div id="mainSection" className={classes.root}>
<ErrorBoundary>
<Paper>
<Router history={history}>
<Switch>
<Route path="/newaccount">
<NewAccountPage />
</Route>
<Route path="/disqualify">
<DisqualificationPage />
</Route>
<Route path="/">
<LandingPage />
</Route>
</Switch>
</Router>
</Paper>
</ErrorBoundary>
</div>
</div>
);
}
export default App;
Component: (omitted some redundant fields)
import React, { useCallback, useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useHistory } from "react-router-dom";
import Grid from "#material-ui/core/Grid";
import Button from "#material-ui/core/Button";
import TextFieldStyled from "../TextFieldStyled/TextFieldStyled.js";
import * as actionTypes from "../../redux/actions/rootActions.js";
import {
checkAllErrors,
} from "../../validators/validators.js";
import mockFetch from "../../fetchCall/fetchCall";
import "./LandingPage.css";
const LandingPage = () => {
const dispatch = useDispatch();
const history = useHistory();
const {
changeCarPrice,
changeErrorMessage,
resetState,
} = actionTypes;
useEffect(() => {
return () => {
dispatch({ type: resetState });
};
}, [dispatch, resetState]);
const { carPrice } = useSelector(
(state) => state
);
const handleSubmit = (e) => {
e.preventDefault();
if (!checkAllErrors(allErrors)) {
// Call the API
mockFetch(carPrice)
.then((response) => {
history.push("/newaccount");
})
.catch((error) => {
dispatch({ type: changeErrorMessage, payload: error });
history.push("/disqualify");
});
}
};
const [allErrors, setAllErrors] = useState({
carValueError: false,
});
return (
<div id="landingPage">
<Grid container>
<Grid item xs={2} />
<Grid item xs={8}>
<form onSubmit={handleSubmit}>
<Grid container id="internalLandingPageForm" spacing={4}>
{/* Text Fields */}
<Grid item xs={6}>
<TextFieldStyled
info={"Enter Car Price ($):"}
value={carPrice}
adornment={"$"}
type="number"
label="required"
required
error={allErrors.carValueError}
id="carPriceField"
helperText={
allErrors.carValueError &&
"Please enter a value below 1,000,000 dollars"
}
passbackFunction={(e) => handleChange(e, changeCarPrice)}
/>
</Grid>
</Grid>
<Grid container id="internalLandingPageFormButton" spacing={4}>
<Grid item xs={4} />
<Grid item xs={3}>
<Button
variant="contained"
color="primary"
type="submit"
id="applyNowButton"
title="applyNowButton"
onSubmit={handleSubmit}
>
Apply Now
</Button>
</Grid>
<Grid item xs={5} />
</Grid>
</form>
</Grid>
<Grid item xs={2} />
</Grid>
</div>
);
};
export default LandingPage;
Test:
const wrapWithRedux = (component) => {
return <Provider store={store}>{component}</Provider>;
};
it("simulates a successful submission form process", () => {
const mockHistoryPush = jest.fn();
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
render(
wrapWithRedux(
<Router history={history}>
<Switch>
<Route path="/newaccount">
<NewAccountPage />
</Route>
<Route path="/">
<LandingPage />
</Route>
</Switch>
</Router>
)
);
const carPriceField= screen.getByTestId("carPriceField");
fireEvent.change(carPriceField, { target: { value: "5000" } });
const buttonSubmission= screen.getByTitle("buttonSubmission");
fireEvent.click(buttonSubmission);
expect(mockHistoryPush).toHaveBeenCalledWith('/newaccount');
});

so after looking further into mocking history, i found this from How to mock useHistory hook in jest?
The answer seems to be to do the code below and then the pushSpy will be called
const history = createMemoryHistory();
const pushSpy = jest.spyOn(history, "push");
render(
wrapWithRedux(
<Router history={history}>
<Switch>
<Route path="/newaccount">
<NewAccountPage />
</Route>
<Route path="/">
<LandingPage />
</Route>
</Switch>
</Router>
)
);
I also had to wrap the expect line using waitFor from the react testing library to get the history.push to call the mock inside of the mockFetch function like so:
await waitFor(() => expect(pushSpy).toHaveBeenCalledWith("/newaccount"));
With these two modifications, the test now passes.

Rather than mock a bunch of things and test implementation, I would test the behavior.
Here are some ideas to maybe help writing for behavior over implementation details.
Refer to React Router testing docs
React Router has a guide on testing navigation based on actions.
Hijack render() to wrap with Redux
Rather than writing a Redux wrapper for every test suite, you could hijack the react-testing-library's render() function to get a clean state or seed the state. Redux has docs here.
Don't mock fetch()
A button submission takes the values of text fields on the page and passes them into mockFetch
I would use an HTTP interceptor to stub a response. That way you get the async behavior and you bind your tests to the backend vs binding it to the tool. Say you don't like fetch(), you'll be stuck with it until you migrate everything. I made a blog post on the subject Testing components that make API calls.
Here's your code example with some edits:
it("creates a new account", () => { // <-- more descriptive behavior
// Stub the server response
nock(`${yoursite}`)
.post('/account/new') // <-- or whatever your backend is
.reply(200);
render(
<MemoryRouter initialEntries={["/"]}> // <-- Start where your forms are at
<Switch>
<Route path="/newaccount">
<NewAccountPage />
</Route>
<Route path="/">
<LandingPage />
</Route>
</Switch>
</MemoryRouter>
);
const carPriceField= screen.getByTestId("carPriceField");
fireEvent.change(carPriceField, { target: { value: "5000" } });
const buttonSubmission= screen.getByTitle("buttonSubmission");
fireEvent.click(buttonSubmission);
expect(document.body.textContent).toBe('New Account Page'); // <-- Tests route change
});

Related

Maintain context values between routes in React.js

I'm trying to maintain state values between routes in context. But it gets reset when the route changes.
aackage.json:
"react-router-dom": "^6.8.0",
"react-dom": "^18.2.0",
"react": "^18.2.0",
App.js:
export default const App = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const getData = async () => {
setLoading(true);
const data = await axios.get("url", {
withCredentials: true,
});
setData(data);
setLoading(false);
};
useEffect(() => {
getData()
console.log("I run on route change");
}, []);
const GlobalContextValue= {
data: data,
loading: loading,
};
return (
<>
<GlobalContextProvider value={GlobalContextValue}>
<BrowserRouter>
<Routes>
<Route index element={<HomePage />} />
<Route path="/:slug" element={<PostPage />} />
{/* <Route path="*" element={<NoPage />} /> */}
</Routes>
</BrowserRouter>
</<GlobalContextProvider />
</>
)
}
Whenever I try to access any route the getData function inside the useEffect calls which inturns resets the data. I have attached a CodeSandbox to replicate the same
I don't know if this problem is related to reactJs or react-router. Thanks in advance
As you don't seem to have any navigation link, I assume you are using the browser search bar, or a normal HTML <a> tag. Well, doing so refreshes the page, so the entire app gets re-created.
Using useNavigate or Link from React Router Dom, doesn't refresh the page, hence your context data remains untouched:
const HomePage = () => {
return (
<>
<h1>Hii Homepage </h1>
<Link to="/1">Go to PostPage</Link>
</>
);
};
const PostPage = () => {
const params = useParams();
return (
<>
<h1>Hii PostPage {params.slug} </h1>
<Link to="/">Go to HomePage</Link>
</>
);
};
export default function App() {
useEffect(() => {
console.log(
"I run on load and route change with browser search bar, not with useNavigate or Link"
);
}, []);
return (
<>
{/* This context wrapping BrowserRouter keeps its value if you navigate with Link or
useNavigate. */}
<GlobalContextProvider value={{ key: "some value" }}>
<BrowserRouter>
<Routes>
<Route index element={<HomePage />} />
<Route path="/:slug" element={<PostPage />} />
{/* <Route path="*" element={<NoPage />} /> */}
</Routes>
</BrowserRouter>
</GlobalContextProvider>
</>
);
}

Can't pass data from one page to another with React-Router-DOM (useHistory, useLocation)

I have a router:
<Switch>
<Route exact path="/">
<CustomPaddingContainer padding="0 0.5em 1.5em">
<TableViewComponent columns={tableAccessors} />
</CustomPaddingContainer>
</Route>
<Route path="/new-objective">
<AddNewObjectiveComponent onSubmit={onSubmitObjective} onCancel={onCancel} />
</Route>
<Route path="/new-kr">
<AddNewKrComponent onSubmit={onSubmitKR} onCancel={onCancel} />
</Route>
<Route path="/okr-details/:id">
<OkrDetailsWithParams />
</Route>
</Switch>
and I want to pass specific data from specific component to one of this Route when specific button will be clicked. to be more precise, I have this component:
const AddOKRButtons: FC<AddOKRButtonsProps> = ({ parentObjectiveId }) => {
const history = useHistory();
const onAddOkrButtonClick = () => {
history.push('/new-objective', { parentObjectiveId: parentObjectiveId });
};
const onAddKrButtonClick = () => {
history.push('/new-kr', { parentObjectiveId: parentObjectiveId });
};
return (
<OkrDetailsChildrenCardsButtonContainerCentered>
<ButtonGroup>
<LinkButton to="/new-objective" appearance="default" onClick={onAddOkrButtonClick}>
Add a new sub-objective
</LinkButton>
<LinkButton to="/new-kr" appearance="default" onClick={onAddKrButtonClick}>
Add a new key-result
</LinkButton>
</ButtonGroup>
</OkrDetailsChildrenCardsButtonContainerCentered>
);
};
Im trying to pass the **parentObjectiveId** which is coming from props to the /new-objective page or /new-kr page in order what button was clicked. After that Im trying to get that data in component where it should be with useLocation hook:
export const AddNewObjectiveComponent: FC<NonNullable<AddNewOKRProps>> = props => {
const location = useLocation();
console.log(location);
return(<div></div>)
}
and unfortunately i got undefined in the state key, where the data is probably should be:
Try to push history route like
history.push({
pathname: '/new-objective',
state: { parentObjectiveId: parentObjectiveId }
});
I hope it will be work for you. Thanks!

ReactRouterDom, AuthRoute returns react render functions are not valid as react child warning

My Router is a simple component containing public and private routes. I have created an AuthRoute referring to the great tutorial from here
So, my Router looks like:
<Router>
<div>
<Navigation />
<Route exact path={ROUTES.LANDING} component={Landing} />
<Route path={ROUTES.SIGN_UP} component={SignUp} />
<Route path={ROUTES.SIGN_UP_SUCCESS} component={SignUpSuccess} />
<AuthenticationRoute path={ROUTES.HOME} component={Home} />
</div>
</Router>
and my AuthenticationRoute looks like this:
export const AuthenticationRoute = ({ component: Component, ...rest }) => {
const [authChecking, setAuthChecking] = useState(true);
const [{ isAuth }, dispatch] = useStateValue();
useEffect(() => {
checkLoggedIn().then(res => {
setAuthChecking(false);
dispatch({
op: 'auth',
type: 'toggleSessionAuth',
toggleSessionAuth: res
});
});
}, [])
if(authChecking)
return null;
if(!isAuth) {
return <Redirect to='/' />;
}
return <Route {...rest} render={(props) => (
<Component {...props} />
)
} />
}
Everything looks fine, however, my console returns such warning:
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from the render. Or maybe you meant to call this function rather than return it.
I have tried different solutions using component/render etc, however, I could not find a solution to this problem and I have no idea what I am doing wrong.
For testing purposes, instead of rendering Component, I tried to render simple <div>test</div> and it worked fine. However, when I am passing a JSX component in props, it returns the warning shown above.
Implementation oh Home Component (Home.js):
export const Home = () => {
const [{ user }, dispatch] = useStateValue();
const { history } = useReactRouter();
const moveTo = path => {
dispatch({
op: 'other',
type: 'setView',
setView: path
});
history.push(path);
}
return (
<div className="pageMenuWrapper">
<h1 className="homeTitle">Hi {() => user ? `, ${user.username}` : ``}.</h1>
<div className="wrapper">
<Tile image={leagueico} alt="text" onClick={() => moveTo(ROUTES.TEST)}/>
<Tile comingSoon />
</div>
</div>
);
}
export default Home;
Could anyone help me solve this little problem?

Rendering two different components with Reactjs

I've 2 different components, and I would like to render them separately. Logic is simple when user is logged in render 1st component, if it's not render default component.
Please review my code, and help me with this, I'm not good at react, so please help me
export default function App(props) {
const Authenticated = props.Authenticated
const RoutingCabinet = (
<React.Fragment>
<Header/>
<Switch>
<Route exact path = '/' component = { () => TempBody} />
<Route exact path = '/new' component = { () => <ResponsiveDrawer />}/>
</Switch>
<Footer/>
</React.Fragment>
);
const RoutingContent = (
<React.Fragment>
<Cabinet />
<Switch>
<Route exact path = '/user.test'component = { () => <div>Element</div> }/>
</Switch>
</React.Fragment>
);
return(
<ThemeProvider theme = {Theme}>
<BrowserRouter>
{
Authenticated
? RoutingCabinet
: RoutingContent
}
</BrowserRouter>
</ThemeProvider>
);
}
Currently I can see only RoutingCabinet
Here is the index.js
ReactDOM.render(<App Authenticated = {false} />, document.getElementById('__body__', '__root__'));
I may be wrong here, but is <Cabinet /> the same as <RoutingCabinet />? If so, it might work ok. The <div>Element</div> will be visible only if url matches user.test. If you are on any other page, this won't be rendered, so it'll look like <RoutingCabinet /> is rendered, while in fact it's <RoutingContent />, it just renders the same output.

ReactJS: Redirecting to a new page within an axios request

I am building a MERN app with a login/register system and I am stuck on being able to redirect a user to a confirmation page which then prompts them to login.
It seems like I could use the useHistory hook in react-router-dom and do history.push() within my axios request which is within my register function:
function handleRegister(e) {
let history = useHistory();
e.preventDefault();
// Grab state
const user = {
username: formState.username,
email: formState.email,
password: formState.password,
password2: formState.password2,
};
// Post request to backend
axios
.post("http://localhost:4000/register", user)
.then((res) => {
// Redirect user to the /thankyouForRegistering page which prompts them to login.
history.push("/thankyouForRegistering");
})
.catch((err) => {
console.log(err);
});
}
But this does not work. I get an error back saying:
React Hook "useHistory" is called in function "handleRegister" which is neither a React function component or a custom React Hook function
Upon further research, it seems that in order to use the useHistory hook, it has to be within <Router>(possibly?) or directly on an onClick handler.
So something like this:
<Button onClick={() => history.push()}></button>
I can't really do that though, because I am not using onClick for my register button, I am using onSubmit and my own register function.
I also looked into using <Redirect />, so I tried making a new state called authorized, set the authorize state to true in my axios request, and then tried this:
<Route
path="/thankyouForRegistering"
render={() => (
authorized ? (
<ThankyouForRegistering />
) : (
<Redirect to="/register" />
))
}
/>
But this is not working either, and it also does not give me any kind of error.
Does anyone know the best way to redirect a user to a new page upon registering/logging in? I've been struggling with this for going on two weeks.
Thanks!!
EDIT: Here is the entire component - it's a bit messy but if anyone needs any explanations please let me know.
import React, { useState } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
useHistory,
} from "react-router-dom";
let navMenu;
function App() {
let history = useHistory();
const [navMenuOpen, setNavMenuOpen] = useState(false);
const [loggedIn, setLoggedIn] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [token, setToken] = useState("");
const [authorized, setAuthorized] = useState(false);
const initialState = {
username: "",
email: "",
password: "",
password2: "",
};
const [formState, setFormState] = useState(initialState);
const { username, email, password, password2 } = formState;
const handleChange = (e) => {
setFormState({ ...formState, [e.target.name]: e.target.value });
};
function handleRegister(e) {
//const history = useHistory();
e.preventDefault();
// Grab setState
const user = {
username: formState.username,
email: formState.email,
password: formState.password,
password2: formState.password2,
};
// Post request to backend
axios
.post("http://localhost:4000/register", user)
.then((res) => {
console.log(user);
history.push("/thankyouForRegistering");
setAuthorized(true);
// Redirect user to the /thankyouForRegistering page which prompts them to login.
})
.catch((err) => {
console.log(err);
});
// Once a user has registered, clear the registration form and redirect the user to a page that says thank you for registering, please login.
}
const handleLogin = (e) => {
e.preventDefault();
// Grab setState
const userData = {
email: formState.email,
password: formState.password,
};
axios
.post("http://localhost:4000/login", userData)
.then((res) => {
// Get token from local storage if there is a token
localStorage.setItem("token", res.data.token);
// If there is a token, redirect user to their profile and give them access to
// their recipeList and shoppingList
setLoggedIn(true);
//props.history.push("/profile");
})
.catch((err) => console.log(err));
};
const navMenuToggle = () => {
console.log("toggle");
setNavMenuOpen(!navMenuOpen);
};
const navMenuClose = () => {
setNavMenuOpen(false);
};
const logoutFromNavMenu = () => {
setLoggedIn(false);
navMenuClose();
};
return (
<Router>
<div>
<Navbar
loggedIn={loggedIn}
navMenuToggle={navMenuToggle}
/>
<NavMenu
loggedIn={loggedIn}
show={navMenuOpen}
navMenuClose={navMenuClose}
logoutFromNavMenu={logoutFromNavMenu}
/>
<Switch>
<Route
path="/login"
render={(props) => (
<Login
handleLogin={handleLogin}
handleChange={handleChange}
email={email}
password={password}
errorMsg={errorMsg}
/>
)}
/>
<Route
path="/register"
render={(props) => (
<Register
handleRegister={handleRegister}
handleChange={handleChange}
email={email}
username={username}
password={password}
password2={password2}
errorMsg={errorMsg}
/>
)}
/>
<Route
path="/profile"
render={() => (loggedIn ? <Profile /> : <Redirect to="/login" />)}
/>
<Route
path="/thankyouForRegistering"
render={() =>
authorized ? (
<ThankyouForRegistering />
) : (
<Redirect to="/register" />
)
}
/>
<Route
path="/recipes"
render={(props) =>
loggedIn ? <RecipeBook /> : <Redirect to="/login" />
}
/>
<Route
path="/list"
render={(props) =>
loggedIn ? <ShoppingList /> : <Redirect to="/login" />
}
/>
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Route
path="/accountSettings"
render={(props) =>
loggedIn ? <AccountSettings /> : <Redirect to="/login" />
}
/>
<Route
exact
path="/"
component={() => <Home isLoggedIn={loggedIn} />}
/>
</Switch>
</div>
</Router>
);
}
export default App;
I figured out what the problem was. It wasn't enough to be using useHistory, I had to be using withRouter to get access to the history prop.
So I imported withRouter to my App component:
import { withRouter } from 'react-router-dom';
Then added this at the very bottom of the page, right below the App function:
const AppWithRouter = withRouter(App);
export default AppWithRouter;
So the App now must be exported as an argument of withRouter to get access to the history prop (as well as location and params I think?)
But this still wasn't enough. It was giving me an error saying that withRouter can only be used within <Router />(or <BrowserRouter />, whichever one you are using). So I wrapped my main <App /> in index.js with <BrowserRouter />
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
And imported it of course.
This still wasn't enough, though. After adding
history.push("/thankYouForRegistering);
to my axios request, it added the route to the URL, but it did not re render the view. It was still stuck on the register page. Then I realized I had <BrowserRouter /> in two different places now. In my App component, I still had it wrapped around all of my routes, so after removing that and just returning a div with <Switch /> wrapped around all of my routes, that made it work like expected.
I hope this helps somebody who is facing the same issue!

Categories

Resources