Beginning React - Laracast - Notes

Learned a lot about React from this series, it just went straight to the point, all the necessary skills we need. Although sometimes he doesn't give us a template to work on, I had to get it from his repository on Git Hub.

Getting the boilerlate

<https://github.com/drehimself/lc-react>
find the commit then 
git reset --hard {commit number}

Introduction and Demo

Create React App

npx create-react-app lc-react
  • App.js can be renamed to App.jsx
  • need to use className not just class
import { useState } from 'react'
import './App.css';

function App() {
  const [count, setCount] = useState(0);

  function decrement() {
    setCount(prevCount => prevCount - 1);
  }

  function increment() {
    setCount(prevCount => prevCount + 1);
  }

  const someStyle = {
    background: 'blue',
    color: 'white',
    fontSize: '28px',
    fontWeigt: 'bold',
  }

  return (
    <div className="App">
      <header className="App-header">
        <div><span>{count}</span>
          <button onClick={decrement}>-</button>
          <button onClick={increment}>+</button>
        </div>
        <p style={someStyle}>{ 3 + 2 }</p>
      </header>
    </div>
  );
}

export default App;
//Another.jsx
import React from "react";

export default function Another(props) {
    return (
        <div>
            Another Component , { props.name }
        </div>
    )
}

// App.jsx
import Another from './Another';
return (
    <div className="App">
	      ...
          <Another name="madindo"/>
          ...
    </div>
  );

A Better Development Experience

Make a list of todos, useState, and loop using map.

const [todos, setTodos] = useState([
      {
        id: 1,
        title: "Finish React Series 1",
        isCompleted: false
      },
      {
        id: 2,
        title: "Go groceries",
        isCompleted: true
      },
      {
        id: 3,
        title: "Take over world",
        isCompleted: false
      },
  ]);

<ul className="todo-list">
  { todos.map((todo,index) => (
  <li className="todo-item-container" key={todo.id}>
    <div className="todo-item">
      <input type="checkbox" />
      <span className="todo-item-label">{todo.title}</span>
    </div>
  </li>
  ))}
</ul>

add on submit

<form action="#" onSubmit={addTodo}>
  <input
    type="text"
    className="todo-input"
		value={todoInput} 
		onChange={handleInput}
    placeholder="What do you need to do?"
  />
</form>

// check if the input is empty
if (todoInput.trim().length === 0) {
  return;
}

function addTodo() {
  setTodos([...todos, {
    id: idForTodo, // get the id from setIdForTodo
    title: todoInput, // get input with handleInput, setTodoInput, onChange
    isCompleted: false
  }]);

	setTodoInput(''); // set to empty after todo
  // setIdForTodo(idForTodo + 1)
  setIdForTodo(prevIdForTodo => prevIdForTodo + 1) // increment id put in setTodos
}

const [todoInput, setTodoInput] = useState('');
const [idForTodo, setIdForTodo] = useState(4);

// to get the value from inputtext on change
function handleInput(event) {
  setTodoInput(event.target.value);
}

add todo (classed based)

<form action="#" onSubmit={this.addTodo}>
  <input
    type="text"
    className="todo-input"
    placeholder="What do you need to do?"
  />
</form>

addTodo = event => {
  event.preventDefault();
  this.setState(prevState => {
    const newTodos = [...prevState.todos, {
      id: 4,
      title: 'this is class based',
      isCompleted: false
    }];
    return { todos: newTodos }
  })
}

delete todo

<button onClick={deleteTodo(todo.id)} className="x-button"></button>

function deleteTodo(id) {
  setTodos([... todos].filter(todo => todo.id !== id));
}

edit todo

<span 
	className={`todo-item-label ${todo.isCompleted ? 'line-through' : ''}`}
	onDoubleClick={() => markAsEditing(todo.id)}
>{todo.title}</span>

<div className="todo-item">
  <input type="checkbox" 
    onChange={() => completeTodo(todo.id)}
    checked={todo.isCompleted ? true : false}
  />

function completeTodo(id) {
  const updatedTodos = todos.map(todo => {
    if (todo.id === id) {
      todo.isCompleted = !todo.isCompleted
    }
    return todo;
  });
  setTodos(updatedTodos);
}

// double click on span to input to be able to edit the title
<input type="text" 
  className={`todo-item-input ${todo.isCompleted ? 'line-through' : ''}`} 
  value={todo.title} 
	onBlur={(event) => updateTodo(event, todo.id)}
  autoFocus
/>

{
  id: 3,
  title: "Take over world",
  isCompleted: false,
  isEditing: false // add new field isEditing
},

function markAsEditing(id) {
  const editingTodos = todos.map(todo => {
    if (todo.id === id) {
      todo.isEditing = !todo.isEditing
    }
    return todo;
  });
  setTodos(editingTodos);
}

function updateTodo(event, id) {
  const editingTodos = todos.map(todo => {
    if (todo.id === id) {
      if (event.target.value.trim().length === 0) {
        todo.isEditing = false
        return todo;
      }
      todo.title = event.target.value
      todo.isEditing = false
    }
    return todo;
  });
  setTodos(editingTodos);
}

Hide when there are no todos

{ todos.length > 0 ? (
<>
  <ul className="todo-list">
    { todos.map((todo,index) => (
    <li className="todo-item-container" key={todo.id}>
      <div className="todo-item">
        <input type="checkbox" 
          onChange={() => completeTodo(todo.id)}
          checked={todo.isCompleted ? true : false}
        />
        { !todo.isEditing ? (
        <span 
          className={`todo-item-label ${todo.isCompleted ? 'line-through' : ''}`}
          onDoubleClick={() => markAsEditing(todo.id)}
        >
          {todo.title}
        </span>
        ) :
        (
        <input type="text" 
          className={`todo-item-input ${todo.isCompleted ? 'line-through' : ''}`} 
          defaultValue={todo.title} 
          onKeyDown={event => {
            if (event.key === 'Enter') {
              updateTodo(event, todo.id)
            } else if (event.key === 'Escape') {
              cancelEdit(event, todo.id)
            }
          }}
          onBlur={(event) => updateTodo(event, todo.id)}
          autoFocus
        />
        )}
      </div>
      <button onClick={() => deleteTodo(todo.id)} className="x-button">
        <svg
          className="x-button-icon"
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M6 18L18 6M6 6l12 12"
          />
        </svg>
      </button>
    </li>
    ))}
  </ul>
  <div className="check-all-container">
    <div>
      <div className="button">Check All</div>
    </div>

    <span>3 items remaining</span>
  </div>

  <div className="other-buttons-container">
    <div>
      <button className="button filter-button filter-button-active">
        All
      </button>
      <button className="button filter-button">Active</button>
      <button className="button filter-button">Completed</button>
    </div>
    <div>
      <button className="button">Clear completed</button>
    </div>
  </div>
</>
) : (
<div className="no-todos-container">
  <p>Add some todos</p>
</div>
) 
}

Extract the else into a component

<div className="no-todos-container">
  <p>Add some todos</p>
</div>

to

<NoTodos />

// create file Notodos.jsx
import NoTodos from './NoTodos'
) : (
  <NoTodos />
)

Extract the form and the list, addTodo functions stay in App.jsx because it’s dependent on others but in the form add a new handle submit move some lines, and then add props to the receive function from App.jsx

//App.jsx
//this stays
function addTodo(todo) {

  setTodos([...todos, {
    id: idForTodo,
    title: todo,
    isCompleted: false
  }]);

  setIdForTodo(prevIdForTodo => prevIdForTodo + 1)
}

//TodoForm.jsx
import React, { useState } from 'react'

export default function TodoForm(props) {

  const [todoInput, setTodoInput] = useState('');

  function handleInput(event) {
    setTodoInput(event.target.value);
  }
  function handleSubmit(event) {
    event.preventDefault();

    if (todoInput.trim().length === 0) {
      return;
    }

    props.addTodo(todoInput)

    setTodoInput('');
  }

  return (
    <form action="#" onSubmit={handleSubmit}>
        <input
        type="text"
        value={todoInput}
        onChange={handleInput}
        className="todo-input"
        placeholder="What do you need to do?"
        />
    </form>
  )
}

Extract the list

//App.jsx
{ todos.length > 0 ? 
  <TodoList 
    todos={todos}
    completeTodo={completeTodo}
    markAsEditing={markAsEditing}
    updateTodo={updateTodo}
    cancelEdit={cancelEdit}
    deleteTodo={deleteTodo}
  /> 
  : <NoTodos /> }

//TodoList.jsx
import React from 'react'
export default function TodoList(props) {
  return (
    <>
      <ul className="todo-list">
          { props.todos.map((todo,index) => (
          <li className="todo-item-container" key={todo.id}>
          <div className="todo-item">
              <input type="checkbox" 
              onChange={() => props.completeTodo(todo.id)}
              checked={todo.isCompleted ? true : false}
              />
              { !todo.isEditing ? (
              <span 
              className={`todo-item-label ${todo.isCompleted ? 'line-through' : ''}`}
              onDoubleClick={() => props.markAsEditing(todo.id)}
              >
              {todo.title}
              </span>
              ) :
              (
              <input type="text" 
              className={`todo-item-input ${todo.isCompleted ? 'line-through' : ''}`} 
              defaultValue={todo.title} 
              onKeyDown={event => {
                  if (event.key === 'Enter') {
                      props.updateTodo(event, todo.id)
                  } else if (event.key === 'Escape') {
                      props.cancelEdit(event, todo.id)
                  }
              }}
              onBlur={(event) => props.updateTodo(event, todo.id)}
              autoFocus
              />
              )}
          </div>
          <button onClick={() => props.deleteTodo(todo.id)} className="x-button">
              <svg
              className="x-button-icon"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
              >
              <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M6 18L18 6M6 6l12 12"
              />
              </svg>
          </button>
          </li>
          ))}
      </ul>
      <div className="check-all-container">
          <div>
          <div className="button">Check All</div>
          </div>

          <span>3 items remaining</span>
      </div>

      <div className="other-buttons-container">
          <div>
          <button className="button filter-button filter-button-active">
              All
          </button>
          <button className="button filter-button">Active</button>
          <button className="button filter-button">Completed</button>
          </div>
          <div>
          <button className="button">Clear completed</button>
          </div>
      </div>
      </>
  )
}

Checking types of props to be sent

npm: prop-types

npm install --save prop-types
//TodoForm
\\import PropTypes from 'prop-types';
function TodoForm(props) { 
}

TodoForm.propTypes = {
    addTodo: PropTypes.func,
}

export default TodoForm

Remaining to a new component

//App.jsx
function remaining() {
  return todos.filter(todo => !todo.isCompleted).length;
}

//pass function
<TodoList 
  ...
  remaining={remaining}
/>

//TodoList.jsx
import TodoItemRemaining from './TodoItemRemaining';
TodoList.propTypes = {
    remaining: PropTypes.func.isRequired,
}
<TodoItemRemaining remaining={props.remaining}/>

//TodoRemaining
import React from 'react'

function TodoItemRemaining(props) {
  return (
    <span>{props.remaining()} items remaining</span>
  )
}
export default TodoItemRemaining

Clear Completed to new component as well

//app.jsx
function clearCompleted() {
  setTodos([... todos].filter(todo => !todo.isCompleted))
}
<TodoList 
  ...
  clearCompleted={clearCompleted}
/>

//TodoList.jsx
import TodoClearCompleted from './TodoClearCompleted';
TodoList.propTypes = {
		...
    clearCompleted: PropTypes.func.isRequired,
}
<TodoClearCompleted clearCompleted={props.clearCompleted} />

//TodoClearCompleted
import React from 'react'

function TodoClearCompleted(props) {
  return (
    <button className="button" onClick={props.clearCompleted}>Clear completed</button>
  )
}

export default TodoClearCompleted

Filters

//App.jsx
function todosFiltered(filter) {
  if (filter === 'all') {
    return todos;
  } else if (filter === 'active') {
    return todos.filter(todo => !todo.isCompleted)
  }else if (filter === 'completed') {
    return todos.filter(todo => todo.isCompleted)
  }
}

<TodoList 
	...
  todosFiltered={todosFiltered}
/>

//TodoList.jsx
import TodoFilters from './TodoFilters';
<TodoFilters 
    todosFiltered={props.todosFiltered}
    filter={filter}
    setFilter={setFilter}
/>

//TodoFilters.jsx
import React from 'react'
import PropTypes from 'prop-types';

TodoFilters.propTypes = {
    todosFiltered: PropTypes.func.isRequired,
    filter: PropTypes.string.isRequired,
    setFilter: PropTypes.func.isRequired,
}

function TodoFilters(props) {
  return (
    <div>
        <button onClick={() => {
            props.setFilter('all');
            props.todosFiltered('all');
        }} className={ `button filter-button  ${props.filter === 'all' ? 'filter-button-active' : ''}`}>
            All
        </button>
        <button onClick={() => {
            props.setFilter('active');
            props.todosFiltered('active');
        }} className={ `button filter-button  ${props.filter === 'active' ? 'filter-button-active' : ''}`}>Active</button>
        <button onClick={() => {
            props.setFilter('completed');
            props.todosFiltered('completed');
        }} className={ `button filter-button  ${props.filter === 'completed' ? 'filter-button-active' : ''}`}>Completed</button>
    </div>
  )
}

export default TodoFilters

Other Built-in React Hooks

Adding name

const [name, setName] = useState('');

<div className="name-container">
  <h2>What is your name?</h2>
  <form action="#">
    <input type="text" className="todo-input" placeholder="What is your name" value={name} onChange={event => setName(event.target.value)}/>
  </form>
  { name && <p className="name-label">Hello, {name}</p>}
</div>

useRef

import { useRef } from 'react';
const nameInputEl = useRef(null);
<button onClick={() => nameInputEl.current.focus()}>Click Me</button>
<input type="text" ref={nameInputEl} />

Equivalent to componentDidMount

import { useEffect } from 'react';
useEffect(() => {
  nameInputEl.current.focus()
}, []);

useMemo

import { useMemo } from 'react';
function remainingCaculation() {
  return todos.filter(todo => !todo.isCompleted).length;
}
const remaining = useMemo(remainingCaculation, [todos])

Custom Hooks

//before 
const [twoVisible, setTwoVisible] = useState(true);
<button onClick={() => setTwoVisible(prevTwoVisible => !prevTwoVisible)} className="button">Feature Two Toggle</button>

//after, naming must have use
// create new file hooks/useToggle.js

import { useState } from 'react';
function useToggle(initialState = true) {
  const [visible, setVisible] = useState(initialState);
  function toggle() {
    setVisible(prevVisible => !prevVisible)
  }
  return [visible, toggle];
}
export default useToggle

set to localstorage then handling handleNameInput as a custom hook


//before 
const [name, setName] = useState('');
<input 
  type="text" 
  ref={nameInputEl} 
  className="todo-input" 
  placeholder="What is your name" 
  value={name} 
  onChange={handleNameInput}
/>

function handleNameInput(event) {
  setName(event.target.value);
  localStorage.setItem('name', JSON.stringify(event.target.value));
}

useEffect(() => {
  setName(JSON.parse(localStorage.getItem('name')) ?? '');
}, []);

//after ... custom hook make file useLocalStorage

import { useEffect, useState } from 'react';
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const item = localStorage.getItem(key)
    return item ? JSON.parse(item) : initialValue
  });
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value]);
  return [value,setValue];
}
export default useLocalStorage

const [name, setName] = useLocalStorage('name', '');

using localstorage for the list

//before
const [todos, setTodos] = useState([]);

//after
const [todos, setTodos] = useLocalStorage('todos', []);
const [idForTodo, setIdForTodo] = useLocalStorage('idForTodo', 1);

Context for state management

//create new context folder /context/TodoContext.js

import { createContext } from "react";
export const TodosContext = createContext();
return (
	<TodosContext.Provider value={{ todos, setTodos, idForTodo, setIdForTodo}}>
	  <div className="todo-app-container">
	    ...
	  </div>
	</TodosContext.Provider>
)

Removing from app.jsx

//TodoForm ... remove app.jsx

function addTodo(event) {
  event.preventDefault();

  if (todoInput.trim().length === 0) {
    return;
  }

  setTodos([...todos, {
    id: idForTodo,
    title: todoInput,
    isCompleted: false
  }]);

  setIdForTodo(prevIdForTodo => prevIdForTodo + 1)

  setTodoInput('');
}

//TodoItemRemaining

function TodoItemRemaining() {

  const { todos } = useContext(TodosContext);
  
  function remainingCalculation() {
    return todos.filter(todo => !todo.isCompleted).length;
  }

  const remaining = useMemo(remainingCalculation, [todos]);

  return (
    <span>{remaining} items remaining</span>
  )
}
//TodoCompleteAll

import React, {useContext} from 'react'
import { TodosContext } from '../context/TodosContext';

function TodoCompleteAll(props) {
  const { todos, setTodos } = useContext(TodosContext);

  function completeAllTodos() {
    const editingTodos = todos.map(todo => {
      todo.isCompleted = true
      return todo;
    });
    setTodos(editingTodos);
  }

  return (
    <div>
        <div onClick={completeAllTodos} className="button">Check All</div>
    </div>
  )
}

export default TodoCompleteAll
//TodoClearCompleted

import React, { useContext } from 'react'
import { TodosContext } from '../context/TodosContext';

function TodoClearCompleted() {
  const { todos, setTodos} = useContext(TodosContext);

  function clearCompleted() {
    setTodos([...todos].filter(todo => !todo.isCompleted))
  }

  return (
    <button className="button" onClick={clearCompleted}>Clear completed</button>
  )
}

export default TodoClearCompleted
//TodoFilters

import React, { useContext } from 'react'
import { TodosContext } from '../context/TodosContext';

function TodoFilters() {

  const { filter, setFilter, todosFiltered } = useContext(TodosContext);
  return (
    <div>
        <button onClick={() => {
            setFilter('all');
            todosFiltered('all');
        }} className={ `button filter-button  ${filter === 'all' ? 'filter-button-active' : ''}`}>
            All
        </button>
        <button onClick={() => {
            setFilter('active');
            todosFiltered('active');
        }} className={ `button filter-button  ${filter === 'active' ? 'filter-button-active' : ''}`}>Active</button>
        <button onClick={() => {
            setFilter('completed');
            todosFiltered('completed');
        }} className={ `button filter-button  ${filter === 'completed' ? 'filter-button-active' : ''}`}>Completed</button>
    </div>
  )
}

export default TodoFilters

CSS transition with react transition

React Transition Group

# npm
npm install react-transition-group --save

# yarn
yarn add react-transition-group
import { CSSTransition } from 'react-transition-group';

<CSSTransition 
  in={isFeaturesOneVisible} 
  timeout={300} 
  classNames="slide-vertical"
  unmountOnExit>
	<div className="check-all-container">
	    <TodoCompleteAll />
	    <TodoItemRemaining />
	</div>
</CSSTransition>
import { CSSTransition, TransitionGroup } from 'react-transition-group';

<TransitionGroup component="ul" className="todo-list">
            { todosFiltered().map((todo,index) => (
            <CSSTransition key={todo.id} timeout={300} classNames="slide-horizontal">
						...
						</CSSTransition>
            ))}
</TransitionGroup>
import { CSSTransition, SwitchTransition } from 'react-transition-group';

<SwitchTransition mode="out-in" >
  <CSSTransition key={todos.length > 0} timeout={300} classNames="slide-vertical" unmountOnExit>
  { todos.length > 0 ? 
    <TodoList /> : <NoTodos /> }
  </CSSTransition>
</SwitchTransition>

note: use classNames not className

React Router

//create new Root.jsx this will be on top of the main app file
import React from 'react'
import App from './App'
import About from './pages/About'
import NavigationBar from './NavigationBar'

export default function Root() {
  return (
    <div className="todo-app-container">
        <NavigationBar />
        <div className="content">
            <App />
            {/* <About /> */} 
        </div>
    </div>
  )
}
//navigation bar

import React from 'react'

export default function NavigationBar() {
  return (
    <nav>
        <ul>
            <li><a href="#">Home</a></li>
            <li><a href="#">About</a></li>
            <li><a href="#">Contact</a></li>
        </ul>
    </nav>
  )
}
// change App to Root the one we just made
ReactDOM.render(
  <React.StrictMode>
    <Root /> 
  </React.StrictMode>,
  document.getElementById('root')
);
yarn add react-router-dom
// Root.jsx now add Switch, Route
export default function Root() {
  return (
    <Router>
        <div className="todo-app-container">
            <NavigationBar />
            <div className="content">
                <Switch>
                    <Route exact path="/">
                        <App />
                    </Route>
                    <Route path="/about">
                        <About />
                    </Route>
                    <Route path="/contact">
                        <Contact />
                    </Route>
                </Switch>
            </div>
        </div>
    </Router>
  )
}
<Switch>
    ...
    <Route exact path="/blog">
        <Blog />
    </Route>
    <Route path="/blog/:id">
        <BlogPost />
    </Route>
</Switch>

create Blog & BlogPost, note for Link (can't add active state) NavLink can

import { Link } from 'react-router-dom'
export default function Blog() {
  return (
    <div className="container"><ul>
        <li>
            <Link to="/blog/1">Post One</Link>
        </li>
        <li>
            <Link to="/blog/2">Post Two</Link>
        </li>
    </ul></div>
  )
}

export default function BlogPost() {
    const params = useParams();
  return (
    <div className="container">
        this is blog post {params.id}
    </div>
  )
}

404

// create NoMatch.jsx
<Switch>
    ...
	<Route path="*">
	    <NoMatch />
	</Route>
</Switch>

another way to do it

export default function Root() {

    const routes = [
        { path: '/', name: 'Home', Component: App, exact:true },
        { path: '/about', name: 'About', Component: About, exact:false },
        { path: '/contact', name: 'Contact', Component: Contact, exact:false },
        { path: '/blog', name: 'Blog', Component: Blog, exact:true },
        { path: '/blog/:id', name: 'Post', Component: BlogPost, exact:false },
        { path: '*', name: 'No Match', Component: NoMatch, exact:true },
    ];

    return (
    <Router>
        <div className="todo-app-container">
            <NavigationBar />
            <div className="content">
                <Switch>
                    { routes.map(({ path, Component, exact }) => (
                        <Route key={path} path={path} exact={exact}>
                            <Component />
                        </Route>
                    ))}
                </Switch>
            </div>
        </div>
    </Router>
  )
}

laracast-react.zip

Fetching data

import './App.css';
import { useState } from 'react';
import Reddit from './Reddit';
import Joke from './Joke';

function App() {
  const [redditVisible, setRedditVisible] = useState(false);
  const [jokeVisible, setJokeVisible] = useState(false);
  return (
    <div>
      <div className='buttons'>
        <button
          onClick={() => setRedditVisible(prevRedditVisible => !redditVisible)}
        >Toggle Redit</button>
        <button
          onClick={() => setJokeVisible(prevJokeVisible => !jokeVisible)}
        >Toggle Joke</button>
      </div>
      {redditVisible && <Reddit />}
      {jokeVisible && <Joke />}
    </div>
  );
}

export default App;
import React from 'react'
import { useEffect } from 'react';
import { useState } from 'react';

export default function Reddit() {
    const [posts, setPosts] = useState([]);
    const [isLoading, setIsLoading] = useState(true);
    const [errorMessage, setErrorMessage] = useState(null);
    useEffect(() => {
        fetch('<https://www.reddit.com/r/gaming/.json>')
            .then(response => response.json())
            .then(results => {
                setIsLoading(false);
                setPosts(results.data.children)
            })
            .catch(error => {
                setIsLoading(false);
                setErrorMessage("There was an error")
            })
    }, [])
  return (
    <div>
        <h2>Reddit Api</h2>
        { isLoading && (
            <div>Loading...</div>
        )}
        { errorMessage && (
            <div>{errorMessage}</div>
        )}
        { posts && (
            <ul >
                {posts.map( post => (
                <li key={post.data.id}>
                    <a href={`https://reddit.com${post.data.permalink}`}>
                        {post.data.title}
                    </a>
                </li>
                ))}
            </ul>
        )}
    </div>
  )
}
import React from 'react'
import { useEffect } from 'react';
import { useState } from 'react';

export default function Reddit() {
    const [joke, setJoke] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [errorMessage, setErrorMessage] = useState(null);
    useEffect(() => {
        fetch('<https://official-joke-api.appspot.com/jokes/random>')
            .then(response => response.json())
            .then(result => {
                setIsLoading(false);
                setJoke(result.setup + ' ' + result.punchline)
            })
            .catch(error => {
                setIsLoading(false);
                setErrorMessage("There was an error")
            })
    }, [])
  return (
    <div>
        <h2>JOKE Api</h2>
        { isLoading && (
            <div>Loading...</div>
        )}
        { errorMessage && (
            <div>{errorMessage}</div>
        )}
        { joke && (
            <p>{joke}</p>
        )}
    </div>
  )
}

Simplify

//useFetch.js

import { useEffect, useState } from "react";

function useFetch(url) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [errorMessage, setErrorMessage] = useState(null);
  useEffect(() => {
      fetch(url)
          .then(response => response.json())
          .then(results => {
              setIsLoading(false);
              setData(results)
          })
          .catch(error => {
              setIsLoading(false);
              setErrorMessage("There was an error")
          })
  }, [url])
  
  return { data, isLoading, errorMessage}
}

export default useFetch
import React from 'react'
import useFetch from './useFetch';

export default function Reddit() {
    const { data: posts, isLoading, errorMessage } = useFetch('<https://www.reddit.com/r/gaming/.json>')
  return (
    <div>
        <h2>Reddit Api</h2>
        { isLoading && (
            <div>Loading...</div>
        )}
        { errorMessage && (
            <div>{errorMessage}</div>
        )}
        { posts && (
            <ul >
                {posts.data.children.map( post => (
                <li key={post.data.id}>
                    <a href={`https://reddit.com${post.data.permalink}`}>
                        {post.data.title}
                    </a>
                </li>
                ))}
            </ul>
        )}
    </div>
  )
}
import React from 'react'
import useFetch from './useFetch'

export default function Reddit() {
    const { data: joke, isLoading, errorMessage } = useFetch('<https://official-joke-api.appspot.com/jokes/random>')
  return (
    <div>
        <h2>JOKE Api</h2>
        { isLoading && (
            <div>Loading...</div>
        )}
        { errorMessage && (
            <div>{errorMessage}</div>
        )}
        { joke && (
            <p>{joke.setup + ' ' + joke.punchline}</p>
        )}
    </div>
  )
}

Fetching data with react-query

npm i react-query

Installation | TanStack Query Docs

react query, devtools

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {
  QueryClient,
  QueryClientProvider,
} from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';

const root = ReactDOM.createRoot(document.getElementById('root'));
const queryClient = new QueryClient()

root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools />
    </QueryClientProvider>
  </React.StrictMode>
);

reportWebVitals();

Reddit

import React from 'react'
import { useQuery } from 'react-query';

export default function Reddit() {

    const { data: posts, isLoading, isError, error, isSuccess } = useQuery('posts', fetchPosts, {retry: false});

    function fetchPosts() {
        return fetch('<https://www.reddit.com/r/gaming/.json>').then(response => response.json());
    }

  return (
    <div>
        <h2>Reddit Api</h2>
        { isLoading && (
            <div>Loading...</div>
        )}
        { isError && (
            <div>{error.message}</div>
        )}
        { isSuccess && (
            <ul >
                {posts.data.children.map( post => (
                <li key={post.data.id}>
                    <a href={`https://reddit.com${post.data.permalink}`}>
                        {post.data.title}
                    </a>
                </li>
                ))}
            </ul>
        )}
    </div>
  )
}

Joke

import React from 'react'
import { useQuery } from 'react-query';

export default function Joke() {

    const { data: joke, isLoading, isError, error, isSuccess } = useQuery('joke', fetchJoke, {staleTime: 6000});

    function fetchJoke() {
        return fetch('<https://official-joke-api.appspot.com/jokes/random>').then(response => response.json());
    }

    return (
    <div>
        <h2>JOKE Api</h2>
        { isLoading && (
            <div>Loading...</div>
        )}
        { isError && (
            <div>{error.message}</div>
        )}
        { isSuccess && (
            <p>{joke.setup + ' - ' + joke.punchline}</p>
        )}
    </div>
  )
}