Is there a way I can turn this hardcoded jsx code into its own individual component?

Viewed 27

I have a blog website that I'm working on in React. I have a form that allows users to make a blog post, and part of that form is a little slider we'll call a "fireLevelSlider" that allows users to select a fire level 1-5 based on how excited they are about the post. I have some functions that generate dynamic classNames to change the icons color/size when users hover/click on them. I am very new to react and I feel like the way I built this out is not efficient or within best practices, and I'm just curious how I should reformat this code, if at all.

It works now, but I just feel like the way I hardcoded the 5 elements into the blogPostForm just doesn't feel right. I was thinking maybe I should generate an array of img objects above the blogPostForm component function, and then call that array in the jsx? or Maybe I should be building my slider as its own component and importing it into the form? The only problem I have with that second option is that I'm setting state with the slider, and that state is held by the blogPostForm.

Here is my react code

import React from 'react'
import fireIcon from '../images/fireIcon.png'

export default function BlogPostForm () {
    const [formState, setFormState] = React.useState({ flaire: '', title: '', text: ''});
    const [isHovered, setIsHovered] = React.useState();
    const [isLit, setIsLit] = React.useState();

    function changeFlaire(event) {
        const selectedFlaire = event.target.value;
        setFormState( {...formState, flaire: selectedFlaire });
    }

    function changeTitle(event) {
        const title = event.target.value;
        setFormState( {...formState, title: title });
    }

    function changeText(event) {
        const text = event.target.value;
        setFormState( {...formState, text: text });
    }

    function handleMouseOver(e) {
        setIsHovered(e.target.id);
    }

    function handleMouseLeave(e) {
        setIsHovered();
    }

    function handleFireIconClick(e) {
        setIsLit(e.target.id);
    }

    function handleFireIconClass(fireLevel) {
        const classNames = ['fireIcon']
            if (isHovered >= fireLevel) {
                classNames.push('isHeld')
            }
            if (isLit >= fireLevel) {
                classNames.push('isLit')
            }
            console.log(classNames)
        return classNames.join(' ');
        
    }
    
    return (
        <form className="postForm">
            <h1 className="postFormHeader">Create a blog post!</h1>        
                <div className="fireIconContainer">
                    <img onClick={handleFireIconClick} onMouseLeave={handleMouseLeave} onMouseOver={handleMouseOver} className={handleFireIconClass(1)} src={fireIcon} alt="fireIcon" id="1"></img>
                    <img onClick={handleFireIconClick} onMouseLeave={handleMouseLeave} onMouseOver={handleMouseOver} className={handleFireIconClass(2)} src={fireIcon} alt="fireIcon" id="2"></img>
                    <img onClick={handleFireIconClick} onMouseLeave={handleMouseLeave} onMouseOver={handleMouseOver} className={handleFireIconClass(3)} src={fireIcon} alt="fireIcon" id="3"></img>
                    <img onClick={handleFireIconClick} onMouseLeave={handleMouseLeave} onMouseOver={handleMouseOver} className={handleFireIconClass(4)} src={fireIcon} alt="fireIcon" id="4"></img>
                    <img onClick={handleFireIconClick} onMouseLeave={handleMouseLeave} onMouseOver={handleMouseOver} className={handleFireIconClass(5)} src={fireIcon} alt="fireIcon" id="5"></img>
                </div>  
                <select
                    required
                    className="flaireSelect" 
                    value={formState.flaire}
                    onChange={changeFlaire}>
                        <option disabled={true} value="">Choose a flaire</option>
                        <option value="JavaScript">JavaScript</option>
                        <option value="CSS">CSS</option>
                        <option value="HTML">HTML</option>
                        <option value="REACT">REACT</option>
                        <option value="BACKEND">BACKEND</option>
                </select>
                <input
                    value={formState.title}
                    onChange={changeTitle}
                    className="titleBox"
                    placeholder="title"
                    type="text"
                    id="title"
                    name="title"
                />
                <textarea
                    value={formState.text}
                    onChange={changeText}
                    className="textBox"
                    placeholder="text"
                    type="text"
                    id="blogPost"
                    name="blogPost"
                />
                <div className="blogPostFormButtonContainer">
                    <button className="blogPostSubmit" type="submit">SUBMIT</button>
                    <button className="blogPostCancel" type="submit">CANCEL</button>
                </div>
        </form>
    )
}

Here is my Css code

/*blogPostForm */
.postForm {
  font-family: "Montserrat", sans-serif;
  color: white;
  background-color: var(--color2);
  border-radius: 10px;
  margin-top: 10px;
  width: 80%;
  max-width: 750px;

  display: grid;
  align-items: center;
  justify-content: center;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  grid-template-areas:
    "flaire"
    "title"
    "post"
    "buttons";
  grid-gap: 5px;
}

.postFormHeader {
  justify-self: center;
}

.fireIconContainer {
  align-self: center;
}

.fireIcon {
  color: white;
  background-color: white;
  width: 40px;
  height: 40px;
  transition: all 0.2s ease-in-out;
}

.isLit {
  background-color: red;
}

.isHeld {
  transform: scale(1.1);
}

.flaireSelect {
  font-family: "Montserrat", sans-serif;
  justify-self: center;
  width: 150px;
  height: 35px;
  text-align: center;
}

select:invalid {
  color: grey;
}

.titleBox {
  font-family: "Montserrat", sans-serif;
  height: 40px;
  margin: 10px;
}

.textBox {
  font-family: "Montserrat", sans-serif;
  height: 100px;
  margin: 10px;
  resize: vertical;
}

.blogPostFormButtonContainer {
  font-family: "Montserrat", sans-serif;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  height: 50px;
  margin-bottom: 10px;
}

.blogPostSubmit {
  font-family: "Montserrat", sans-serif;
  width: 35%;
  margin: 5px;
  height: 35px;
}

.blogPostCancel {
  font-family: "Montserrat", sans-serif;
  width: 35%;
  margin: 5px;
  height: 35px;
}
1 Answers

This is a good starting point! Now that you've got it working, refactoring it into smaller components is definitely the right "next step". React tends to work best if you build your app by creating lots of little components that each only do a little bit of work, and then you compose those components into a larger app.

I'd start by creating a FireIcon component (note: this is in TypeScript which I'd also encourage you to switch to!):

interface FireIconProps {
  id: number;
  lit: boolean;
  hovered: boolean;
  onHoverChange: (id: number, isHovered: boolean) => void;
  onClick: (id: number) => void;
}

const getFireIconClassNames = (isHovered: boolean, isLit: boolean) => {
  const classNames = ["fireIcon"];

  // ----- 8< -----

  return classNames.join(" ");
};

export const FireIcon = ({
  id,
  lit,
  hovered,
  onHoverChange,
  onClick
}: FireIconProps) => {
  const handleMouseOver = () => onHoverChange(id, true);
  const handleMouseLeave = () => onHoverChange(id, false);
  const handleClick = () => onClick(id);

  return (
    <div
      onClick={handleClick}
      onMouseOver={handleMouseOver}
      onMouseLeave={handleMouseLeave}
      className={getFireIconClassNames(hovered, lit)}
    >
      {id}
    </div>
  );
};

A few things to note -

  • This component doesn't keep its own state, it relies on the state being passed to it from a parent component.
  • There's no need to have the logic for calculating the class names inside the component - the logic is deterministic, and moving the logic out of the component means it doesn't get redefined on every render.
  • I also changed the image to a div (just for the purposes of this example) but obviously you can change this to an image :-).

Now as you said, the slider would be another good component, but the "trap" is that you're holding state in the parent form. Well, actually, the only state the parent form probably cares about is which flame is "lit", because you probably want to save that when you save the form post. But as for which one the user is hovering over - well, the parent form doesn't care about that! So you could create your slider component with a mix of passed props, as well as its own internal state:

import { useState } from 'react';
import { FireIcon } from './FireIcon';

const ICON_COUNT = 5;
const FIRE_ICONS_IDS = [...new Array(ICON_COUNT)].map((_, index) => index + 1);

interface Props {
  litId: number;
  onLitChange: (litId: number) => void;
}

export const Slider = ({ litId, onLitChange }: Props) => {
  const [hoverId, setHoverId] = useState(0);

  const handleHoverChange = (id: number, isHovered: boolean) => 
    setHoverId(() => (isHovered ? id : 0));

  return (
    <div className="fireIconContainer">
    {FIRE_ICONS_IDS.map((id) => (
      <FireIcon
        key={id}
        id={id}
        lit={id <= litId}
        hovered={id <= hoverId}
        onHoverChange={handleHoverChange}
        onClick={onLitChange}
      />
    ))}
  </div>
);
}

Note also that the icons are now generated from an array of 'IDs' (as per your suggestion) - so if you want to rate out of 10, you'd just have to update the ICON_COUNT variable.

Now your BlogPostForm becomes a bit simpler:

import React, { useState } from "react";
import { Slider } from './Slider';

export const BlogPostForm = () => {
  const [litId, setLitId] = useState(0);

  // ----- 8< -----

  return (
    <form className="postForm">
      <h1 className="postFormHeader">Create a blog post!</h1>

      <Slider litId={litId} onLitChange={setLitId} />

      {/* ----- 8< ----- */}
    </form>
  );
};

Ideally, you should keep refactoring "BlogPostForm" so that it's only responsibility is maintaining the state for the blog post, and then passing that state down to the child components (as well as receiving state updates from those child components).

Here's a working example that you can check out.

Related