How can I convert a Class Component which extends another Class component in a Functional Component in ReactJS?

How can I convert a Class Component which extends another Class component in a Functional Component in ReactJS?

input.jsx [Functional Component]

const Input = ({ name, label, error, ...rest }) => {
    return (
        <div className="mb-3">
            <label htmlFor={name} className="form-label">
                {label}
            </label>
            <input
                autoFocus
                {...rest}
                id={name}
                name={name}
                className="form-control"
            />
            {error && <div className="alert alert-danger">{error}</div>}
        </div>
    )
}

export default Input

form.jsx [Class Component]

import React, { Component } from "react"
import Input from "./input"
import Joi from "joi"

class Form extends Component {
    state = {
        data: {},
        errors: {}
    }

    validate = () => {
        const options = { abortEarly: false }
        const schemaJoi = Joi.object(this.schema)
        const { error } = schemaJoi.validate(this.state.data, options)
        if (!error) return null

        const errors = {}
        error.details.map(item => (errors[item.path[0]] = item.message))
        return errors
    }

    validateProperty = ({ name, value }) => {
        const obj = { [name]: value }
        const schema = {
            [name]: this.schema[name]
        }
        const schemaJoi = Joi.object(schema)
        const { error } = schemaJoi.validate(obj)
        return error ? error.details[0].message : null
    }

    handleSubmit = e => {
        e.preventDefault()

        const errors = this.validate()
        console.log(errors)
        this.setState({ errors: errors || {} })
        if (errors) return

        this.doSubmit()
    }

    handleChange = ({ currentTarget: input }) => {
        const errors = { ...this.state.errors }
        const errorMessage = this.validateProperty(input)
        if (errorMessage) errors[input.name] = errorMessage
        else delete errors[input.name]

        const data = { ...this.state.data }
        data[input.name] = input.value
        this.setState({ data, errors })
    }

    renderButton = label => {
        return (
            <button disabled={this.validate()} className="btn btn-primary">
                {label}
            </button>
        )
    }

    renderInput = (name, label, type = "text") => {
        const { data, errors } = this.state
        return (
            <Input
                name={name}
                label={label}
                error={errors[name]}
                type={type}
                value={data[name]}
                onChange={this.handleChange}
            />
        )
    }
}

export default Form

loginForm.jsx [Class Component which extends the other]

import Joi from "joi"
import Form from "./common/form"

class LoginForm extends Form {
    state = {
        data: { username: "", password: "" },
        errors: {}
    }

    schema = {
        username: Joi.string().required().label("Username"),
        password: Joi.string().required().label("Password")
    }

    doSubmit = () => {
        console.log("Submitted")
    }

    render() {
        return (
            <div>
                <h1>Login</h1>
                <form onSubmit={this.handleSubmit}>
                    {this.renderInput("username", "Username")}
                    {this.renderInput("password", "Password", "password")}
                    {this.renderButton("Login")}
                </form>
            </div>
        )
    }
}

export default LoginForm

I already know how to convert a simple Class Component to a Stateless Functional Component but what I don’t know is how to convert a Class Component which extends another Class Component.

Please, may you explain me how?

1 Like

Hi,

Thanks for your reply but, as I wrote, I already know how to convert a Class Component to a Functional one. What I don’t know is how to convert a Class Component which extends another Class Component.

I don’t know if there are other ways around, but using HOOKS should be the most common way to solve this issue.

Check this link out , I believe it can help a lot :

Thanks but I think this don’t solve the issue. Mosh, where are you? Could you answer?

1 Like

Hi.

I am actually wondering about the same thing.

The form as presented in the React course is awesome. It provides a Form component with all the common logic and ways for derived components to be customised. This makes the code clean and easy to maintain.

In the few months after I finished the course I learnt hooks and functional components is my new way to go.

The counterpart to inheritance is composition. So I thought I’d read about it but I am not convinced yet it would be a good solution.

I just saw a video on YT about the useForm hook. It is very nice. Validation logic is inline which differs from what we learnt with Mosh course.
You can’t benefit from the renderInput() functionality and alike though. So you still need to write the code entirely in every form.

I would really enjoy to know how we could get the same level of functionality / ease of {use | maintenance}.

Best regards.

1 Like

I just started to play with HOC.

import React from 'react'

const withForm = (Component) => {

    const renderInput = (name, value, label, placeholder) =>
        <>
            <label htmlFor={`${name}_id`}>{label}</label>
            <input id={`${name}_id`} name={name} placeholder={placeholder} defaultValue={value} /><br />
        </>

    const formMethods = { renderInput };
    return (props) => <Component formMethods={formMethods} {...props} />
}

export default withForm;

On an actual form to be made.

import React from 'react';
import withForm from '../../HOC/withForm';

const PrimoForm = (props) => {
    const { renderInput } = props.formMethods;
    return (
        <div>
            <h1>PrimoForm</h1>
            {renderInput('firstname', 2.3, 'First Name', '')}
            {renderInput('lastname', 'Reactson', 'Last name')}
            {renderInput('email', 2, "Email but I chose to send number")}
        </div>
    )
}

export default withForm(PrimoForm);

image

The thing is we must pass the methods via props and of course there would be more than in this sample so I’d rather pass them in a global object instead of millions of separate props.

I did not use useForm hook but the concept of providing a set of methods looks promising.

So the <Form /> component shall be converted to HOC with all the data validation logic and then actual forms should be wrapped in that HOC.

Hello, have you found the solution yet?
i really like mosh’s code implementation but don’t know how to convert to react 18. Please help me

great stuff, thanks for sharing

I don’t know if this is correct but I am a beginner, sorry . I am also struggling to convert it to functional component with reusable functions just like on the course but I came up with this. Dont know if acceptable but it seems working on my end. Anyone could help me? Thank you.

Im on 18- Extracting a Reusable Form lesson

LoginForm.jsx

import Joi from "joi-browser";
import React, { useState } from "react";
import * as formHelpers from "../utils/form";
import Input from "./common/Input";
function LoginForm() {
  const [data, setData] = useState({ username: "", password: "" });
  const [errors, setErrors] = useState({});

  const schema = {
    username: Joi.string().required().label("Username"),
    password: Joi.string().required().label("Password")
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    const errors = formHelpers.validate(data, schema);

    setErrors(errors || {});
    if (errors) return;
    console.log("Submitted");
  };

  return (
    <div className="row justify-content-center">
      <div className="col-5">
        <div className="login">
          <h1>Log In</h1>
          <div className="body">
            <form onSubmit={handleSubmit}>
              <Input
                onChange={(e) =>
                  formHelpers.handleChange(
                    data,
                    errors,
                    setData,
                    setErrors,
                    schema,
                    e
                  )
                }
                focus={true}
                label="Username"
                value={data.username}
                name="username"
                type="email"
                error={errors.username}
              />

              <Input
                onChange={(e) =>
                  formHelpers.handleChange(
                    data,
                    errors,
                    setData,
                    setErrors,
                    schema,
                    e
                  )
                }
                focus={false}
                label="Password"
                value={data.password}
                name="password"
                type="password"
                error={errors.password}
              />

              <div className="mb-3 form-check">
                <input
                  type="checkbox"
                  className="form-check-input"
                  id="exampleCheck1"
                />
                <label className="form-check-label" htmlFor="exampleCheck1">
                  Check me out
                </label>
              </div>
              <button
                disabled={formHelpers.validate(data, schema)}
                className="btn btn-primary">
                Submit
              </button>
            </form>
          </div>
        </div>
      </div>
    </div>
  );
}

export default LoginForm;

form.js

var Joi = require("joi-browser");

export const validate = (data, schema) => {
  const options = {
    abortEarly: false
  };
  // can be destructure with const { error } and remove "result" word from IF statement and "For Loop";
  const result = Joi.validate(data, schema, options);

  if (!result.error) return null;
  const formErrors = {};
  for (let item of result.error.details) {
    formErrors[item.path[0]] = item.message;
  }
  return formErrors;
};

export const validateProperty = (schema, { name, value }) => {
  const obj = { [name]: value };
  const subSchema = { [name]: schema[name] };
  const { error } = Joi.validate(obj, subSchema);
  return error ? error.details[0].message : null;
};

export const handleChange = (
  data,
  errors,
  setData,
  setErrors,
  schema,
  { currentTarget: input }
) => {
  const cloneErrors = { ...errors };
  const errorMessage = validateProperty(schema, input);
  if (errorMessage) cloneErrors[input.name] = errorMessage;
  else delete cloneErrors[input.name];

  const updatedData = { ...data };
  updatedData[input.name] = input.value;
  setData(updatedData);
  setErrors(cloneErrors);
};

hi, here is some of the piece my code convert extends concept from class to functional component, hope its may help. and if there is the best approach to optimize this, please let me know

demo vidly using functional component

useForm.jsx

import Input from "./input";
import Select from "./select";
import Joi from "joi";

function useForm(props) {
  const { schema, onSubmit, data, setData, error, setError } = props;

  const validation = () => {
    const options = { abortEarly: false };
    const { error } = schema?.validate(data, options);
    if (!error) return null;

    const errors = {};
    for (let item of error?.details) {
      errors[item?.path[0]] = item?.message;
    }
    return errors;
  };

  const validateProperty = ({ name, value }) => {
    const rule = schema?.extract(name);
    const propertySchema = Joi.object({
      [name]: rule
    });
    const valueObject = { [name]: value };
    const { error } = propertySchema?.validate(valueObject);
    return error ? error?.details[0]?.message : null;
  };

  const changeHandler = ({ currentTarget: input }) => {
    const errorMessage = validateProperty(input);
    const errors = { ...error };
    errorMessage
      ? (errors[input.name] = errorMessage)
      : delete errors[input.name];
    setError(errors);
    setData({ ...data, [input.name]: input.value });
  };

  const submitHandler = (e) => {
    e.preventDefault();
    setError(validation() || {});
    if (Object.keys(error).length) return;
    console.log(data);
    onSubmit();
  };

  const renderButton = (label) => {
    return (
      <button
        disabled={validation() === null ? false : true}
        className="btn btn-primary mt-4 w-100"
        type="submit"
      >
        {label}
      </button>
    );
  };

  const renderInput = ({ label, name, type = "text", focused = false }) => {
    return (
      <Input
        name={name}
        label={label}
        type={type}
        autoFocus={focused}
        value={data[name]}
        error={error[name]}
        onChange={changeHandler}
      />
    );
  };

  const renderSelect = ({ label, name, options }) => {
    return (
      <Select
        name={name}
        value={data[name]}
        label={label}
        options={options}
        onChange={changeHandler}
        error={error[name]}
      />
    );
  };

  return {
    submitHandler,
    renderButton,
    renderInput,
    renderSelect
  };
}

export default useForm;

loginForm.jsx

import useForm from "../components/form";
import { useState } from "react";
import Joi from "joi";

function Login() {
  const [data, setData] = useState({
    username: "",
    password: ""
  });
  const [error, setError] = useState({});

  const rule = {
    schema: Joi.object({
      username: Joi.string().required().label("Username"),
      password: Joi.string().required().label("Password")
    }),
    onSubmit: () => {
      console.log("submitted");
    },
    data,
    setData,
    error,
    setError
  };

  const { renderInput, renderButton, submitHandler } = useForm(rule);

  return (
    <div className="w-100 vh-100 d-flex justify-content-center align-items-center">
      <form className="w-25" onSubmit={submitHandler}>
        <h3 className="fw-bold text-center">Login</h3>
        {renderInput({ label: "Username", name: "username", focused: true })}
        {renderInput({ label: "Password", name: "password", type: "password" })}
        {renderButton("Login")}
      </form>
    </div>
  );
}

export default Login;

1 Like