How to separate component with shared logic - React Hooks

Hello!
I’m building a website with two pages (Workshops.js and Shows.js) fetching data (with different parameters) from the same API.
The problem is that most of the JSX code is the same.
I already managed to write a custom hook and extrapolate the logic…

Here’s my custom Hook


import { useState, useEffect } from 'react';

function useMyDataFetch(GET_URL) {
    const [ data, setData ] = useState([])
    const [ isLoading, setIsLoading ] = useState(true)
    const [ error, setError ] = useState(null)

    useEffect(() => {
        let hasBeenAborted = false;
        setIsLoading(true)
        fetch(GET_URL, {headers: {
            "Accept": "application/json",
            "Access-Control-Allow-Origin": "*"
        }})
        .then(res => {
            return (res.ok) ?  res.json() : new Error("Mistake!")
        })
        .then(data => {
            if (hasBeenAborted) return;
            if(data.upcoming) {
                setData(data.upcoming);
            }
            setIsLoading(false);
        })
        .catch(error => {
            if (hasBeenAborted) return;
            setIsLoading(false);
            setError(error)
        });
        return () => { hasBeenAborted = true; }
    }, [GET_URL]);

    return { data, error, isLoading };
}
export default useMyDataFetch;

Now the problem is that the JSX in the two pages is the same.
This is Workshops.js. The code in Shows.js is mostly the same.

import React from 'react';
import api from '../../maps/Api'
import useMyDataFetch from '../../DataFetch'
import { Link } from 'react-router-dom'

const Workshops = () => {
	const {isLoading, error, data: workshops} = useMyDataFetch(api.get.workshops); // In Shows.js I would call api.get.shows
	if ( error ){ return <p>{ error.message }</p> }
	if ( isLoading ){ return <p>Loading workshops...</p> }

	return(
		<main>
			<div className='content'>
				<div className='contentCol'>
                                       /* FROM HERE --------------- */
					<ul id='workshopBox'>
						{
							workshops.map( (workshop, i) => (
								<li key={i}>
									<div className='workshop-active'>
									<h2>{ workshop.title }</h2>
									<p>{ workshop.description }</p>
									<p>{ workshop.place }</p>
									<p>{ (new Date(workshop.date).toLocaleDateString("it-IT", {
										weekday: 'long',
										year: 'numeric',
										month: 'long',
										day: 'numeric'
										}))}</p>
									<p>{ (new Date(workshop.date).toLocaleTimeString("it-IT", {
										hour: '2-digit',
										minute: '2-digit',
										hour12: true
										}))}</p>
									<p> Full Price { workshop.price_full + ', 00'} &euro; </p>
									<p> Price early bird { workshop.price_earlybirds + ', 00'} &euro; </p>
									<p>
										<Link to={`/workshops/${ workshop.id}`}>
											<button>DETAILS</button>
										</Link>
									</p>
									<br/>
									</div>
								</li>
							))
						}
					</ul>

                                         /* TO HERE...THE CODE IS THE SAME*/
				</div>
			</div>
		</main>
		)
	}

export default Workshops

How can I move the JSX to another child component (like ), pass the data as props and use it in Workshops.js AND Shows.js?
Or is there a way I can use Context / Provider in both pages?

Thank you!

1 Like

Hi!

I’m not a react expert, but maybe You could use higher order components: https://reactjs.org/docs/higher-order-components.html.

possible solution, there’s absolutely no need for context here, you’re basically just changing between two strings (“workshop” and “shows”) (sorry, this was done quite quickly and I can probably simplify it further):

Some utility components, split out to make them easier to develop/test:

import React from "react"

export const Error = ({ children }) => <p className="error-message" >{ children }</p>;

export const Loading = () => <p className="loading-message">Loading...</p>;

export const Date = ({ dateTime }) => {
  const config = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  };
  return <p>{ (new Date(dateTime)).toLocaleDateString("it-IT", config) }</p>;
};

export const Time = ({ dateTime }) => {
  const config = {
    hour: '2-digit',
    minute: '2-digit',
    hour12: true
  };
  return <p>{ (new Date(dateTime)).toLocaleTimeString("it-IT", config) }</p>;
};
import React from "react";
import api from "../../maps/Api";
import useMyDataFetch from "../../DataFetch";
import { Link } from "react-router-dom";
import { Error, Loading, Date, Time } from "./Utilities";

const ListingLoader = ({type}) => {
  const {isLoading, error, data} = useMyDataFetch(api.get[type]);
  
  if (error) return <Error>{error.message}</Error>
  if (loading) return <Loading />

  return <ul>{ data.map(datum => <Listing data={datum} key={datum.id} typeSingular={type} typePlural={`${type}s` } />) }</ul>;
}

const Listing = ({ data, typeSingular, typePlural }) => {
  const {id, title, description, place, date, price_full, price_earlybird } = data;

  return(
    <li className={ `${typeSingular}-active` }>
      <h2>{title}</h2>
      <p>{description}</p>
      <p>{place}</p>
      <Date dateTime={date} />
      <Time dateTime={time} />
      <p>Full Price €{price_full + ',00'}</p>
      <p>Earlybird Price €{price_earlybird + ',00'}</p>
      <p>
        <Link to={`/${typePlural}/${id}`}>
          <button>DETAILS</button>
        </Link>
      </p>
    </li>
  );
};

const WhateverTheOuterStuffIsCalled = ({ type }) => (
  <main>
    <div className='content'>
      <div className='contentCol'>
        <ListingLoader type={type} />
      </div>
    </div>
  </main>
);

export default WhateverTheOuterStuffIsCalled;