React-Hooks, thoughts regarding my implementation - good / bad?

Viewed 86

I have been implementing an architecture pattern that I found here. The examples below are my implementation of what I learned from that site. I'll admit, with first impressions I am really liking the feel of this approach. But I am no means an expert on hooks or more advanced "Reacty" knowledge. All of the hooks below are integrated with Redux (the model), they decouple state and logic from the view (screen), and serve as what I figure to be controllers. Again, I'm not an expert on this stuff but that is how it currently feels. So, by all accounts assuming my interpretation is correct what I've been implementing is a hooks-based MVC architecture.

My question is, what are areas of concern with the approach below. I have been struggling to write tests for it, but I think that's more in regards to my lack of testing experience than anything else.

So, here is the most involved implementation so far: My signup/registration form that interacts with redux, component state (formData), and authentication.


useAuthHooks (controller?)

Takes the jwt token and isAuthenticated boolean from the Redux store auth object. All forms related to login/registration require access to currently at most those elements of formData state. If a user logs in, registers, or logs out, the associate action is dispatched with the correct arguments as needed by Redux.

import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { logout, login, register } from '../redux/actions/auth';

const useAuthHooks = () => {
  const { token, isAuthenticated } = useSelector((state) => state.auth);
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    password2: '',
  });

  const { username, email, password } = formData;

  const dispatch = useDispatch();
  const loginUser = () => dispatch(login(email, password));
  const registerUser = () => dispatch(register({ username, email, password }));
  const logoutUser = () => dispatch(logout());

  return {
    state: { formData, setFormData },
    loginUser,
    registerUser,
    logoutUser,
    token,
    isAuthenticated,
  };
};

export default useAuthHooks;

Form (I don't know what this would be interpreted as in other architectures)

My Form has a format (either "login" or "register") that is passed in as a prop. My Form needs (or at least has access to) everything that exists in the above useAuthHooks. Additionally, my form also sends out alerts that are handled in Redux. So, I spread the useAlertHooks in as well. Lastly, the format is being used to determine the FormScreen output so it get's passed along as a prop.

import React from 'react';
import useAuthHooks from '../../hooks/useAuthHooks';
import useAlertHooks from '../../hooks/useAlertHooks';
import FormScreen from './FormScreen';

const Form = ({ format }) => {
  const { ...authProps } = useAuthHooks();
  const { ...alertProps } = useAlertHooks();

  return <FormScreen {...authProps} {...alertProps} format={format} />;
};

export default Form;

FormScreen (Ok, this one is definitely the view)

We destructure the props and use them accordingly. Nothing too wild. Don't be put off by all the camel case elements like <Label>, <Input> for example. They're part of styled-components.

import React from 'react';
import styled from 'styled-components';
import { Link, Redirect } from 'react-router-dom';
import styles, { P, H200 } from '../../ui/styles/variables';
import RectButton from '../_buttons/RectButton';

const { borders, colors, templates } = styles;

const FormScreen = ({
  format,
  renderAlert,
  registerUser,
  loginUser,
  isAuthenticated,
  state,
}) => {
  const { username, email, password, password2 } = state.formData;

  const onChange = (e) => {
    state.setFormData({ ...state.formData, [e.target.name]: e.target.value });
  };

  // # Redirect if logged in
  if (isAuthenticated) {
    return <Redirect to="/profile" />;
  }

  const onRegister = async (e) => {
    e.preventDefault();
    if (password !== password2) {
      renderAlert('Passwords do not match', 'success');
    } else {
      registerUser();
    }
  };

  const onLogin = async (e) => {
    e.preventDefault();
    loginUser();
  };

  const registerForm = (
    <>
      <H200> Create an Account</H200>
      <FormContent onSubmit={(e) => onRegister(e)}>
        <Label htmlFor="username">Username</Label>
        <Input
          type="text"
          name="username"
          value={username}
          onChange={(e) => onChange(e)}
        />
        <Label htmlFor="email">Email</Label>
        <Input
          type="email"
          name="email"
          value={email}
          onChange={(e) => onChange(e)}
        />
        <Label htmlFor="password">Password</Label>
        <Input
          type="password"
          name="password"
          value={password}
          onChange={(e) => onChange(e)}
        />
        <Label htmlFor="password2">Confirm Password</Label>
        <Input
          type="password"
          name="password2"
          value={password2}
          onChange={(e) => onChange(e)}
        />
        <RectButton text="Register" />
        <Options>
          <P>Already have an account?</P>
          <Link to="/login">Sign in</Link>
        </Options>
      </FormContent>
    </>
  );

  const loginForm = (
    <>
      <H200>Log into Account</H200>
      <FormContent onSubmit={(e) => onLogin(e)}>
        <Label htmlFor="email">Email</Label>
        <Input
          type="email"
          name="email"
          value={email}
          onChange={(e) => onChange(e)}
        />
        <Label htmlFor="password">Password</Label>
        <Input
          type="password"
          name="password"
          value={password}
          onChange={(e) => onChange(e)}
        />
        <RectButton text="Login" />
        <Options>
          <P>Don't have an account?</P>
          <Link to="/register">Register</Link>
        </Options>
      </FormContent>
    </>
  );

  return format === 'register' ? registerForm : loginForm;
};

export default FormScreen;

// ... Everything below this line was styled-components stuff

LoginPage (I also have a separate (very similar) page for the register version.)

For my LoginPage, I want the login version of the Form so wallah, I pass in the "login" argument.

import React from 'react';
import { Page, CColumn, CHeader } from '../../../ui/styles/layouts';
import Form from '../../../components/_auth/Form';
import Alert from '../../../containers/_alerts/Alert/Alert';

const LoginPage = () => {
  return (
    <Page>
      <CHeader>
        <Alert />
      </CHeader>
      <CColumn>
        <Form format="login" />
      </CColumn>
    </Page>
  );
};

export default LoginPage;

The benefits of this approach is I can (and have) very easily add and remove hooks that are not needed in a given Screen. They gain access to everything (spread) and ... now that I come to think of it ... maybe can be given specific bits of logic (simply by not spreading) from the custom hooks. Not sure if that will work, but worth a try. Additionally, my hook behaviors are all held in one location, my view's only handle inputs and generic navigational features, and I am respecting the single-source of truth being Redux. That's my takeaway at least. Curious what others think.

Thank you :)

0 Answers
Related