React Components: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
Line 1,431: Line 1,431:
   );
   );
};
};
export default Speakers;
</syntaxhighlight>
==Implementing into Speaker Component==
We now implement the use of the context into the Speaker context built with the HOC. The context been renamed to DataContext and DataProvider but the rendering code is much the same. The main change is to the export which wraps the component in a context ensuring that any child props are not lost
<syntaxhighlight lang="js">
import React, { useState, useContext } from 'react';
import { DataContext, DataProvider } from '../../contexts/DataContext';
import SpeakerSearchBar from '../SpeakerSearchBar/SpeakerSearchBar';
import Speaker from '../Speaker/Speaker';
import { REQUEST_STATUS } from '../../reducers/request';
import withRequest from '../HOCs/withRequest';
import withSpecialMessage from '../HOCs/withSpecialMessage';
import { compose } from 'recompose';
const SpeakersComponent = ({ bgColor }) => {
  const specialMessage = '';
  const { speakers, status } = useContext(DataContext);
  const onFavoriteToggleHandler = async (speakerRec) => {
    put({
      ...speakerRec,
      isFavorite: !speakerRec.isFavorite,
    });
  };
  const [searchQuery, setSearchQuery] = useState('');
  const success = status === REQUEST_STATUS.SUCCESS;
  const isLoading = status === REQUEST_STATUS.LOADING;
  const hasErrored = status === REQUEST_STATUS.ERROR;
// Rendering code unchanged
  return (
    <div className={bgColor}>
...
    </div>
  );
};
const Speakers = (props) => {
  return (
    <DataProvider>
      <SpeakersComponent {...props}></SpeakersComponent>
    </DataProvider>
  );
};
export default Speakers;
export default Speakers;
</syntaxhighlight>
</syntaxhighlight>

Revision as of 01:17, 5 December 2020

Designing Components

Resources

These can found here https://github.com/pkellner/pluralsight-designing-react-components-course-code Implements

  • Component Reuse
  • Single Responsibility
  • Dont Repeat Yourself

Next JS Setup

Create project with

npm install react react-dom next --save

Add three commands to packages.json

"dev": "next",
"build": "next build",
"start": "next start"

Basic Page

In the demo the tutor builds an app which looks like this
With next js we can create a basic page using images to represent components

function Page() {
    return (
        <div>
            <img src="images/header.png" />
            <img src="images/menu.gif" />
            <img src="images/searchbar.gif" />
            <img src="images/speakers.png" />
            <img src="images/footer.png" />
        </div>
    )
}
export default Page

Replacing with Components

To start making our components we can replace the images with components. For example

import React from 'react'
const Header = () => <img src="images/header.png" />
export default Header

And replace the Page with the components i.e.

function Page() {
    return (
        <div>
            <Header />
            <Menu />
            <SpeakerSearchBar />
            <Speakers />
            <Footer />
        </div>
    )
}
export default Page

Breaking Down Futher

We can now break down further the speaker image into individual speakers.

const Speakers = () => {
  const speakers = [
    { image: "images/speaker-component-1124.png", name: "a" },
    { image: "images/speaker-component-1530.png", name: "b" },
    { image: "images/speaker-component-10803.png", name: "c" },
  ];
  return (
    <div>
      {speakers.map((x) => {
        return <img src={x.image} alt={x.name} key={x.image} />;
      })}
    </div>
  );
};

export default Speakers;

Component Abstractions

In programming three patterns used to abstract

  • HOC - Higher Order Component

A higher-order component is a function that takes a component and returns a new component

  • RP - Render Prop

A RP is simply a prop that takes a function which returns elements that will be used in render(). You can pass an element directly into a prop and use it in render() which would make the whole thing a RP by name, but generally, when people speak about RPs, they mean the first definition.

  • Context

Context in React is used to share data that is global to a component tree such as an authenticated user or preferred theme without passing that data as props.

HOC

Simple Example of HOC

As specified above a HOS is a function that takes a component and returns a new component

const EnhancedSpeakerComponent = withData(Speakers)
function withData(Component) {
   return function() {
      return <Component />
   }
}
export default EnhancedSpeakerComponent

Move the Data to HOC

Now lets move our array into the HOC to have

const EnhancedSpeakerComponent = withData(Speakers)
function withData(Component) {
    const speakers = [
        { image: "images/speaker-component-1124.png", name: "a" },
        { image: "images/speaker-component-1530.png", name: "b" },
        { image: "images/speaker-component-10803.png", name: "c" },
      ]
   return function() {
      return <Component speakers={speakers} />
   }
}
export default EnhancedSpeakerComponent


And change the component to have speakers as a prop

const Speakers = ({speakers}) => {
  return (
    <div>
      {speakers.map((x) => {
        return <img src={x.image} alt={x.name} key={x.image} />;
      })}
    </div>
  );
};


We can now put the withData in a separate function and import into the Speakers component so we now have.

import React from 'react'
import withData from './withData'

const Speakers = ({speakers}) => {
  return (
    <div>
      {speakers.map((x) => {
        return <img src={x.image} alt={x.name} key={x.image} />;
      })}
    </div>
  );
};
export default withData(Speakers)

Adding Parameter to Our HOC

Lets say we want to limit the number of images. We can do this but passing a value to the function like below

const maxSpeakersToShow = 2
export default withData(maxSpeakersToShow)(Speakers)

This is very similar to how we implement decorators in python, java or other languages because to implement this in the function we simply wrap the function and return it

function withData(maxSpeakersToShow) {
   return function(Component) {
...
   }
}


Here is the full example.

import React from "react";

function withData(maxSpeakersToShow) {
  return function(Component) {
    const speakers = [
      { image: "images/speaker-component-1124.png", name: "a" },
      { image: "images/speaker-component-1530.png", name: "b" },
      { image: "images/speaker-component-10803.png", name: "c" },
    ];

    return function () {
      const limitSpeakers = speakers.slice(0, maxSpeakersToShow)
      return <Component speakers={limitSpeakers} />;
    };
  }
}
export default withData;


Let use the more modern format with lambdas.

const withData = (maxSpeakersToShow) => (Component) => {
  const speakers = [
    { image: "images/speaker-component-1124.png", name: "a" },
    { image: "images/speaker-component-1530.png", name: "b" },
    { image: "images/speaker-component-10803.png", name: "c" },
  ]

  return () => {
    const limitSpeakers = speakers.slice(0, maxSpeakersToShow);
    return <Component speakers={limitSpeakers} />;
  }
}

Render Props

Boilerplate Code

Originally we had

const Speakers = () => {
  const speakers = [
    { image: "images/speaker-component-1124.png", name: "a" },
    { image: "images/speaker-component-1530.png", name: "b" },
    { image: "images/speaker-component-10803.png", name: "c" },
  ];
  return (
    <div>
      {speakers.map((x) => {
        return <img src={x.image} alt={x.name} key={x.image} />;
      })}
    </div>
  );
};


With Render Prop we can abstract the rendering with a function. A react function takes the argument of props and returns the child of the props

function speakersRenderProps(props) {
   return props.children()
}
const Speakers = () => {
  const speakers = [
    { image: "images/speaker-component-1124.png", name: "a" },
    { image: "images/speaker-component-1530.png", name: "b" },
    { image: "images/speaker-component-10803.png", name: "c" },
  ];
  return (
    <SpeakerRenderProps>
      {() => {
       return (
         <div>
           {speakers.map((x) => {
             return <img src={x.image} alt={x.name} key={x.image} />;
           })}
         </div>
      )
    }}
    </SpeakerRenderProps>
  );
};

Finished Solution

We can now abstract the code into it's own file and pass the prop from the original component to the abstraction to render the speakers.
Speaker Component

const Speakers = () => {
  return (
    <SpeakerRenderProps>
      {({speakers}) => {
       return (
         <div>
           {speakers.map((x) => {
             return <img src={x.image} alt={x.name} key={x.image} />;
           })}
         </div>
      )
    }}
    </SpeakerRenderProps>
  );
};


Speaker Render Component

function SpeakersRenderProps(props) {
  const speakers = [
    { image: "images/speaker-component-1124.png", name: "a" },
    { image: "images/speaker-component-1530.png", name: "b" },
    { image: "images/speaker-component-10803.png", name: "c" },
  ];
   return props.children({
     speakers: speakers
   })
}
export default SpeakersRenderProps

Context

This is the simplest way to share data. We create a context with

import React from 'react';
const SpeakerContext = React.createContext({})
export default SpeakerContext


Add the context around the components to have access

function Page() {
    return (
        <div>
            <Header />
            <Menu />
            <SpeakerContext.Provider value={speakers}>
              <SpeakerSearchBar />
              <Speakers />
            </SpeakerContext.Provider>
            <Footer />
        </div>
    )
}


Use the context in the component with the hook useContext

import SpeakerContext from './SpeakerContext'
const Speakers = () => {
  const speakers = useContext(SpeakerContext)
  return (
    <div>
      {speakers.map((x) => {
        return <img src={x.image} alt={x.name} key={x.image} />;
      })}
    </div>
  );
};

TailWind

Set up

This is a bit of a departure from the topic but useful to have some exposure to tailwind.

npm i @fullhuman/postcss-purgecss postcss-preset-env tailwindcss --save-dev
npx tailwindcss init -p

Replace purge[] in tailwind.config.js with

purge: ['./pages/**/*.js', './components/**/*.js'],


Then create styles/globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;


And create pages/_app.js

// pages/_app.js
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

global.css

We can create class which can be applied to html in here. For example

.component-highlight {
  hidden: true;
  border-radius: 0.4rem !important;
  border: 5px solid #9bc850 !important;
  margin-top: 10px;
  margin-bottom: 10px;
  margin-left: 5px;
  margin-right: 5px;
}

.component-sub-highlight {
  hidden: true;
  border-radius: 0.4rem !important;
  border: 4px solid #675ba7 !important;
  margin-top: 10px;
  margin-bottom: 10px;
  margin-left: 5px;
  margin-right: 5px;
}

Combining Classes with Tailwind

We can create a file button.css under styles and then combine classes using the @apply keyword in a class

.btn-blue {
    @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}

The btn-blue class can then be used like any other class.

Refactoring into Components

Lazy Loading and The Fold

There is a concept of the fold similar to newspapers where they place an interesting part of the story on the top half of the newspaper. Where to put the fold depends on the audience

  • Certain visitors

Certain visitors are those who are likely to react to the call to action as they have largely made their mind up before visiting the site. This is where known brands have an advantage, as there is little for the “certain visitor” to learn about a product or service. In these cases, placing a call to action above the fold is only a matter of convenience.

  • Uncertain visitors that are familiar with your product – or the proposition is simple

For uncertain visitors that understand the call to action simply or that have some knowledge of the product or service, placing the call to action above the fold is generally best practice for much the same reasons as those listed above, although the informative content is also important.

  • Uncertain visitors that are presented with a complex proposition

For uncertain visitors that are presented with a complex proposition, such as a product or service that isn’t obviously beneficial to them, placing the call to action above the fold will not suffice. What is required is a more in-depth explanation of why your call to action should be acted upon. In fact, placing your call to action up front can appear a little pushy.
For react there is a package called react-simpl-img which will take care of this for us but there are of course alternatives. To implement this we separate the speaker image into its own component.

import React from 'react';
import { SimpleImg } from 'react-simple-img';

function SpeakerImage({ id }) {
  const imageUrl = `/speakers/speaker-${id}.jpg`;
  return (
    <SimpleImg
      src={imageUrl}
      animationDuration="1"
      width={200}
      height={200}
      applyAspectRatio="true"
    />
  );
}
export default SpeakerImage;


@jmperezperez provided a component to detect if visible

class Observer extends Component {
  constructor() {
    super();
    this.state = { isVisible: false };
    this.io = null;
    this.container = null;
  }
  componentDidMount() {
    this.io = new IntersectionObserver([entry] => {
      this.setState({ isVisible: entry.isIntersecting });
    }, {});
    this.io.observe(this.container);
  }
  componentWillUnmount() {
    if (this.io) {
      this.io.disconnect();
    }
  }
  render() {
    return (
      // we create a div to get a reference.
      // It's possible to use findDOMNode() to avoid
      // creating extra elements, but findDOMNode is discouraged
      <div
        ref={div => {
          this.container = div;
        }}
      >
        {Array.isArray(this.props.children)
          ? this.props.children.map(child => child(this.state.isVisible))
          : this.props.children(this.state.isVisible)}
      </div>
    );
  }
}

Implementing Filtering

To add filtering we use the useState hook in the Speakers page. We passed the value and the setter to the SpeakerSearchBar.

const SpeakerSearchBar = ({ searchQuery, setSearchQuery }) => (
  <div className="mb-6">
    <input
      className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
      id="username"
      type="text"
      placeholder="Search by name"
      value={searchQuery}
      onChange={(e) => setSearchQuery(e.target.value)}
    />
  </div>
);
export default SpeakerSearchBar;

And now add the filtering changing

      <div className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-12">
        {speakers
          .map((speaker) => (
            <Speaker key={speaker.id} {...speaker} />
          ))}
      </div>


To add the filtering

      <div className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-12">
        {speakers
          .filter((rec) => {
            const targetString = `${rec.firstName} ${rec.lastName}`.toLowerCase();
            return searchQuery.length === 0
              ? true
              : targetString.includes(searchQuery.toLowerCase());
          })
          .map((speaker) => (
            <Speaker key={speaker.id} {...speaker} />
          ))}
      </div>

Using State to update

This is a problem I encountered with the play state of a track. There is the original data (tracks) and the current state which could be playing, stopped, paused, neither. For the speaker app we are toggling a favourite button against a speaker and the solution was to

  • Put the data into a state
  • Merge the data when changed (onFavoriteToggleHandler)
  • Pass the handler to the component
// Splits the record and toggles the isFavourite
  function toggleSpeakerFavorite(speakerRec) {
    return {
      ...speakerRec,
      isFavorite: !speakerRec.isFavorite,
    };
  }

// Replaces the record in the state for this record
// Like the use of slice for replacing
  function onFavoriteToggleHandler(speakerRec) {

    const toggledSpeakerRec = toggleSpeakerFavorite(speakerRec)

    const speakerIndex = speakers
      .map((speaker) => speaker.id)
      .indexOf(speakerRec.id);

    setSpeakers([
      ...speakers.slice(0, speakerIndex),
      toggledSpeakerRec,
      ...speakers.slice(speakerIndex + 1),
    ]);
  }

Implementing REST

Not many surprises we are going to be using axios for this.

Add GET Request

Using the useEffect we create a function to fetch the data

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('http://localhost:4000/speakers');
        setSpeakers(response.data);
        setStatus(REQUEST_STATUS.SUCCESS);
      } catch (e) {
        setStatus(REQUEST_STATUS.ERROR);
        setError(e);
      }
    };
    fetchData();
  }, []);

Add PUT Request

We change the previous useState to implement a PUT request

  async function onFavoriteToggleHandler(speakerRec) {
    const toggledSpeakerRec = toggleSpeakerFavorite(speakerRec);
    const speakerIndex = speakers
      .map((speaker) => speaker.id)
      .indexOf(speakerRec.id);

    try {
      await axios.put(
        `http://localhost:4000/speakers/${speakerRec.id}`,
        toggledSpeakerRec,
      );
      setSpeakers([
        ...speakers.slice(0, speakerIndex),
        toggledSpeakerRec,
        ...speakers.slice(speakerIndex + 1),
      ]);
    } catch (e) {
      setStatus(REQUEST_STATUS.ERROR);
      setError(e);
    }
  }

Add Json server to the project

Add the package json-server and add an additional script

"json-server": "json-server --watch db.json --port 4000 --delay 1000"


Create some data

{
  "speakers": [
    {
      "id": 1530,
      "firstName": "Tamara",
      "lastName": "Baker",
      "sat": false,
      "sun": true,
      "isFavorite": false,
      "bio": "Tammy has held a number of executive and management roles over the past 15 years, including VP engineering Roles at Molekule Inc., Cantaloupe Systems, E-Color, and Untangle Inc."
    },
    {
      "id": 5996,
      "firstName": "Craig",
      "lastName": "Berntson",
      ....

Showing Status

In Angular we used templates and *ngIf and names for react we do the following

...
      {isLoading && <div>Loading...</div>}
      {hasErrored && (
        <div>
          Loading error... Is the json-server running? (try "npm run
          json-server" at terminal prompt)
          <br />
          <b>ERROR: {error.message}</b>
        </div>
      )}
      {success && (
        <div className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-12">
          {speakers
...

Implementing Reducers

In the code we are managing three lots of state. This is a good case for using a reducer (redux).
Before Code

import React, { useState, useEffect } from 'react';
import axios from 'axios';

import SpeakerSearchBar from '../SpeakerSearchBar/SpeakerSearchBar';
import Speaker from '../Speaker/Speaker';

const Speakers = () => {
  function toggleSpeakerFavorite(speakerRec) {
    return {
      ...speakerRec,
      isFavorite: !speakerRec.isFavorite,
    };
  }

  async function onFavoriteToggleHandler(speakerRec) {
    const toggledSpeakerRec = toggleSpeakerFavorite(speakerRec);
    const speakerIndex = speakers
      .map((speaker) => speaker.id)
      .indexOf(speakerRec.id);

    try {
      await axios.put(
        `http://localhost:4000/speakers/${speakerRec.id}`,
        toggledSpeakerRec,
      );
      setSpeakers([
        ...speakers.slice(0, speakerIndex),
        toggledSpeakerRec,
        ...speakers.slice(speakerIndex + 1),
      ]);
    } catch (e) {
      setStatus(REQUEST_STATUS.ERROR);
      setError(e);
    }
  }

  const [searchQuery, setSearchQuery] = useState('');
  const [speakers, setSpeakers] = useState([]);

  const REQUEST_STATUS = {
    LOADING: 'loading',
    SUCCESS: 'success',
    ERROR: 'error',
  };

  const [status, setStatus] = useState(REQUEST_STATUS.LOADING);
  const [error, setError] = useState({});

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('http://localhost:4000/speakers/');
        setSpeakers(response.data);
        setStatus(REQUEST_STATUS.SUCCESS);
      } catch (e) {
        console.log('Loading data error', e);
        setStatus(REQUEST_STATUS.ERROR);
        setError(e);
      }
    };
    fetchData();
  }, []);

  const success = status === REQUEST_STATUS.SUCCESS;
  const isLoading = status === REQUEST_STATUS.LOADING;
  const hasErrored = status === REQUEST_STATUS.ERROR;

  return (
    <div>
      <SpeakerSearchBar
        searchQuery={searchQuery}
        setSearchQuery={setSearchQuery}
      />
      {isLoading && <div>Loading...</div>}
      {hasErrored && (
        <div>
          Loading error... Is the json-server running? (try "npm run
          json-server" at terminal prompt)
          <br />
          <b>ERROR: {error.message}</b>
        </div>
      )}
      {success && (
        <div className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-12">
          {speakers
            .filter((rec) => {
              const targetString = `${rec.firstName} ${rec.lastName}`.toLowerCase();
              return searchQuery.length === 0
                ? true
                : targetString.includes(searchQuery.toLowerCase());
            })
            .map((speaker) => (
              <Speaker
                key={speaker.id}
                {...speaker}
                onFavoriteToggle={() => onFavoriteToggleHandler(speaker)}
              />
            ))}
        </div>
      )}
    </div>
  );
};
export default Speakers;


After code

import React, { useState, useEffect, useReducer } from 'react';
import axios from 'axios';

import SpeakerSearchBar from '../SpeakerSearchBar/SpeakerSearchBar';
import Speaker from '../Speaker/Speaker';

const Speakers = () => {
  function toggleSpeakerFavorite(speakerRec) {
    return {
      ...speakerRec,
      isFavorite: !speakerRec.isFavorite,
    };
  }

  async function onFavoriteToggleHandler(speakerRec) {
    const toggledSpeakerRec = toggleSpeakerFavorite(speakerRec);
    const speakerIndex = speakers
      .map((speaker) => speaker.id)
      .indexOf(speakerRec.id);

    try {
      await axios.put(
        `http://localhost:4000/speakers/${speakerRec.id}`,
        toggledSpeakerRec,
      );
      setSpeakers([
        ...speakers.slice(0, speakerIndex),
        toggledSpeakerRec,
        ...speakers.slice(speakerIndex + 1),
      ]);
    } catch (e) {
      setStatus(REQUEST_STATUS.ERROR);
      setError(e);
    }
  }

  const [searchQuery, setSearchQuery] = useState('');

  const REQUEST_STATUS = {
    LOADING: 'loading',
    SUCCESS: 'success',
    ERROR: 'error',
  };

  const reducer = (state, action) => {
    switch (action.type) {
      case 'GET_ALL_SUCCESS':
        return {
          ...state,
          status: REQUEST_STATUS.SUCCESS,
          speakers: action.speakers,
        };
      case 'UPDATE_STATUS':
        return {
          ...state,
          status: action.status,
        };
    }
  };

  const [{ speakers, status }, dispatch] = useReducer(reducer, {
    status: REQUEST_STATUS.LOADING,
    speakers: [],
  });

  const [error, setError] = useState({});

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('http://localhost:4000/speakers/');

        dispatch({
          speakers: response.data,
          type: 'GET_ALL_SUCCESS',
        });
      } catch (e) {
        console.log('Loading data error', e);
        dispatch({
          status: REQUEST_STATUS.ERROR,
          type: 'UPDATE_STATUS',
        });
        setError(e);
      }
    };
    fetchData();
  }, []);

  const success = status === REQUEST_STATUS.SUCCESS;
  const isLoading = status === REQUEST_STATUS.LOADING;
  const hasErrored = status === REQUEST_STATUS.ERROR;

  return (
    <div>
      <SpeakerSearchBar
        searchQuery={searchQuery}
        setSearchQuery={setSearchQuery}
      />
      {isLoading && <div>Loading...</div>}
      {hasErrored && (
        <div>
          Loading error... Is the json-server running? (try "npm run
          json-server" at terminal prompt)
          <br />
          <b>ERROR: {error.message}</b>
        </div>
      )}
      {success && (
        <div className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-12">
          {speakers
            .filter((rec) => {
              const targetString = `${rec.firstName} ${rec.lastName}`.toLowerCase();
              return searchQuery.length === 0
                ? true
                : targetString.includes(searchQuery.toLowerCase());
            })
            .map((speaker) => (
              <Speaker
                key={speaker.id}
                {...speaker}
                onFavoriteToggle={() => onFavoriteToggleHandler(speaker)}
              />
            ))}
        </div>
      )}
    </div>
  );
};
export default Speakers;

Final Reducer Code

This shows the final changes. Whilst we will never use the code it does show the approach to take if we want to start using reducers.

Add consts to replace string literals

Add actions/request.js

export const GET_ALL_SUCCESS = 'GET_ALL_SUCCESS';
export const GET_ALL_FAILURE = 'GET_ALL_FAILURE';
export const PUT_SUCCESS = 'PUT_SUCCESS';
export const PUT_FAILURE = 'PUT_FAILURE';

Externalize Reducer

Move reducer code to reducers/request.js

import {
  GET_ALL_SUCCESS,
  GET_ALL_FAILURE,
  PUT_SUCCESS,
  PUT_FAILURE,
} from '../actions/request';

export const REQUEST_STATUS = {
  LOADING: 'loading',
  SUCCESS: 'success',
  ERROR: 'error',
};

const requestReducer = (state, action) => {
  switch (action.type) {
    case GET_ALL_SUCCESS: {
      return {
        ...state,
        records: action.records,
        status: REQUEST_STATUS.SUCCESS,
      };
    }
    case GET_ALL_FAILURE: {
      return {
        ...state,
        status: REQUEST_STATUS.ERROR,
        error: action.error,
      };
    }
    case PUT_SUCCESS:
      const { records } = state;
      const { record } = action;
      const recordIndex = records.map((rec) => rec.id).indexOf(record.id);
      return {
        ...state,
        records: [
          ...records.slice(0, recordIndex),
          record,
          ...records.slice(recordIndex + 1),
        ],
      };
    case PUT_FAILURE:
      console.log(
        'PUT_FAILURE: Currently just logging to console without refreshing records list',
      );
      return {
        ...state,
        error: action.error,
      };
    default:
      return state;
  }
};
export default requestReducer;

Remove dead code from Speakers

Remove the dead code and add call to useReducer

import React, { useState, useEffect, useReducer } from 'react';
import axios from 'axios';

import SpeakerSearchBar from '../SpeakerSearchBar/SpeakerSearchBar';
import Speaker from '../Speaker/Speaker';

import requestReducer from '../../reducers/request';

import {
  GET_ALL_FAILURE,
  GET_ALL_SUCCESS,
  PUT_FAILURE,
  PUT_SUCCESS,
} from '../../actions/request';

const REQUEST_STATUS = {
  LOADING: 'loading',
  SUCCESS: 'success',
  ERROR: 'error',
};

const Speakers = () => {
  const onFavoriteToggleHandler = async (speakerRec) => {
    try {
      const toggledSpeakerRec = {
        ...speakerRec,
        isFavorite: !speakerRec.isFavorite,
      };
      await axios.put(
        `http://localhost:4000/speakers/${speakerRec.id}`,
        toggledSpeakerRec,
      );
      dispatch({
        type: PUT_SUCCESS,
        record: toggledSpeakerRec,
      });
    } catch (e) {
      dispatch({
        type: PUT_FAILURE,
        error: e,
      });
    }
  };

  const [searchQuery, setSearchQuery] = useState('');
  const [{ records: speakers, status, error }, dispatch] = useReducer(
    requestReducer,
    {
      records: [],
      status: REQUEST_STATUS.LOADING,
      error: null,
    },
  );

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('http://localhost:4000/speakers/');

        dispatch({
          type: GET_ALL_SUCCESS,
          records: response.data,
        });
      } catch (e) {
        console.log('Loading data error', e);
        dispatch({
          type: GET_ALL_FAILURE,
          error: e,
        });
      }
    };
    fetchData();
  }, []);

  const success = status === REQUEST_STATUS.SUCCESS;
  const isLoading = status === REQUEST_STATUS.LOADING;
  const hasErrored = status === REQUEST_STATUS.ERROR;

  return (
    <div>
      <SpeakerSearchBar
        searchQuery={searchQuery}
        setSearchQuery={setSearchQuery}
      />
      {isLoading && <div>Loading...</div>}
      {hasErrored && (
        <div>
          Loading error... Is the json-server running? (try "npm run
          json-server" at terminal prompt)
          <br />
          <b>ERROR: {error.message}</b>
        </div>
      )}
      {success && (
        <div className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-12">
          {speakers
            .filter((rec) => {
              const targetString = `${rec.firstName} ${rec.lastName}`.toLowerCase();
              return searchQuery.length === 0
                ? true
                : targetString.includes(searchQuery.toLowerCase());
            })
            .map((speaker) => (
              <Speaker
                key={speaker.id}
                {...speaker}
                onFavoriteToggle={() => onFavoriteToggleHandler(speaker)}
              />
            ))}
        </div>
      )}
    </div>
  );
};
export default Speakers;

More HOC and Render Props

Approach

The way to approach creating an HOC is to look at what the rendering component needs in terms of props. In the case of Speakers this was Speakers, Status and Error. Also the Speakers are updated on whether they are favourite or not so adding a function as a parameter will be a way to remove this from the rendering component.
So our HOC will now look like this, the baseURL and routeName are passed from the rendering component (possibly needs to be somewhere else), but the main takeaway is that you look at the outputs and frame your return function around it.
Warning Dont use this

const withRequest = (baseUrl, routeName) => (Component) => () => {

  const props = { 
     records, // Speakers
     status,
     error,
     put, // Function to update speaker
  }
  return <Component {...props}></Component>
}
export default withRequest


This above approach works but is does not consider that the component being passed may have props. It is did they would be lost. Instead, add the props to the incoming and outgoing so they are not lost and rename the hoc props to avoid collisions

const withRequest = (baseUrl, routeName) => (Component) => (props) => {

  const localProps = { 
     records, // Speakers
     status,
     error,
     put, // Function to update speaker
  }
  return <Component {...props}{...localProps}></Component>
}
export default withRequest

Multiple HOCs

We may need to have multiple HOC for a component. To implement this you could chain the calls together

export default withSpecialMessage(
    withRequest('http://localhost:4000', 'speakers')(Speakers))


But this could get messy so you can use the npm package recompose. Then you can do. Add the import and you can pass them like this

export default compose(
    withRequest('http://localhost:4000', 'speakers'),
    withSpecialMessage()
)(Speakers)


Final HOC Speaker Component

Lets look at the finished component with two HOCs

import React, { useState } from 'react';

import SpeakerSearchBar from '../SpeakerSearchBar/SpeakerSearchBar';
import Speaker from '../Speaker/Speaker';

import { REQUEST_STATUS } from '../../reducers/request';

import withRequest from '../HOCs/withRequest';
import withSpecialMessage from '../HOCs/withSpecialMessage';
import { compose } from 'recompose';

const Speakers = ({
  records: speakers,
  status,
  error,
  put,
  bgColor,
  specialMessage,
}) => {
  const onFavoriteToggleHandler = async (speakerRec) => {
    put({
      ...speakerRec,
      isFavorite: !speakerRec.isFavorite,
    });
  };

  const [searchQuery, setSearchQuery] = useState('');

  const success = status === REQUEST_STATUS.SUCCESS;
  const isLoading = status === REQUEST_STATUS.LOADING;
  const hasErrored = status === REQUEST_STATUS.ERROR;

  return (
    <div className={bgColor}>
      <SpeakerSearchBar
        searchQuery={searchQuery}
        setSearchQuery={setSearchQuery}
      />

      {specialMessage && specialMessage.length > 0 && (
        <div
          className="bg-orange-100 border-l-8 border-orange-500 text-orange-700 p-4 text-2xl"
          role="alert"
        >
          <p className="font-bold">Special Message</p>
          <p>{specialMessage}</p>
        </div>
      )}

      {isLoading && <div>Loading...</div>}
      {hasErrored && (
        <div>
          Loading error... Is the json-server running? (try "npm run
          json-server" at terminal prompt)
          <br />
          <b>ERROR: {error.message}</b>
        </div>
      )}
      {success && (
        <div className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-12">
          {speakers
            .filter((rec) => {
              const targetString = `${rec.firstName} ${rec.lastName}`.toLowerCase();
              return searchQuery.length === 0
                ? true
                : targetString.includes(searchQuery.toLowerCase());
            })
            .map((speaker) => (
              <Speaker
                key={speaker.id}
                {...speaker}
                onFavoriteToggle={() => onFavoriteToggleHandler(speaker)}
              />
            ))}
        </div>
      )}
    </div>
  );
};
export default compose(
  withRequest('http://localhost:4000', 'speakers'),
  withSpecialMessage(),
)(Speakers);

Render Props

Below is the equivalent code using render props. I think it is far more cluttered than the HOC Approach. Firstly the RP functions then then final Speaker component

SpecialMessageRenderProps

For create the special message

function SpecialMessageRenderProps({ children }) {
  return children({
    // could get this from something like a push notification.
    specialMessage: 'Angular Class at 2:45PM Cancelled',
  });
}

export default SpecialMessageRenderProps;

Request

For performing the request

import React, { useEffect, useReducer } from 'react';
import axios from 'axios';

import requestReducer, { REQUEST_STATUS } from '../../reducers/request';
import {
  PUT_SUCCESS,
  PUT_FAILURE,
  GET_ALL_SUCCESS,
  GET_ALL_FAILURE,
} from '../../actions/request';

const Request = ({ baseUrl, routeName, children }) => {
  const [{ records, status, error }, dispatch] = useReducer(requestReducer, {
    status: REQUEST_STATUS.LOADING,
    records: [],
    error: null,
  });

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get(`${baseUrl}/${routeName}`);
        dispatch({
          type: GET_ALL_SUCCESS,
          records: response.data,
        });
      } catch (e) {
        console.log('Loading data error', e);
        dispatch({
          type: GET_ALL_FAILURE,
          error: e,
        });
      }
    };
    fetchData();
  }, [baseUrl, routeName]);

  const childProps = {
    records,
    status,
    error,
    put: async (record) => {
      dispatch({
        type: 'PUT',
      });
      try {
        await axios.put(`${baseUrl}/${routeName}/${record.id}`, record);
        dispatch({
          type: PUT_SUCCESS,
          record,
        });
      } catch (e) {
        dispatch({
          type: PUT_FAILURE,
          error: e,
        });
      }
    },
  };

  return children(childProps);
};

export default Request;

Final Speaker Component in RP

import React, { useState } from 'react';

import SpeakerSearchBar from '../SpeakerSearchBar/SpeakerSearchBar';
import Speaker from '../Speaker/Speaker';

import { REQUEST_STATUS } from '../../reducers/request';

import Request from '../RPs/Request';
import SpecialMessageRenderProps from '../RPs/SpecialMessageRenderProps';

const Speakers = ({ bgColor }) => {
  const [searchQuery, setSearchQuery] = useState('');

  return (
    <div className={bgColor}>
      <SpeakerSearchBar
        searchQuery={searchQuery}
        setSearchQuery={setSearchQuery}
      />
      <>
        <SpecialMessageRenderProps>
          {({ specialMessage }) => {
            return (
              <Request baseUrl="http://localhost:4000" routeName="speakers">
                {({ records: speakers, status, error, put }) => {
                  const onFavoriteToggleHandler = async (speakerRec) => {
                    put({
                      ...speakerRec,
                      isFavorite: !speakerRec.isFavorite,
                    });
                  };
                  const success = status === REQUEST_STATUS.SUCCESS;
                  const isLoading = status === REQUEST_STATUS.LOADING;
                  const hasErrored = status === REQUEST_STATUS.ERROR;

                  return (
                    <>
                      {specialMessage && specialMessage.length > 0 && (
                        <div
                          className="bg-orange-100 border-l-8 border-orange-500 text-orange-700 p-4 text-2xl"
                          role="alert"
                        >
                          <p className="font-bold">Special Message</p>
                          <p>{specialMessage}</p>
                        </div>
                      )}

                      {isLoading && <div>Loading...</div>}
                      {hasErrored && (
                        <div>
                          Loading error... Is the json-server running? (try "npm
                          run json-server" at terminal prompt)
                          <br />
                          <b>ERROR: {error.message}</b>
                        </div>
                      )}
                      {success && (
                        <div className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-12">
                          {speakers
                            .filter((rec) => {
                              const targetString = `${rec.firstName} ${rec.lastName}`.toLowerCase();
                              return searchQuery.length === 0
                                ? true
                                : targetString.includes(
                                    searchQuery.toLowerCase(),
                                  );
                            })
                            .map((speaker) => (
                              <Speaker
                                key={speaker.id}
                                {...speaker}
                                onFavoriteToggle={() =>
                                  onFavoriteToggleHandler(speaker)
                                }
                              />
                            ))}
                        </div>
                      )}
                    </>
                  );
                }}
              </Request>
            );
          }}
        </SpecialMessageRenderProps>
      </>
    </div>
  );
};

export default Speakers;

More Context

Orginal Context

We need to create a more encapsulated context. Previously

import React from 'react';
const SpeakerContext = React.createContext({});
export default SpeakerContext;


And Wrapping the components with the context

export default function Page() {
  const speakers = [
    {
      imageSrc: 'speaker-component-1124',
      name: 'Douglas Crockford',
    },
    {
      imageSrc: 'speaker-component-1530',
      name: 'Tamara Baker',
    },
    {
      imageSrc: 'speaker-component-10803',
      name: 'Eugene Chuvyrov',
    },
  ];
  return (
    <div>
      <Header />
      <Menu />
      <SpeakerContext.Provider value={speakers}>
        <SpeakerSearchBar />
        <Speakers />
      </SpeakerContext.Provider>
      <Footer />
    </div>
  );
}

Creating a Better Context

This can now be expanded to encapsulate the provider and to manage it's own data

import React, { createContext } from 'react';
const SpeakersContext = createContext();

const SpeakersProvider = ({ children }) => {
  const speakers = [
    { imageSrc: 'speaker-component-1124', name: 'Douglas Crockford' },
    { imageSrc: 'speaker-component-1530', name: 'Tamara Baker' },
    { imageSrc: 'speaker-component-10803', name: 'Eugene Chuvyrov' },
  ];

  return (
    <SpeakersContext.Provider value={speakers}>
      {children}
    </SpeakersContext.Provider>
  );
};
export { SpeakersContext, SpeakersProvider };


Below is the reworked Speakers component

const Speakers = () => {
  return (
    <SpeakersProvider>
      <SpeakersComponent></SpeakersComponent>
    </SpeakersProvider>
  );
};
export default Speakers;

Implementing into Speaker Component

We now implement the use of the context into the Speaker context built with the HOC. The context been renamed to DataContext and DataProvider but the rendering code is much the same. The main change is to the export which wraps the component in a context ensuring that any child props are not lost

import React, { useState, useContext } from 'react';
import { DataContext, DataProvider } from '../../contexts/DataContext';

import SpeakerSearchBar from '../SpeakerSearchBar/SpeakerSearchBar';
import Speaker from '../Speaker/Speaker';

import { REQUEST_STATUS } from '../../reducers/request';

import withRequest from '../HOCs/withRequest';
import withSpecialMessage from '../HOCs/withSpecialMessage';
import { compose } from 'recompose';

const SpeakersComponent = ({ bgColor }) => {
  const specialMessage = '';

  const { speakers, status } = useContext(DataContext);

  const onFavoriteToggleHandler = async (speakerRec) => {
    put({
      ...speakerRec,
      isFavorite: !speakerRec.isFavorite,
    });
  };

  const [searchQuery, setSearchQuery] = useState('');

  const success = status === REQUEST_STATUS.SUCCESS;
  const isLoading = status === REQUEST_STATUS.LOADING;
  const hasErrored = status === REQUEST_STATUS.ERROR;
// Rendering code unchanged
  return (
    <div className={bgColor}>
...
    </div>
  );
};

const Speakers = (props) => {
  return (
    <DataProvider>
      <SpeakersComponent {...props}></SpeakersComponent>
    </DataProvider>
  );
};

export default Speakers;