Search code examples
javascriptreactjsmvp

React hooks, Error handling. How should I do when using MVP(Model-view-presenter)?


I'm doing a login view for my application and I have at tough time getting this right. I'm pretty new at this so the solution may be trival for you. I have a presenter and a login view, now I want to catch the error in the presenter and then send it over as prop to my view and handle it there. How should I go about solving this?

My first solution was to just solve everything in the presenter but that seemed wrong to do that because it doesn't follow the rules of MVP. I did this in the presenter :

try {await props.model.signIn(email, password)
}
catch (error) {
var errorMessage = error.message;
if (errorMessage.includes("password")) {
document.getElementById("error").innerHTML = "Wrong password, try again"}

This works but it doesn't follow the rules of MVP.

The solution was then to use react state hooks for error handling. What I want to do is when I catch the error in the presenter, to send it down as a prop to the view then handle the error there instead.

I began by declaring the state:

const [error,setError] = React.useState('') 

then in the catch I did:

setError(error)

Then I send it to my view as a prop:

return <LoginView onUserLogIn={handleLoginACB} error = {error} />

LoginView has this in the render to show the error to the user:

  <div id = "error">{errorHandling()}</div>

I then do the error handling in a function in view called errorHandling.

But then when I handle it in my view it says that the props.error is not defined. What is the proper way to send an error prop from the presenter to view, and display it?

import LoginView from "../views/loginView";
import React from "react";
function Login(props) {
const [error,setError] = React.useState('') 

async function handleLoginACB() {
        var email = document.getElementById('email').value
        var password = document.getElementById('password').value
        try {
            await props.model.signIn(email, password)
        } catch (error) {
            console.log(error.message)
            setError(error)
            
          
        }

        if (props.model.currentUser != null) {
            window.location.hash = "#HomeScreen"
        }

    }

    return <LoginView onUserLogIn={handleLoginACB} error = {error} />
}

export default Login;

function LoginView(props) {


    return( <div className="loginBox">
    <img src="logo.svg" className="image blob"/>
        <form>
        <input type = "email" placeholder = "Email" id="email"></input> 
        <div><input type = "password" placeholder = "Password" id="password"></input></div>
        <div id = "error">{errorHandling}</div>
        <a href="#CreateAccount">Create a new account</a>
        
        </form>
        <button  onClick={props.onUserLogIn}>Log in</button>
</div>);

function errorHandling(){
    
    console.log(props.error.message)
    if (props.error.message.includes("password")) {
        return "Wrong password, try again"
      }

      if (props.error.message.includes("email")) {
          return "Wrong email, try again"
      }
}


}

export default LoginView;

Solution

  • This does not feel Reacty. Also, I think you are overengineering this. Unless you have to manage a complex state pipeline with things like Redux and Saga, I don't feel the need to go beyond self-contained Components for most use cases.

    But, I will try to make this more Reacty while trying to maintain the MVP architecture.

    • Login.jsx
    import LoginView from "./LoginView";
    import React from "react";
    
    // This is probably your Model in `MVP`
    async function handleLoginACB(model, data) {
      const { email, password } = data;
    
      await model.signIn(email, password);
    
      return model.currentUser;
    }
    
    // This is probably your View in `MVP`
    function Login(props) {
      const [error, setError] = React.useState("");
    
      // This is probably your Presenter in `MVP`
      function onLoginFormData(data) {
        handleLoginACB(props.model, data).then((currentUser) => {
          if (currentUser != null) {
            // TODO: probably should be using a router library
            window.location.hash = "#HomeScreen";
          }
        }).catch(e => {
          console.log(e);
          setError(e.message);
        });
      }
    
      return <LoginView onUserLogIn={onLoginFormData} error={error} />;
    }
    
    export default Login;
    
    • LoginView.jsx
    import { useState } from "react";
    
    // Maybe this is your Presenter in `MVP`
    function errorHandling(error) {
      console.log(error.message);
      if (error.message.includes("password")) {
        return "Wrong password, try again";
      }
    
      if (error.message.includes("email")) {
        return "Wrong email, try again";
      }
    }
    
    // I think this is your View in `MVP`
    function LoginView(props) {
      const { onUserLogIn, error } = props;
    
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
    
      return (<div className="loginBox">
        <img src="logo.svg" className="image blob" />
        <form>
          <input type="email" placeholder="Email" id="email" onChange={(e) => setEmail(e.target.value)}></input>
          <div><input type="password" placeholder="Password" id="password"
                      onChange={(e) => setPassword(e.target.value)}></input></div>
          <div id="error">{errorHandling(error)}</div>
          <a href="#CreateAccount">Create a new account</a>
    
        </form>
        <button onClick={() => onUserLogIn({ email, password })}>Log in</button>
      </div>);
    }
    
    export default LoginView;
    

    Event Handler

    • Login.jsx
    import LoginView from "./LoginView";
    import React from "react";
    
    async function handleLoginACB(model, data) {
      const { email, password } = data;
    
      await model.signIn(email, password);
    
      return model.currentUser;
    }
    
    function Login(props) {
      const [error, setError] = React.useState("");
    
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
    
      function onFormData(data) {
        case (data.field) {
            'email': setEmail(data.value); break;
            'password': setPassword(data.value); break;
        }
      }
    
      function onSubmit() {
        let data = {email, password};
        handleLoginACB(props.model, data).then((currentUser) => {
          if (currentUser != null) {
            window.location.hash = "#HomeScreen";
          }
        }).catch(e => {
          console.log(e);
          setError(e.message);
        });
      }
    
      return <LoginView onFormData={onFormData} onSubmit={onSubmit} error={error} />;
    }
    
    export default Login;
    
    • LoginView.jsx
    import { useState } from "react";
    
    function errorHandling(error) {
      console.log(error.message);
      if (error.message.includes("password")) {
        return "Wrong password, try again";
      }
    
      if (error.message.includes("email")) {
        return "Wrong email, try again";
      }
    }
    
    function LoginView(props) {
      const { onUserLogIn, error } = props;
    
      return (<div className="loginBox">
        <img src="logo.svg" className="image blob" />
        <form>
          <input type="email" placeholder="Email" id="email" onChange={(e) => onFormData({field: 'email', value: e.target.value})}></input>
          <div><input type="password" placeholder="Password" id="password"
                      onChange={(e) => onFormData({field: 'password', value: e.target.value})}></input></div>
          <div id="error">{errorHandling(error)}</div>
          <a href="#CreateAccount">Create a new account</a>
    
        </form>
        <button onClick={onSubmit}>Log in</button>
      </div>);
    }
    
    export default LoginView;