React Fruits App Complete Solution

Understanding the Problem

This assessment involves creating a React application with various components for managing information about fruits. The app requires:

  1. Setting up routes in App.jsx
  2. Creating a Navigation component with NavLinks
  3. Creating a FruitsIndex component that lists fruits
  4. Creating a FruitShow component that displays details about a specific fruit
  5. Implementing a FruitForm component with validation and form submission
  6. Creating a FavoriteFruit component that links to the user's favorite fruit
  7. Creating a SetFavoriteFruit component that allows changing the favorite fruit

Devising a Plan

  1. Create App.jsx with defined routes
  2. Implement Navigation.jsx with NavLink components
  3. Implement FruitsIndex.jsx to display a list of fruits with links
  4. Implement FruitShow.jsx to display specific fruit details
  5. Enhance FruitForm.jsx with form controls and validation
  6. Implement FavoriteFruit.jsx component using context
  7. Enhance SetFavoriteFruit.jsx component using context

App.jsx


import { Routes, Route } from "react-router-dom";
import Navigation from "./components/Navigation";
import FruitsIndex from "./components/FruitsIndex";
import FruitShow from "./components/FruitShow";
import FruitForm from "./components/FruitForm";
import FavoriteFruit from "./components/FavoriteFruit";
import SetFavoriteFruit from "./components/SetFavoriteFruit";
import fruits from "./mockData/fruits.json";

function App() {
  return (
    <>
      <Navigation />
      <Routes>
        <Route path="/" element={<FruitsIndex fruits={fruits} />} />
        <Route path="/fruits/new" element={<FruitForm fruits={fruits} />} />
        <Route path="/fruits/:fruitId" element={<FruitShow fruits={fruits} />} />
        <Route path="/fav-fruit" element={<FavoriteFruit fruits={fruits} />} />
        <Route path="/set-fav-fruit" element={<SetFavoriteFruit fruits={fruits} />} />
        <Route path="*" element={<h2>Page Not Found</h2>} />
      </Routes>
    </>
  );
}

export default App;
            

Key points:

Navigation.jsx


import { NavLink } from "react-router-dom";

function Navigation() {
  return (
    <nav>
      <NavLink to="/">Home</NavLink>
      <NavLink to="/fruits/new">Enter a Fruit</NavLink>
      <NavLink to="/fav-fruit">Favorite Fruit</NavLink>
      <NavLink to="/set-fav-fruit">Set Favorite Fruit</NavLink>
    </nav>
  );
}

export default Navigation;
            

Key points:

FruitsIndex.jsx


import { Link } from "react-router-dom";

function FruitsIndex({ fruits }) {
  return (
    <div>
      <h2>Fruits Index</h2>
      {fruits.map(fruit => (
        <div key={fruit.id}>
          <Link to={`/fruits/${fruit.id}`}>{fruit.name}</Link>
        </div>
      ))}
    </div>
  );
}

export default FruitsIndex;
            

Key points:

FruitShow.jsx


import { useParams } from "react-router-dom";

function FruitShow({ fruits }) {
  const { fruitId } = useParams();
  const fruit = fruits.find(fruit => fruit.id === fruitId);

  return (
    <div>
      <h2>{fruit.name}</h2>
      <p>Color: {fruit.color}</p>
      <p>Sweetness: {fruit.sweetness}</p>
      <p>Seeds: {fruit.seeds}</p>
    </div>
  );
}

export default FruitShow;
            

Key points:

FruitForm.jsx


import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";

const COLORS = [
  "red",
  "orange",
  "yellow",
  "green",
  "blue",
  "purple"
];

function FruitForm({ fruits }) {
  const navigate = useNavigate();
  
  // State for form fields
  const [name, setName] = useState("");
  const [sweetness, setSweetness] = useState(1);
  const [color, setColor] = useState("orange");
  const [seeds, setSeeds] = useState("yes");
  
  // State for validation errors
  const [errors, setErrors] = useState({
    name: "",
    sweetness: ""
  });

  // Validation function
  useEffect(() => {
    const validateForm = () => {
      const newErrors = {
        name: "",
        sweetness: ""
      };
      
      // Name validation
      if (name.length < 3) {
        newErrors.name = "Name must be 3 or more characters";
      } else if (name.length > 20) {
        newErrors.name = "Name must be 20 characters or less";
      } else if (fruits.some(fruit => fruit.name.toLowerCase() === name.toLowerCase())) {
        newErrors.name = "Name already exists";
      }
      
      // Sweetness validation
      if (sweetness < 1 || sweetness > 10) {
        newErrors.sweetness = "Sweetness must be between 1 and 10";
      }
      
      setErrors(newErrors);
    };
    
    validateForm();
  }, [name, sweetness, fruits]);

  // Check if form is valid
  const isFormValid = !errors.name && !errors.sweetness;

  // Handle form submission
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // Create fruit object
    const fruitData = {
      name,
      sweetness,
      color,
      seeds
    };
    
    // Log the fruit data
    console.log(fruitData);
    
    // Navigate to home page
    navigate("/");
  };

  return (
    <form
      className="fruit-form"
      onSubmit={handleSubmit}
    >
      <h2>Enter a Fruit</h2>
      
      <label>
        Name
        <input
          type="text"
          name="name"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </label>
      <p className="errors">{errors.name}</p>
      
      <label>
        Select a Color
        <select
          value={color}
          onChange={(e) => setColor(e.target.value)}
        >
          {COLORS.map(color => (
            <option
              key={color}
              value={color}
            >
              {color}
            </option>
          ))}
        </select>
      </label>
      
      <label>
        Sweetness
        <input
          type="number"
          name="sweetness"
          value={sweetness}
          onChange={(e) => setSweetness(e.target.value)}
        />
      </label>
      <p className="errors">{errors.sweetness}</p>
      
      <label>
        <input
          type="radio"
          value="no"
          name="seeds"
          checked={seeds === "no"}
          onChange={() => setSeeds("no")}
        />
        No Seeds
      </label>
      
      <label>
        <input
          type="radio"
          value="yes"
          name="seeds"
          checked={seeds === "yes"}
          onChange={() => setSeeds("yes")}
        />
        Seeds
      </label>
      
      <button
        type="submit"
        disabled={!isFormValid}
      >
        Submit Fruit
      </button>
    </form>
  );
}

export default FruitForm;
            

Key points:

FavoriteFruit.jsx


import { useContext } from "react";
import { Link } from "react-router-dom";
import { FavFruitContext } from "../context/FavFruitContext";

const FavoriteFruit = ({ fruits }) => {
  const { favFruitId } = useContext(FavFruitContext);
  const favoriteFruit = fruits.find(fruit => fruit.id === favFruitId);
  
  return (
    <div>
      <h2>Favorite Fruit</h2>
      <Link to={`/fruits/${favFruitId}`}>{favoriteFruit.name}</Link>
    </div>
  );
}

export default FavoriteFruit;
            

Key points:

SetFavoriteFruit.jsx


import { useContext } from "react";
import { FavFruitContext } from "../context/FavFruitContext";

const SetFavoriteFruit = ({ fruits }) => {
  const { favFruitId, setFavFruitId } = useContext(FavFruitContext);
  
  const handleChange = (e) => {
    setFavFruitId(e.target.value);
  };
  
  return (
    <div className="set-fav-fruit">
      <h2>Select your Favorite Fruit</h2>
      <label>
        <select
          value={favFruitId}
          onChange={handleChange}
        >
          {fruits.map(fruit => (
            <option
              key={fruit.id}
              value={fruit.id}
            >
              {fruit.name}
            </option>
          ))}
        </select>
      </label>
    </div>
  );
}

export default SetFavoriteFruit;
            

Key points:

FavFruitContext.jsx

This file is already provided but included here for completeness:


import { createContext, useState } from 'react';

export const FavFruitContext = createContext();

export default function FavFruitProvider(props) {
  const [favFruitId, setFavFruitId] = useState('1');

  return (
    <FavFruitContext.Provider
      value={{
        favFruitId,
        setFavFruitId
      }}
    >
      {props.children}
    </FavFruitContext.Provider>
  )
}
            

Key points:

Key React Concepts

Routes and Navigation

React Router provides client-side routing capabilities:

Forms and Validation

React forms typically use controlled components:

Context API

React's Context API allows sharing state across components:

Common Gotchas

Route Order Matters

In React Router, routes are evaluated in order. More specific routes like "/fruits/new" must come before wildcard routes like "/fruits/:fruitId".

Exact Text Content

Tests often check for specific text content. For example, the heading must be exactly "Favorite Fruit" not "My Favorite Fruit".

Form Validation Timing

Validation must run on initial render and after every input change, not just during form submission.

Default Values

Pay attention to the required default values for inputs (color="orange", seeds="yes", etc).

Testing

To test the application:

  1. Run npm install to install dependencies
  2. Run npm test to run all tests
  3. Run npm test 1-Routes to test specific components
  4. Run npm run dev to view the app in a browser

The tests check for: