I have a question regarding lifting state up, I am aware that his is a frequently asked question on here but I hope someone will try to help me understand.
Problem: I have a login component consisting of a form component in which I do my fetch call to execute the login. I want to keep the token I get from my fetch call and save it in the App component so I can use this token and pass it down other pages/components.
So to begin with, I start with my code in my App.js / the parent component?
In here i manage my Routes, and this is also her that I want to store my state.
I pass down the token from this state to the Login component as seen on the Route that renders the Login component.
From what I read, props are not mutable so this actually doesn't work the way I thought out? I made the handleToken function but I will return to that.
export default class App extends Component {
state = {
token: "",
regNo: "",
};
handleToken = (token) => {
this.setState({ token: token})
}
render() {
console.log(this.state.token);
return (
<Router >
<div>
<Header />
<Navbar isLoggedIn={this.state.token} />
<Switch>
<Route exact path="/" render={() => <Home />} />
<Route path="/profile" render={() => <Profile userToken={this.state.token} />} />
<Route path="/login" render={() => <Login userToken={this.state.token} />} />
<Route path="/register" render={() => <Register />} />
<Route path="/carpage" render={() => <CarPage regNo={this.state.regNo} />} />
<Route path="/find-rental" render={() => <FindRental regNo={this.state.regNo} setRegno={this.setRegno}/>} />
<Route path="/rent-out-car" render={() => <RentOutCar />} />
<Route component={NoMatch} />
</Switch>
</div>
</Router >
);
};
Now we move down to the Login component. This component consist of multiple other components that makes up the login page. One of which is the FormContainer where the fetch call for the Login happens.
const Login = (props) => (
<Container fluid={true}>
<BrandRow>
</BrandRow>
<FormRow>
<FormContainer /* userToken={this.props.userToken} */ />
</FormRow>
</Container>
);
export default Login;
(Not sure why it wouldn't render it properly as a code sample, so it went into code snippet)
So as you can see in this Login component, I have the FormContainer which it were all the action happens.
Let me show you:
class FormContainer extends Component {
state = {
userName: '',
password: '',
};
handleChange = event => {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({ [name]: value });
}
handleSubmit = event => {
event.preventDefault();
const credentials = { username: this.state.userName, password: this.state.password };
this.login(credentials);
}
login = credentials => {
const url = "https://fenonline.dk/SYS_Backend/api/login";
const postHeader = {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(credentials)
};
fetch(url, postHeader).then(res => {
if (!res.ok) { throw Error(res.status + ": " + res.statusText + " | Wrong username or password!"); }
return res.json();
}).then(data => {
this.props.userToken = data.token;
alert("You have succesfully logged in!" + data.token);
this.props.history.push('/profile')
}).catch(error => alert(error));
}
render() {
const { userName, password } = this.state;
return (
<Container>
<Form onSubmit={this.handleSubmit}>
<Title>Sign In</Title>
<Input
type="text"
name="userName"
value={userName}
onChange={this.handleChange}
placeholder="Username.."
/>
<Input
type="password"
name="password"
value={password}
onChange={this.handleChange}
placeholder="Password.."
/>
<Button type="submit">Sign In</Button>
<Button onClick={() => this.props.history.push('/register')}>Register</Button>
</Form>
<Text>Rental service created by users for users.</Text>
</Container>
);
}
}
export default withRouter(FormContainer);
What you want to look at here is the login function, where I created the fetch call and this is where i get the token that i want to my lifted up state to store. Now i try and set it by saying this.props.userToken = data.token; but if props are immuteable how do I do this?
In order to lift state to your top level component, you need to pass your handleToken function into your login component as a prop.
<Route path="/login" render={() => <Login userToken={this.state.token} handleToken={this.handleToken} />} />
Then you will need to pass that into your FormContainer component as a prop as well.
<FormContainer handleToken={this.props.handleToken} />
Lastly, in your fetch, you'll need to call this method in the second .then() instead of trying to assign the token to a prop.
this.props.handleToken(data.token)
This will allow the state to be lifted up to the parent and then allow the token to be passed as a prop to your other components.
Related
I am working on a react application.
I am trying to create login and register functionality.
I have a Authorized.tsx component which looks like this
export const Authorized = (props: authorizedProps) => {
const [isAuthorized, setIsAuthorized] = useState(true);
const { claims } = useContext(AuthContext);
useEffect(() => {
if (props.role) {
const index = claims.findIndex(
claim => claim.name === 'role' && claim.value === props.role)
setIsAuthorized(index > -1);
} else {
setIsAuthorized(claims.length > 0);
}
}, [claims, props.role]);
return (
<>
{isAuthorized ? props.authorized : props.notAuthorized}
</>
);
};
interface authorizedProps {
authorized: ReactElement;
notAuthorized?: ReactElement;
role?: string;
}
This component hides and shows diffrent kind of components depending on if the user is authorized or not.
I am using this component to only show the Login.tsx component for users that are not logged in. I dont want anyone who is not logged in to be able to visit the website.
In my Index.tsx I am using the Authorized.tsx component like this
const Index = () => {
const [claims, setClaims] = useState<claim[]>([
// { name: "email", value: "test#hotmail.com" },
]);
return (
<div>
<BrowserRouter>
<AuthContext.Provider value={{ claims, update: setClaims }}>
<Authorized authorized={<App />} notAuthorized={<Login />} />
</AuthContext.Provider>
</BrowserRouter>
</div>
);
};
All the authorized users will be able to visit the site, everyone else will be asked to log in.
However, the problem I have is when I tried adding the Register.tsx component into the Login.tsx component as a navigational link.
I wish to be able to navigate between Register and Login
This is how the Login.tsx component looks like
export const Login = () => {
return (
<>
<h3>Log in</h3>
<DisplayErrors errors={errors} />
<AuthForm
model={{ email: "", password: "" }}
onSubmit={async (values) => await login(values)}
BtnText="Log in" />
<Switch>
<Route path="/register">
<Register />
</Route>
<Link to='/register'>Register</Link>
</Switch>
</>
);
};
But what actually happends when I press the 'Register' link is that the Register component gets added below the Login component
Before pressing the 'Register' link
After pressing the 'Register' link
I understand it has something to do with the Authorized.tsx component in Index.tsx.
That I am telling it to only show the Login component when not authorized.
But I dont know how I could fix it so I will be able to navigate between the Login and the Register
All help I could get would be much appreciated!
Thanks
With the current implementation you are rendering a Login component that then also renders a route for a Register component to be rendered on. Login remains mounted and rendered the entire time. From what you describe you want to render Login and Register each on their own route.
Abstract both these components into a parent component that manages the route matching and rendering.
Example
const Unauthenticated = () => (
<Switch>
<Route path="/register" component={Register} />
<Route component={Login} />
</Switch>
);
...
export const Login = () => {
...
return (
<>
<h3>Log in</h3>
<DisplayErrors errors={errors} />
<AuthForm
model={{ email: "", password: "" }}
onSubmit={login}
BtnText="Log in"
/>
<Link to='/register'>Register</Link>
</>
);
};
...
const Index = () => {
const [claims, setClaims] = useState<claim[]>([
// { name: "email", value: "test#hotmail.com" },
]);
return (
<div>
<BrowserRouter>
<AuthContext.Provider value={{ claims, update: setClaims }}>
<Authorized
authorized={<App />}
notAuthorized={<Unauthenticated />}
/>
</AuthContext.Provider>
</BrowserRouter>
</div>
);
};
So I have a straight forward app that requires you to login to see a dashboard. I've based my auth flow off of https://reactrouter.com/web/example/auth-workflow which in return bases their flow off of https://usehooks.com/useAuth/
Currently, when a user logs in it calls a function within the context provider to sign in and that function updates the state of the context with the user data retrieved from the server. This is reflected in React dev tools under my context providers as shown in the teacher attribute:
When the context state has successfully been updated I then use useHistory().push("dashboard/main") from the react-router API to go to the dashboard page. The dashboard is a consumer of the context provider but the teacher value is still null when I try rendering the page- even though React dev tools clearly shows the value has been updated. When I log in again, the dashboard will successfully render, so, ultimately, it takes two context updates in order for my Dashboard to reflect the changes and render. See my following code snippets (irrelevant code has been redacted):
App.js
const App = () => {
return (
<AuthProvider>
<div className="App">
<Switch>
<Route path="/" exact >
<Home setIsFetching={setIsFetching} />
</Route>
<ProtectedRoute path="/dashboard/:page" >
<Dashboard
handleToaster={handleToaster}
/>
</ProtectedRoute>
<ProtectedRoute path="/dashboard">
<Redirect to="/dashboard/main"/>
</ProtectedRoute>
<Route path="*">
<PageNotFound/>
</Route>
</Switch>
<Toaster display={toaster.display} setDisplay={(displayed) => setToaster({...toaster, display: displayed})}>{toaster.body}</Toaster>
</div>
</AuthProvider>
);}
AuthProvider.js
const AuthProvider = ({children}) => {
const auth = useProvideAuth();
return(
<TeacherContext.Provider value={auth}>
{children}
</TeacherContext.Provider>
);};
AuthHooks.js
export const TeacherContext = createContext();
export const useProvideAuth = () => {
const [teacher, setTeacher] = useState(null);
const memoizedTeacher = useMemo(() => ({teacher}), [teacher]);
const signin = (data) => {
fetch(`/api/authenticate`, {method: "POST", body: JSON.stringify(data), headers: JSON_HEADER})
.then(response => Promise.all([response.ok, response.json()]))
.then(([ok, body]) => {
if(ok){
setTeacher(body);
}else{
return {...body};
}
})
.catch(() => alert(SERVER_ERROR));
};
const register = (data) => {
fetch(`/api/createuser`, {method: "POST", body: JSON.stringify(data), headers: JSON_HEADER})
.then(response => Promise.all([response.ok, response.json()]))
.then(([ok, body]) => {
if(ok){
setTeacher(body);
}else{
return {...body};
}
})
.catch(() => alert(SERVER_ERROR));
};
const refreshTeacher = async () => {
let resp = await fetch("/api/teacher");
if (!resp.ok)
throw new Error(SERVER_ERROR);
else
await resp.json().then(data => {
setTeacher(data);
});
};
const signout = () => {
STORAGE.clear();
setTeacher(null);
};
return {
...memoizedTeacher,
setTeacher,
signin,
signout,
refreshTeacher,
register
};
};
export const useAuth = () => {
return useContext(TeacherContext);
};
ProtectedRoute.js
const ProtectedRoute = ({children, path}) => {
let auth = useAuth();
return (
<Route path={path}>
{
auth.teacher
? children
: <Redirect to="/"/>
}
</Route>
);
};
Home.js
const Home = ({setIsFetching}) => {
let teacherObject = useAuth();
let history = useHistory();
const handleFormSubmission = (e) => {
e.preventDefault();
const isLoginForm = modalContent === "login";
const data = isLoginForm ? loginObject : registrationObject;
const potentialSignInErrors = isLoginForm ?
teacherObject.signin(data) : teacherObject.register(data);
if(potentialSignInErrors)
setErrors(potentialSignInErrors);
else{
*******MY ATTEMPT TO PUSH TO THE DASHBOARD AFTER USING TEACHEROBJECT.SIGNIN********
history.replace("/dashboard/main");
}
};
};)};
Dashboard.js
const Dashboard = ({handleToaster}) => {
const [expanded, setExpanded] = useState(true);
return (
<div className={"dashboardwrapper"}>
<Sidebar
expanded={expanded}
setExpanded={setExpanded}
/>
<div className={"dash-main-wrapper"}>
<DashNav/>
<Switch>
<Route path="/dashboard/classroom" exact>
<Classroom handleToaster={handleToaster} />
</Route>
<Route path="/dashboard/progressreport" exact>
<ProgressReport/>
</Route>
<Route path="/dashboard/help" exact>
<Help/>
</Route>
<Route path="/dashboard/goalcenter" exact>
<GoalCenter />
</Route>
<Route path="/dashboard/goalcenter/create" exact>
<CreateGoal />
</Route>
<Route path="/dashboard/profile" exact>
<Profile />
</Route>
<Route path="/dashboard/test" exact>
<Test />
</Route>
<Route path="/dashboard/main" exact>
<DashMain/>
</Route>
</Switch>
</div>
</div>
);
};
Let me know if there's anything that stands out to you that would be preventing my Dashboard from rendering with the updated context values the first time instead of having to update it twice. Do let me know if you need more insight into my code or if I missed something- I'm also fairly new to SO. Also, any pointers on the structure of my app would be greatly appreciated as this is my first React project. Thank you.
I think the problem is in the handleFormSubmission function:
const handleFormSubmission = (e) => {
e.preventDefault();
const isLoginForm = modalContent === "login";
const data = isLoginForm ? loginObject : registrationObject;
const potentialSignInErrors = isLoginForm ?
teacherObject.signin(data) : teacherObject.register(data);
if(potentialSignInErrors)
setErrors(potentialSignInErrors);
else{
history.replace("/dashboard/main");
}
};
You call teacherObject.signin(data) or teacherObject.register(data) and then you sequentially change the history state.
The problem is that you can't be sure the teacher state has been updated, before history.replace is called.
I've made a simplified version of your home component to give an example how you could approach the problem
function handleSignin(auth) {
auth.signin("data...");
}
const Home = () => {
const auth = useAuth();
useEffect(() => {
if (auth.teacher !== null) {
// state has updated and teacher is defined, do stuff
}
}, [auth]);
return <button onClick={() => handleSignin(auth)}>Sign In</button>;
};
So when auth changes, check if teacher has a value and do something with it.
I'm trying to add simple authentication to a React+Typescript app using private routes and context. I have a simple login component with a button that just sets a boolean var authenticated in the context to true. The private routes should check this var and redirect to the login component if it's not true otherwise show the specified component. The problem is authenticated seems to always be false and I'm always redirected to the login page.
When I debug it I can see the setAuthenticated function in AuthContextProvider is called when the login button is clicked. But if I then click any of the links to private routes authenticated is always false.
Here's my App.tsx:
function App() {
return (
<AuthContextProvider>
<Router>
<Link to="/">Home</Link>
<Link to="/projects">Projects</Link>
<div>
<Route path="/login" component={Login} />
<PrivateRoute path="/" exact component={Home} />
<PrivateRoute path="/projects" component={Projects} />
</div>
</Router>
</AuthContextProvider>
);
}
export default App;
PrivateRoute.tsx:
interface PrivateRouteProps extends RouteProps {
// tslint:disable-next-line:no-any
component: any;
}
const PrivateRoute = (props: PrivateRouteProps) => {
const { component: Component, ...rest } = props;
return (
<AuthContextConsumer>
{authContext => authContext && (
<Route {...rest}
render={ props =>
authContext.authenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
)}
</AuthContextConsumer>
);
};
export default PrivateRoute;
AuthContext.tsx:
export interface AuthContextInterface {
authenticated: boolean,
setAuthenticated(newAuthState: boolean):void
}
const ctxt = React.createContext<AuthContextInterface>({
authenticated: false,
setAuthenticated: () => {}
});
export class AuthContextProvider extends React.Component {
setAuthenticated = (newAuthState:boolean) => {
this.setState({ authenticated: newAuthState });
};
state = {
authenticated: false,
setAuthenticated: this.setAuthenticated,
};
render() {
return (
<ctxt.Provider value={this.state}>
{this.props.children}
</ctxt.Provider>
);
}
}
export const AuthContextConsumer = ctxt.Consumer;
Login.tsx:
function Login() {
return (
<AuthContextConsumer>
{({ authenticated, setAuthenticated }) => (
<div>
<p>Login</p>
<form>
<input type="text" placeholder="Username"/>
<input type="password" placeholder="Password"/>
<button onClick={event => {
setAuthenticated(true);
}}>Login</button>
</form>
</div>
)}
</AuthContextConsumer>
);
}
export default Login;
My suspicious is that there's something wrong with the state definition in AuthContextProvider. If I change authenticatedin here to true I see the opposite behaviour, I never see the login page. Should this be something dynamic?
Or, in the onClick callback, set event.preventDefault() so it doesn't submit the form.
The problem turned out to be that the app was reloading every time the login button was pressed, and therefore lost the state in the AuthContext.
The reason for this is that in my Login component I had a button inside a form, which automatically submits the form and reloads the page.
The solution is to either remove the form tags, or in the button specify the attribute type="button".
I am trying to do some basic conditional rendering based on user login. I have my event handlers and axios call in a Login component.
const Login = () => {
const handleChange = event => {
setCustomerLogin({
...customerLogin,
[event.target.name]: event.target.value
});
};
const handleSubmit = e => {
e.preventDefault();
axios
.post("/api/Authentication", customerLogin)
.then(function(response) {
setCustomerLogin(response.data);
console.log(response);
})
.catch(function(error) {
console.log(error);
});
};
My Navbar component is very basic right now and just automatically renders my SignedOutLinks, which are the links I display before a user is logged in.
const Navbar = () => {
return (
<nav className="nav-wrapper blue darken-4">
<div className="container">
<Link to='/' className="brand-logo left">Cars4U</Link>
<SignedOutLinks />
</div>
</nav>
)
};
I would like to define my setCustomerLogin function in App.js and have my Login component call this value. This is my App.js file so far, I am just uncertain how to define the function in my App.js and set the state in my Login component
const [customerLogin, setCustomerLogin] = useState([
{ username: "", password: "" }
]);
function App() {
return(
<div className="App">
<Navbar />
<Switch>
<Route path='/login' component={Login}/>
<Route path='/signup' component={Signup}/>
</Switch>
</div>
);
}
You can pass the state setter(setCustomerLogin) and state value(customerLogin) down to your Login component as props:
const [customerLogin, setCustomerLogin] = useState([
{ username: "", password: "" }
]);
function App() {
return(
<div className="App">
<Navbar />
<Switch>
<Route path='/signup' component={Signup}/>
<Route
path="/login"
render={() =>
<Login
customerLogin={customerLogin}
setCustomerLogin={setCustomerLogin}
/>}
/>
</Switch>
</div>
);
}
Note that I used a little different syntax for routing the Login component, you are still going to get the same result, only that now you can pass in any props you want to the component to render. You can read more about that kind of routing here.
And then, you can access them in the Login component via props:
const Login = ({setCustomerLogin, customerLogin}) => {
const handleChange = event => {
setCustomerLogin({
...customerLogin,
[event.target.name]: event.target.value
});
};
const handleSubmit = e => {
e.preventDefault();
axios
.post("/api/Authentication", customerLogin)
.then(function(response) {
setCustomerLogin(response.data);
console.log(response);
})
.catch(function(error) {
console.log(error);
});
};
I am trying to pass some props from React class to my functional component which is formik and then I want to add some callback function to get those data back on text changed. But not sure how can i make this done. Please check my below code:
Here is my Main.jsx class
// Some imports were removed to keep everything looks cleaner
import RegisterAccount from "RegisterAccount.jsx";
class Main extends React.Compoenent {
constructor(props) {
super(props);
this.state = {
username: "This is username value..."
}
}
render() {
return (
<Container fluid>
<Switch>
<Route exact path="/register" component={RegisterAccount} data={this.state.username} />
</Switch>
</Container>
)
}
}
export default Main;
Here is my RegisterAccount.jsx
// Some imports were removed to keep everything looks cleaner
import { Form as FormikForm, Field, withFormik } from "formik";
import * as Yup from "yup";
const App = ({ values, errors, touched }) => (
<FormikForm className="register-form " action="">
<h3 className="register-title">Register</h3>
<Form.Group className="register-form-group">
<Field
name="username"
type="text"
placeholder="USERNAME"
className="form-control rounded register-form-control"
/>
{touched.username && errors.username && <p>{errors.username}</p>}
</Form.Group>
</FormikForm>
);
const FormikApp = withFormik({
mapPropsToValues({ username, email, password, confirmPassword }) {
return {
username: username || ""
};
},
validationSchema: Yup.object().shape({
username: Yup.string()
.min(6)
.max(32)
.required()
.test("username", "error message of username", async value => {
return true;
})
})(App);
export default FormikApp;
It looks like the issue you're seeing isn't from Formik, but from React Router. Your Route won't pass props in as data the way you have it:
<Route exact path="/register" component={RegisterAccount} data={this.state.username} />
Instead, you have to use Route's render prop and pass props directly into the component. This should pass username into mapPropsToValues:
<Route exact path="/" render={() => <RegisterAccount username={this.state.username} />} />