DJ2
DJ2

Reputation: 1751

Provide formik form value to context provider

I have a <Login> component/form that uses <Formik> to keep track of my forms state. There's a specific value props.values.userType that I want to pass to a context provider so I can pass this prop down to my routing component.

My goal is to redirect users that aren't logged in as admin and if they are indeed an admin proceed to render the route as normal.

So I created an AuthContext.

const AuthContext = React.createContext();

In my <Login> component I have the <Formik> component below. Where should I use AuthContext.Provider and how should I pass values.props.userType to that provider? Should props.values.userType be initialized in state of the class component this <Formik> component lives in ?

Or should I create an object store in state that keeps track of the userType? Something like this

export const AuthContext = createContext({
  user: null,
  isAuthenticated: null
});

I have a codesandbox here.

class FormikLoginForm extends Component {
  constructor(props) {
  super(props);

this.state = {};
}

render() {
const {} = this.state;

return (

<Formik
      initialValues={{
        username: "",
        password: "",
        userType: "",
      }}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 500);
      }}
      validationSchema={Yup.object().shape({
        userType: Yup.string().required("User type is required"),
        username: Yup.string().required(
          "Required -- select a user type role"
        ),
        password: Yup.string().required("Password is required"),
      })}
    >
      {props => {
        const {
          values,
          touched,
          errors,
          dirty,
          isSubmitting,
          handleChange,
          handleBlur,
          handleSubmit,
          handleReset
        } = props;
        return (
          <>
            <Grid>
              <Grid.Column>
                <Header as="h2" color="teal" textAlign="center">
                  Log-in to your account
                </Header>
                <Form size="large" onSubmit={handleSubmit}>
                  <Segment stacked>
                    <RadioButtonGroup
                      id="userType"
                      label="User Type"
                      value={values.userType}
                      error={errors.userType}
                      touched={touched.userType}
                    >

Then in my index.js file, where I render all my routes, I have my AdminRoute that uses the logic I described above

const AdminRoute = props => {
  const { userType, ...routeProps } = props;
  if (userType !== "admin") return <Redirect to="/login" />;
  return <Route {...routeProps} />;
};

const Routes = () => (
  <Router>
    <Switch>
      <Route exact path="/" component={Home} />
      <Route path="/login" component={FormikLoginForm} />
      <Route exact path="/admin" component={AdminPage} />
      />
      <Route path="/admin/change-password" component={ChangePassword} />
    </Switch>
  </Router>
);

Upvotes: 3

Views: 7989

Answers (1)

Simon Ingeson
Simon Ingeson

Reputation: 991

As you are using React Router I would recommend following the example in their docs for authentication flow and create a PrivateRoute component and use that in place of the regular Route component.

Formik itself wouldn't be the solution to this, it's simply a way to make it easier to interact with forms and perform form validation. Letting the user pass their own type that's later used for authorization does not seem to be a good idea unless that's also somehow required for the authentication flow.

As for the userType it seems to me it's something you should get as a claim from a successful login via an API endpoint, or from whatever login backend you are using. And yes, you could store that in your AuthContext and use it like so (assuming you use React 16.8+ in your project setup):

function AdminPrivateRoute({ component: Component, ...rest }) {
  const auth = React.useContext(AuthContext);
  return (
    <Route
      {...rest}
      render={props =>
        auth.isAuthenticated && auth.userType === 'admin' ? (
          <Component {...props} />
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: props.location }
            }}
          />
        )
      }
    />
  );
}

The AuthContext.Provider component should be used close to, if not at, the top of the component tree. In your code sample I'd say in the Routes component. You probably also want to implement a way to interact with the context as it will need to be dynamic. Based on the React documentation it could look something like this:

// may want to pass initial auth state as a prop
function AuthProvider({ children }) {
  const [state, setState] = React.useState({
    isAuthenticated: false,
    userType: null,
    // other data related to auth/user
  });

  // may or may not have use for React.useMemo here
  const value = {
    ...state,
    // login() does not validate user credentials
    // it simply sets isAuthenticated and userType
    login: (user) => setState({ isAuthenticated: true, userType: user.type }),
    // logout() only clears isAuthenticated, will also need to clear auth cookies
    logout: () => setState({ isAuthenticated: false, userType: null }),
  };

  return (
    <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
  );
}

Upvotes: 2

Related Questions