React function triggered reloading? - javascript

So, I have a parent function getData() which will request data from local server. I pass the function to my submit form. I call the function when submitting the form. It get called, but my form state keeps getting reset by itself. Why is this happening?
Also is there a way to refactor my Form useState, I kinda use too many useState I guess.
Thanks before.
This is my Form
const AddForm = ({ getData, classes }) => {
console.log("Rendering AddFORM");
const [checkboxes, setChecked] = React.useState([]);
const [inputs, setInputs] = React.useState({});
const [files, setFiles] = React.useState(null);
const [open, isOpen] = React.useState(false);
const [success, isSuccess] = React.useState(false);
const history = useHistory();
React.useEffect(() => {
setInputs({ ...inputs, platform: checkboxes });
}, [checkboxes]);
const onChangeForField = React.useCallback(({ target: { name, value } }) =>
setInputs((state) => ({ ...state, [name]: value }), [])
);
const onChangeForFiles = ({ target: { files } }) => setFiles(files);
const handleCheck = ({ target: { value } }) => {
checkboxes.includes(value)
? setChecked(checkboxes.filter((item) => item !== value))
: setChecked([...checkboxes, value]);
};
const handleClose = () => isSuccess(false);
async function submitForm() {
isOpen(true);
const formdata = new FormData(); // for adding form files i guess
for (let i = 0; i < files.length; i++) {
formdata.append("files", files[i], files[i].name); // "name", files, filename
}
for (let key in inputs) {
formdata.append(key, inputs[key]);
}
const response = await axios.post("/games", formdata);
const { data } = response;
isOpen(false);
isSuccess(true);
console.log(data.message);
}
return (
<Paper elevation={2} className={classes.Container}>
<form
onSubmit={async (e) => {
e.preventDefault();
await submitForm();
// getData();
// history.push("/");
}}
>
<Snackbar open={success} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success">
Game successfully added!
</Alert>
</Snackbar>
<Typography variant="h5" color="initial">
Add a new game
</Typography>
<TextField
required
id="standard-required"
name="title"
label="Title"
fullWidth
placeholder="Game title"
margin="normal"
onChange={onChangeForField}
/>
<TextField
fullWidth
multiline
required
id="standard-required"
name="description"
label="Description"
placeholder="Description"
margin="normal"
onChange={onChangeForField}
/>
<FormControl margin="normal" fullWidth>
<FormLabel component="legend">Select Platforms</FormLabel>
<FormGroup row>
{platforms.map((p, idx) => (
<FormControlLabel
key={idx}
control={
<Checkbox name="platforms" onChange={handleCheck} value={p} />
}
label={p}
/>
))}
</FormGroup>
</FormControl>
<FormControl>
<Button variant="contained">
<label htmlFor="file-upload">Upload Files</label>
</Button>
<input
type="file"
multiple
id="file-upload"
name="images"
style={{ display: "none" }}
onChange={onChangeForFiles}
></input>
</FormControl>
<div className={classes.ButtonsContainer}>
<Button
variant="contained"
color="primary"
type="submit"
onClick={(e) => {
e.stopPropagation();
}}
>
Submit
</Button>
<Button variant="contained" color="secondary">
<Link to="/">Go Back</Link>
</Button>
</div>
</form>
<Backdrop className={classes.backdrop} open={open}>
<Paper elevation={3} className={classes.Loading}>
<Typography variant="body1" align="center">
Submitting your form
</Typography>
<CircularProgress color="primary" className={classes.Circular} />
</Paper>
</Backdrop>
<Button onClick={() => isSuccess(true)}>Text</Button>
</Paper>
);
};
This is the parent which hold the getData()
function App() {
const [games, setGames] = React.useState([]);
React.useEffect(() => {
getData();
}, []);
async function getData() {
const response = await axios.get("/games");
const { data } = response;
setGames(data);
}
return (
<div className="App">
<Navbar />
<Container maxWidth="lg" style={{ margin: "1.5rem auto" }}>
<Switch>
<Route
path="/"
exact
component={() => <GameList games={games} getData={getData} />}
/>
<Route
path="/add"
exact
component={() => <AddForm getData={getData} />}
/>
<Route path="/addimg" exact component={() => <ImageForm />} />
<Route
path="/games/:id"
exact
component={() => <GameDetails getData={getData} />}
/>
<Route
path="/games/:id/edit"
exact
component={() => <EditForm getData={getData} />}
/>
<Route
path="/games/:id/reviews/:rid/edit"
exact
component={() => <EditReviewForm getData={getData} />}
/>
</Switch>
</Container>
</div>
);
}

Issue
You are rendering all your components on routes with anonymous components via the component prop, this cause new components to be created and mounted.
component
When you use component (instead of render or children, below) the
router uses React.createElement to create a new React element from the
given component. That means if you provide an inline function to the
component prop, you would create a new component every render. This
results in the existing component unmounting and the new component
mounting instead of just updating the existing component. When using
an inline function for inline rendering, use the render or the
children prop (below).
Solution
Use the render prop to pass additional props to your route components.
Since ImageForm component isn't being passed any additional props it can be passed directly to the component prop of a Route.
Additionally, you should reorder your routes so you specify more specific paths prior to less specific paths, so they can be attempted to be matched first. This removes the need to append the exact prop to every route for matching.
<Switch>
<Route path="/add" render={() => <AddForm getData={getData} />} />
<Route path="/addimg" component={ImageForm} />
<Route
path="/games/:id/edit"
render={() => <EditForm getData={getData} />}
/>
<Route
path="/games/:id/reviews/:rid/edit"
render={() => <EditReviewForm getData={getData} />}
/>
<Route
path="/games/:id"
render={() => <GameDetails getData={getData} />}
/>
<Route
path="/"
render={() => <GameList games={games} getData={getData} />}
/>
</Switch>
Also is there a way to refactor my Form useState, I kinda use too many
useState I guess.
IMO you don't have "too may" state hooks. You can either keep all your state values simple and separate, or you can combine them into a more complex state object. Selecting one over the other is a subjective issue. In my opinion, each "chunk" of state should be capable of standing on its own, as a single atomic entity. As such it seems you've separated the state concerns sufficiently (checkboxes, inputs, toggles, etc...). I think trying to merge your state would only make your state updates needlessly more complex.

Related

How do I pass down my search results to other components as a prop

Looking to solve how to pass my search results to other components so when users use the search bar, the searched results gets displayed instead of that components rendered data.. in this case it would homeScreen. using react router v5 and i tried passing it through the router but many attempts didn't work, should i create a seperate search router too?
App.js:
<Container>
<Route path="/" component={HomeScreen} exact />
<Route path="/login" component={LoginScreen} exact />
<Route path="/register" component={RegisterScreen} exact />
<Route path="/product/:id" component={ProductScreen} exact />
<Route path="/cart/:id?" component={CartScreen} exact />
</Container>
header.js:
function Header() {
const userLogin = useSelector((state) => state.userLogin);
const { userInfo } = userLogin;
// const [items, setItems] = useState("");
const [searchResults, setSearchResults] = useState([]);
const debounce = useDebounce(searchResults, 500);
const dispatch = useDispatch();
const logoutHandler = () => {
dispatch(logout());
};
useEffect(() => {
axios.get(`/api/search/?search=${searchResults}`).then((response) => {
setSearchResults(response.data[0]);
console.log(response.data[0]);
});
}, [debounce]);
const handleSearch = (e) => {
setSearchResults(e.target.value);
};
return (
<div>
<Navbar bg="dark" variant="dark" className="navCustom">
<Container>
<LinkContainer to="/">
<Navbar.Brand>eCommerce</Navbar.Brand>
</LinkContainer>
<Form className="d-flex">
<Form.Control
type="search"
placeholder="Search"
className="me-2"
aria-label="Search"
onChange={handleSearch}
/>
<Button variant="outline-success">Search</Button>
</Form>
HomeScreen.js:
function HomeScreen({ searchResults }) {
const dispatch = useDispatch();
const productList = useSelector((state) => state.productList);
const { error, loading, products } = productList;
useEffect(() => {
dispatch(listProducts());
}, [dispatch]);
return (
<div>
{searchResults.length > 0 ? (
<Row>
{searchResults.map((product) => (
<Col key={product._id} sm={12} md={6} lg={4} xl={3}>
<Product product={product} />
</Col>
))}
</Row>
) : (
// Fall back to rendering regular products
<Row>
{products &&
products.map((product) => (
<Col key={product._id} sm={12} md={6} lg={4} xl={3}>
<Product product={product} />
</Col>
))}
</Row>
)}
</div>
);
}
export default HomeScreen;
This seems like a good use case of React Context. Within the Context, you can use useState to set the results. The Context can be provided to other components within your app.

React Router Redirect does not render the component

Here is my App.js:
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Switch>
<PrivateRoute path="/" component={MainPage} />
<PrivateRoute path="/admin" component={MainPage} />
<Container
className="d-flex align-items-center justify-content-center"
style={{ minHeight: "100vh" }}
>
<div className="w-100" style={{ maxWidth: "400px" }}>
<Route path="/signup" component={Signup} />
<Route path="/login" component={Login} /> //The component part
<Route path="/forgot-password" component={ForgotPassword} />
</div>
</Container>
</Switch>
</AuthProvider>
</BrowserRouter>
);
}
Auth Context for PriveRoute Component from firebase:
export default function PrivateRoute({ component: Component, ...rest }) {
const { currentUser } = useAuth();
return (
<Route
render={(props) => {
return currentUser ? <Admin {...props} /> : <Redirect to="/login" />; // it redirects when the user does not log in.
}}
></Route>
);
Login Component:
export default function Login() {
const emailRef = useRef();
const passwordRef = useRef();
const { login, currentUser } = useAuth();
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const history = useHistory();
if (currentUser) {
history.push("/admin/mainpage");
}
async function handleSubmit(e) {
e.preventDefault();
try {
setError("");
setLoading(true);
await login(
emailRef.current.value + "#myosmail.com",
passwordRef.current.value
);
history.push("/admin/mainpage");
} catch {
setError("Failed to log in");
}
setLoading(false);
}
return (
<>
<Card>
<Card.Body>
<h2 className="text-center mb-4">Log In</h2>
{error && <Alert variant="danger">{error}</Alert>}
<Form onSubmit={handleSubmit}>
<Form.Group id="email">
<Form.Label>Username</Form.Label>
<Form.Control ref={emailRef} required />
</Form.Group>
<Form.Group id="password">
<Form.Label>Password</Form.Label>
<Form.Control type="password" ref={passwordRef} required />
</Form.Group>
<Button disabled={loading} className="w-100" type="submit">
Log In
</Button>
</Form>
<div className="w-100 text-center mt-3">
<Link to="/forgot-password">Forgot Password?</Link>
</div>
</Card.Body>
</Card>
<div className="w-100 text-center mt-2">
Need an account? <Link to="/signup">Sign Up</Link>
</div>
</>
);
}
I want to build a component if the user does not logged in, it can not access to content of my web site so I build the component above. But the problem is that when I opened my page and does not logged in the page redirect me to "/login" but the Login component does not be rendered I do not understand what is the problem. I debugged my component and I saw that my code firstly goes to PrivateRoute component after that it redirected to "/login" but nothing rendered when the page redirected to Login. When I removed PrivateRoutes from App.js my code worked correctly.
Issue
My guess is that you've placed a "/" path first within the Switch component:
<PrivateRoute path="/" component={MainPage} />
The redirect to "/login" works but then the Switch matches the "/" portion and tries rendering this private route again.
Your private route is also malformed, it doesn't pass on all the Route props received.
Solution
Fix the private route component to pass on all props.
export default function PrivateRoute({ component: Component, ...rest }) {
const { currentUser } = useAuth();
return (
<Route
{...rest}
render={(props) => {
return currentUser ? <Admin {...props} /> : <Redirect to="/login" />;
}}
/>
);
}
Within the Switch component path order and specificity matter, you want to order more specific paths before less specific paths. "/" is a path prefix for all paths, so you want that after other paths. Nested routes also need to be rendered within a Switch so only a single match is returned and rendered.
<BrowserRouter>
<AuthProvider>
<Switch>
<Container
className="d-flex align-items-center justify-content-center"
style={{ minHeight: "100vh" }}
>
<div className="w-100" style={{ maxWidth: "400px" }}>
<Switch>
<Route path="/signup" component={Signup} />
<Route path="/login" component={Login} /> //The component part
<Route path="/forgot-password" component={ForgotPassword} />
</Switch>
</div>
</Container>
<PrivateRoute path={["/admin", "/"]} component={MainPage} />
</Switch>
</AuthProvider>
</BrowserRouter>
Update
I'm a bit confused by your private route though, you specify the MainPage component on the component prop, but then render an Admin component within. Typically an more agnostic PrivateRoute component may look something more like:
const PrivateRoute = props => {
const { currentUser } = useAuth();
return currentUser ? <Route {...props} /> : <Redirect to="/login" />;
}
This allows you to use all the normal Route component props and doesn't limit to using just the component prop.
Usages:
<PrivateRoute path="/admin" component={Admin} />
<PrivateRoute path="/admin" render={props => <Admin {...props} />} />
<PrivateRoute path="/admin">
<Admin />
</PrivateRoute>
I also had same problem once. This solved me. Wrap your three routes with a Switch.
<Switch>
<Route path="/signup" component={Signup} />
<Route path="/login" component={Login} />
<Route path="/forgot-password" component={ForgotPassword} />
</Switch>
As the first private route has the root path it will always go to the route. You can use exact for the first private route. But the best way should be placing the first private route
<PrivateRoute path={["/admin", "/"]} component={MainPage} />
at the bottom. So that when there is no match it goes there only.
You are not passing the path to the Route in the ProtectedRoute .
<Route
{...rest} // spread the test here
render={(props) => {
return currentUser ? <Admin {...props} /> : <Redirect to="/login" />; // it redirects when the user does not log in.
}}
></Route>
Also switch the Route of the path as #Drew Reese mentioned .

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

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
});

When using render props in a component, what are some common pitfalls to avoid infinite re-rendering?

Let us say we are implementing protected routes in react using react-router-dom, what are some things we have to keep in mind such as when rendering private route the component does go into an infinite re-rendering loop.
This route is giving me an error of maximum call stack exceeded react prevents infinite rerendering. What is wrong in this component?
PrivateRoute.jsx
const PrivateRoute = ({
component: Component,
isAuthenticated,
token,
...rest
}) => {
const employee = decoder(token);
return (
<Route
{...rest}
render={props =>
isAuthenticated && employee.user.emp_role === "admin" ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
};
PrivateRoute.propTypes = {
isAuthenticated: PropTypes.bool
};
const mapStateToProps = state => ({
isAuthenticated: state.authReducer.isAuthenticated,
token: state.authReducer.token
});
export default connect(mapStateToProps, { loadUser })(PrivateRoute);
I'm using mapping to props from redux state, picking necessary parts such as isAuthenticated and token, token is for checking for role which could be admin or employee, putting them in ternary operator and so on. Where is the problem in this component?
EDIT 1:
App.js
//imports here
if (localStorage.token) {
setAuthToken(localStorage.token);
}
function App() {
useEffect(() => {
store.dispatch(loadUser());
}, []);
return (
<Provider store={store}>
<Router>
<Alert />
<Switch>
<Layout>
<Switch>
<Route exact path="/" component={Landing} />
<Route exact path="/login" component={Login} />
<AdminRoutes exact path="/admin" component={Admin} />
<AdminRoutes exact path="/employees" component={Employees} />
<AdminRoutes exact path="/profile" component={Profile} />
<AdminRoutes exact path="/chemists" component={Chemists} />
<AdminRoutes
exact
path="/distributors"
component={Distributors}
/>
<AdminRoutes exact path="/doctors" component={Doctors} />
<AdminRoutes exact path="/products" component={Products} />
</Switch>
</Layout>
</Switch>
<Switch>
<PrivateRoute
exact
path="/representative"
component={Representative}
/>
</Switch>
</Router>
</Provider>
);
}
export default App;
Login.jsx
const Login = ({ login, isAuthenticated, token }) => {
const [formData, handleFormData] = useState({
emp_phone: "",
emp_password: ""
});
const { emp_password, emp_phone } = formData;
const onSubmit = async e => {
e.preventDefault();
login(emp_phone, emp_password);
};
const handleLogin = e => {
handleFormData({ ...formData, [e.target.name]: e.target.value });
};
// if (isAuthenticated) {
// const employee = decoder(token);
// if (employee.user.emp_role === "admin") {
// return <Redirect to="/admin" />;
// } else return <Redirect to="/representative" />;
// }
return (
<Fragment>
<div className="w-full max-w-sm shadow-md rounded p-5 m-3 align-middle h-auto">
<form onSubmit={e => onSubmit(e)}>
<div className="field self-center">
<label className="label" htmlFor="phone">
Phone Number
</label>
<div className="control">
<input
type="text"
pattern="[0-9]*"
id="phone"
placeholder="Enter phone number"
name="emp_phone"
required
value={emp_phone}
onChange={e => handleLogin(e)}
className="input"
/>
</div>
</div>
<div className="field">
<label className="label" htmlFor="password">
Password
</label>
<div className="control">
<input
type="password"
placeholder="Enter password"
name="emp_password"
id="password"
value={emp_password}
required
onChange={e => handleLogin(e)}
className="input"
/>
</div>
</div>
<button type="submit" className="button is-primary">
Login
</button>
</form>
</div>
</Fragment>
);
};
Login.propTypes = {
login: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool
};
const mapStateToProps = state => ({
isAuthenticated: state.authReducer.isAuthenticated,
token: state.authReducer.token
});
export default connect(mapStateToProps, { login, loadUser })(Login);
I think the problem might be due to used logic. This happened to me too some weeks ago.
isAuthenticated && employee.user.emp_role === "admin" ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
In above code the value of isAuthenticated when null or false ( during loading from local storage) can be null multiple times which triggers Redirect component multiple times.
Can you try below logic
!isAuthenticated && employee.user.emp_role !== "admin" ? (
<Redirect to="/login" />
) : (
<Component {...props} />
)

Can't use props in child component when using Formik for building a wizard

I am trying to build a form-wizard. I set up the wizard via react-router and used formik for the forms. Now I ran into a problem while creating a customizable radio-button. I used the react-custom-radio library for that.
When I use the radio-buttons outside of the routes, it is working as it should (code at the bottom of the post).
When I use the with the router, the props are passed down to the child component:
<Route path="/form/location" render={(props) => (<Pricing {...props} />)} />
Inside the child component, I access the props the same way, as I did it in the parent, where it worked.
const Pricing = (props) => {
const {
values,
touched,
errors,
setFieldValue,
setFieldTouched,
} = props;
return (
<div>
<MyRadio
value={values.car}
onChange={setFieldValue}
onBlur={setFieldTouched}
error={errors.car}
touched={touched.car}
/>
</div>
);
}
But now I always get the error Cannot read property 'car' of undefined.
Why doesn't it work if there's a router in between?
If I do it like that, it works:
<Form>
<Switch>
<Redirect from="/" exact to="/form/location" />
<Route path="/form/location" render={(props) => (<Pricing {...props} />)} />
</Switch>
<MyRadio
value={values.car}
onChange={setFieldValue}
onBlur={setFieldTouched}
error={errors.car}
touched={touched.car}
/>
</Form>
The props given to the render function are the route props listed in the documentation. What you want to do in this case is to pass down the props from the parent component, not the route props:
class ParentComponent extends React.Component {
render() {
const { props } = this;
const {
values,
touched,
errors,
setFieldValue,
setFieldTouched,
} = props;
return (
<Form>
<Switch>
<Redirect from="/" exact to="/form/location" />
<Route
path="/form/location"
render={() => <Pricing {...props} />}
/>
</Switch>
<MyRadio
value={values.car}
onChange={setFieldValue}
onBlur={setFieldTouched}
error={errors.car}
touched={touched.car}
/>
</Form>
);
}
}
And if you need both Formik's props and this component's you could do:
render={(formikProps) => <Pricing {...formikProps}, {...props} />}
That will create a long list of attributes from both props for Pricing to use.

Categories

Resources