General tips

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the react category.

Last Updated: 2024-04-25

React

Disclaimer Most of this code comes from the official docs or other, better sources online. I have made simplifications of my own as well as clarifying comments etc.

1. Starting a project

Useful esp for technical interviews

npx create-react-app myappname

2. Hooks

Hook A. useState

Gotchas when updating state of container objects (e.g. arrays, dicts)

What is wrong with this code in React?

const modifyCarsList = (element, id) => {
  carsList[id].checked = element.target.checked;
  setCarsList(carsList);
};

The carList is still the same item (an array) in RAM , even though a value WITHIN the array changed. This update cannot be detected by React. Instead such as change must be done by passing a new array object into the setCarList function.

const modifyCarsList = (element, id) => {
  const { checked } = element.target;
  setcarsList((cars) => {
    return cars.map((car, index) => {
      if (id === index) {
        car = { ...car, checked };
      }
      return car;
    });
  });
};
setState(prevState => [...prevState, newItem])
// Notice the prev, the circles around the object, the single curly braces (only at start and fin)
setState(prev => ({...prev, "foo": "bar"}))

Hook B. useEffect

How often it renders

  1. EVERY render (no dependencies)
   useEffect(() => {
     // Runs after EVERY rendering
   });

2.Once, after initial render only

  useEffect(() => {
     // Runs ONCE after initial rendering
   }, []);

=> ideal for fetching data from server initially

3.Every time prop OR state changes

function MyComponent({ prop }) {
   const [state, setState] = useState('');

   useEffect(() => {
     // Runs ONCE after initial rendering
     // and after every rendering ONLY IF `prop` or `state` changes
   }, [prop, state])
 }

RAM references issues

function Blub() {
  return <Foo bar="bar value" baz={3} />
}

function Foo({bar, baz}) {
  const options = {bar, baz}

  React.useEffect(() => {
    buzz(options)
  }, [options]) // we want this to re-run if bar or baz change

  return <div>foobar</div>
}

// Instead do this (extract the simple values from the objects and use as dependencies)

function Foo({bar, baz}) {
  const options = {bar, baz}

  React.useEffect(() => {
    buzz(options)
  }, [bar, baz]) // take the individual values out of the object instead, which won't change

  return <div>foobar</div>
}

OK - but what if bar is a function and baz is an complex object (e.g. an array) - these will always be different on each render, causing performance death

function Blub() {
  const bar = () => {}
  const baz = [1, 2, 3]
  return <Foo bar={bar} baz={baz} />
}

// This is precisely the reason why useCallback and useMemo exist. So here's how you'd fix that (all together now):
// Then do this -- wrap any FUNCTIONS in useCallback and any objects/arrays in useMemo)

function Blub() {
  const bar = React.useCallback(() => {}, [])
  const baz = React.useMemo(() => [1, 2, 3], [])

  return <Foo bar={bar} baz={baz} />
}

Be careful -- if you define a function in your component body then use it in useEffect, then you HAVE to declare it in your dependences to useEffect

const Example = () => {
  const [count, setCount] = useState(0)
  const [showMessage, setShowMessage] = useState(true)

  // inner helper function that will be called w/in `useEffect()`
  const hideMessage = () => {
    if (count < 10) {
      setShowMessage(false)
    }
  }

  useEffect(() => {
    window
      .fetch('https://api.benmvp.com/')
      .then((res) => res.json())
      .then((data) => {
        if (data.success) {
          // ?? not including `hideMessage()` in the dependencies of
          // `useEffect()` is a lurking bug and will trigger the
          // `react-hooks/exhaustive-deps` ESLint rule
          hideMessage()
        }
      })
  }, [])

  render (
    <p>Bla</p>
  )
}

But wait there is an EVEN BIGGER problem here even if you changed the dependencies to

useEffect(() => {
  ...
}, [hideMessage])

The problem is that hideMessage will be a new object every time!!! Therefore triggering re-renders. We need to wrap it in useCallback to fix this issue!

const Example = () => {
  const [count, setCount] = useState(0)
  const [showMessage, setShowMessage] = useState(true)

  // ???? The reference of `hideMessage()` will only change if/when
  // the value of `count` changes
  const hideMessage = useCallback(() => {
    if (count < 10) {
      setShowMessage(false)
    }
  }, [count])

  useEffect(() => {
    window
      .fetch('https://api.benmvp.com/')
      .then((res) => res.json())
      .then((data) => {
        if (data.success) {
          hideMessage()
        }
      })
  }, [hideMessage])

  render (
    <p>Bla</p>
  )
}

Cleanup

You can clean up with an effect by returning a function

useEffect(() => {
  Subscribe.toMessages()
  return () => Unsubscribe.fromMessages()
}, [])

Async

Remember useEffect itself can't be async -- you need to define a subfunction that is async and call that

 useEffect(() => {  // <--- CANNOT be an async function
    async function fetchEmployees() {
      // ...
    }
    fetchEmployees(); // <--- But CAN invoke async functions
  }, []);

Hook C. useContext

// make some data
const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

// pass data in via createContext and name a variable XContent with the result
const ThemeContext = React.createContext(themes.light);

function App() {
  return (
  // Wrap some components with XContent.Provider, giving it a `value` por with some data
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// This is just here to show we don't need to drill props down to ThemedButton,
// thanks to useContext
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

// Here we actually use the data
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

Here is how to pass multiple values in

const CounterContext = React.createContext("counter")

export default function MultiValueContextDemo() {
    const [counter1, setCounter1] = useState(0);
    const [counter2, setCounter2] = useState(0);

    return(
        <CounterContext.Provider value={{ counter1, setCounter1, counter2, setCounter2 }}>
            <CounterComponent />
        </CounterContext.Provider>
    )
}

Here's a fuller example

// File 1: the context
export type GeoContext = {
  isLoading: boolean;
  status: Status;
  setStatus: React.Dispatch<React.SetStateAction<Status>>;
  setLoading: React.Dispatch<React.SetStateAction<boolean>>;
}

export const GeolocationContext = createContext<GeoContext>({
  isLoading: false,
  status: 'prompt',
  setStatus: () => {},
  setLoading: () => {},
});

export const GeolocationProvider: React.FC = ({ children }) => {
  const [status, setStatus] = useState<Status>('prompt');
  const [isLoading, setLoading] = useState(false);

  return <GeolocationContext.Provider value={{ isLoading, setLoading, status, setStatus }}>{children}</GeolocationContext.Provider>;
}

// File 2: Usage
const App = () => {
  <GeolocationProvider>
    <ChildThatUsesThisContext/>
  <GeolocatoinProvider/>
}

Hook D. useCallback

function MyComponent() {
  // handleClick is re-created on each render and
  // will be a DIFFERENT function object....
  const handleClick = () => {
    console.log('Clicked!');
  };
  // ...
}

Problem? Usually not.Because inline functions are cheap, the re-creation of functions on each rendering is not a problem. A few inline functions per component are acceptable.

However there are special cases where need to mantain that same original photo

That's when useCallback(callbackFun, deps) is helpful: given the same dependency values deps, the hook returns the same function instance between renderings (aka memoization):

import { useCallback } from 'react';
function MyComponent() {
  // handleClick is the same function object
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);
  // ...
}

What's a a good use-case?

Say you have a component MyBigListthat you to memoize

import useSearch from './fetch-items';
function MyBigList({ term, onItemClick }) {
  const items = useSearch(term);
  const map = item => <div onClick={onItemClick}>{item}</div>;
  return <div>{items.map(map)}</div>;
}
export default React.memo(MyBigList);
import { useCallback } from 'react';
export function MyParent({ term }) {
  const onItemClick = useCallback(event => {
    console.log('You clicked ', event.currentTarget);
  }, [term]);
  return (
    <MyBigList
      term={term}
      onItemClick={onItemClick}
    />
  );
}

Memo will re-render if your props change. And that matters here because theuseSearch function could be really slow... therefore optimization that won'd justify itself in "normal areas" would be important here. So you are worried about the onItemClickbit. You don't want it to be re-rendered everytime onTimeClick functin is generated, as what happens normally

Hook E. useMemo

useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.

Here it is used in a filter

const filteredUsers = React.useMemo(
    () =>
      users.filter((user) => {
        console.log('Filter function is running ...');
        return user.name.toLowerCase().includes(search.toLowerCase());
      }),
    [search]
  );

Ensuring hooks are run in the same order

The following code is gonna have weird bugs. If id is empty, the component renders 'Please select a game to fetch' and exits. No hooks are invoked. But if id is not empty (e.g. equals '1'), then the useState() and useEffect() hooks are invoked.

function FetchGame({ id }) {
  if (!id) {
    return 'Please select a game to fetch';
  }
  const [game, setGame] = useState({
    name: '',
    description: ''
  });
  useEffect(() => {
    const fetchGame = async () => {
      const response = await fetch(`/api/game/${id}`);
      const fetchedGame = await response.json();
      setGame(fetchedGame);
    };
    fetchGame();
  }, [id]);
  return (
    <div>
      <div>Name: {game.name}</div>
      <div>Description: {game.description}</div>
    </div>
  );
}

Custom hooks

Purpose: reusing code across components

// Custom hooks make use of useState, and useEffect often
// In general they can use other hooks within them
import { useState, useEffect } from 'react';

// Begin your custom hook with the word "use"
// It takes as many args as you need
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Don't forget to return a function to unsubscribe from whatever is going on
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  // Returns the key result to the caller of the hook
  return isOnline;
}

We can pass dynamic state to our custom hooks

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

3. State closure issues

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

  function handleAlertClick() {
    // This will show count numbers that may be way out of date!
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

Fix by recreating the interval

function WatchCount() {
  const [count, setCount] = useState(0);
  useEffect(function() {
    const id = setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
    return function() {
      clearInterval(id);
    }
  }, [count]);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}

4. Lazy Loading

// We import Suspense, which must wrap lazy component
import React, { Suspense } from 'react';

// React.lazy(() => import("x")) won't load until necessary
// fallback is a loading indicator
const OtherComponent = React.lazy(() => import('./OtherComponent'));

<Suspense fallback={<div>Loading...</div>}>
  <OtherComponent />
</Suspense>

Glimmer problem

<Tabs onTabSelect={handleTabSelect} />
<Suspense fallback={<Glimmer />}>
    {tab === 'photos' ? <Photos /> : <Comments />}
 </Suspense>

When switching tab to comments the glimmer will show. You want the OLD UI (photos) to show until comments load, so do this

const handleTabSelect  = () {
  React.startTransition(() => {
    setTab(tab);
  });
}
import React, { Suspense, lazy } from 'react';
// Notice we grab Router, Routes (wraps multiple routes), and Route (takes path and page/screen component)
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Notice how it is wrapped in lazy
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </Router>
)

5. React.Memo

When a component is wrapped in React.memo(), React renders the component and memoizes the result. Before the next render, if the new props are the same, React reuses the memoized result skipping the next rendering. This is instead of rendering both versions and comparing

export function Movie({ title, releaseDate }) {
  return (
    <div>
      <div>Movie title: {title}</div>
      <div>Release date: {releaseDate}</div>
    </div>
  );
}
export const MemoizedMovie = React.memo(Movie);

6. Commenting in JSX

{/* Don't forget to use error  */}
{isError && <div>Something went wrong ...</div>}

7. Web and web gotchas

  1. Don't forget e.target.value onChange vs. direct value
  2. Don't forget it's input.onChange
  3. Don't forget it's form .onSubmit
function App() {
  const [search, setSearch] = useState("");
  const [text, setText] = useState("");
  const [selectedValue setSelectedValue] = useState("lime");

  return (
    <form onSubmit={(e) => e.preventDefault() }>
      <input
        className="input"
        type="text"
        // We control the value for this input via React,
        // not html
        value={search}
        placeholder="Search Here"
        onChange={(e) => setSearch(e.target.value)}
      />
      <textarea value={text} onChange={(e) => setText(e.target.value)}></textarea>
      <select value={selectedValue} onChange={(e) => setSelectedValue(e.target.value)}>
            <option value="grapefruit">Grapefruit</option>
            <option value="lime">Lime</option>
            <option value="coconut">Coconut</option>
            <option value="mango">Mango</option>
          </select>
    </form>
  );
}

Using Local Storage

import * as React from 'react';

const useLocalStorage = (storageKey, fallbackState) => {
  const [value, setValue] = React.useState(
    JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState
  );

  React.useEffect(() => {
    localStorage.setItem(storageKey, JSON.stringify(value));
  }, [value, storageKey]);

  return [value, setValue];
};


const App =  () => {
  const [isOpen, setOpen] = useLocalStorage('is-open', false);

  const handleToggle = () => {
    setOpen(!isOpen);
  };
}

8. Error Boundaries

// Sadly must use classes still

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    // The main bit of state is {hasError}, defaulting to fase
    this.state = { hasError: false };
  }

  // This is also necessary in order that the next render
  // will show the fallback UI
  static getDerivedStateFromError(error) {

    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  // NB: Use this to log to sentry
  componentDidCatch(error, errorInfo) {
    // Something like this
    Sentry.reportException(error)
  }

  render() {
    if (this.state.hasError) {
      return <h4>Something went wrong</h4>
    }

    // Happy case: show the regular children of the ErrorBoundary
    return this.props.children;
  }
}

The error boundary in use:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Limitations - does not handle errors that are thrown outside of the boundary (obviously) - does not handle errors from async code (e.g. setTeimout, requestAnimationFrame)

9. General best practices

import React, { useState } from "react";

export const TestMemo = () => {
  const [userName, setUserName] = useState("faisal");
  const [count, setCount] = useState(0);

  const increment = () => setCount((count) => count + 1);

  return (
    <>
      // This will get rendered every time count changes... what a waste
      <ChildrenComponent userName={userName} />
      <button onClick={increment}> Increment </button>
    </>
  );
};

// The fix: const ChildrenComponent = React.useMemo(({userName}) => ... )
const ChildrenComponent =({ userName }) => {
  console.log("rendered", userName);
  return <div> {userName} </div>;
};

10. Refs

One use case is to avoid infinite rendinering

// This will render infinitely... the renderCount state constantly changes
const App() {
   const [renderCount, setRenderCount] = useState(0)

   useEffect(() => {
     // WARNING: I don't think access to old state is possible anymore...
     setRenderCount(prevRenderCount => prevRenderCount + 1)
   })


// Instead this would work
    betterRenderCount = useRef(0)
   useEffect(() => {
     // WARNING: I don't think access to old state is possible anymore...
     betterRenderCount.current += 1
   })

   return (
   <>
     <p>{renderCount}</p>
   </>
   )
}

OK so you can also use useRef to access a DOM element rendered as a child in your component. Here is how

const App() {
  const [name, setName] = useState("someName")
   const inputRef = useRef

  const focusInput = () {
    inputRef.current.focus()
    }

   return (
   <>
    <input value={name} ref={inputRef} ..../>
     <button onClick={focusInput}/>
     <p>my name is {name}</p>
   </>
   )
}

Now if you console.log(inputRef.current) you will see the <input....>

Watch out though:

if you go and change data in in the refed input e.g. inputRef.current.value to be something like "FOOO", then you'll see that value for the input.... but you won't see that value propogated throughout the other bits of your application i.e. the p tag with nameis not updated. So it would be better to use the proper useState for this stuff and let things progpogate

Another usecase of refs is to store the previous value of your state

const prevName = useRef()
useEffect(() => {
  prevName.current = name
}, [name])

How to access previous values of state

function Counter() {
  const [count, setCount] = useState(0);
  // We create a custom hook and pass in a useState holder
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}

function usePrevious(value) {
  // We create in ref in our hook
  const ref = useRef();
  useEffect(() => {
    // in a useEffect call, we set the current
    // to the arg passed into usePrevious
    ref.current = value;
  }, [value]);

  // MAGIC BIG: This bit returns BEFORE the useEffect code which
  // updates the value async. Thus the return value of usePrevious
  // will be out of date due to useEffect taking so long. But
  // by the time we call the hook again, the ref will have the now
  // current value.
  return ref.current;
}

Accessing a child element from the DOM

function TextInputWithFocusButton() {
  // create a ref with useRef(null)
  const inputEl = useRef(null);
  // Have some functionality you want to do with this referred item
  const onButtonClick = () => {
    // Now got off and do
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      // Don't forget to pass this ref into target
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

Or more elegantly

const useFocus = () => {
    const htmlElRef = useRef(null)
    const setFocus = () => {htmlElRef.current &&  htmlElRef.current.focus()}

    return [ htmlElRef, setFocus ]
}

function TextInputWithFocusButton() {
  const [inputEl, onButtonClick] = useFocus()

  };
  return (
    <>
      // Don't forget to pass this ref into target
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

Forward refs

Ref forwarding is a feature that lets some components take a ref they receive, and pass it further down to a child.

Basically here we create a ref at the top-level, pass it to our custom ButtonElement, which in turn passes it down to a <button> bit of HTML that it renders

// Notice how the component is built with React.forwardRef(props, ref) => ()
// instead of (props) => {}
const ButtonElement = React.forwardRef((props, ref) => (
  <button ref={ref} className="CustomButton">
    {props.children}
  </button>
));

// Create ref to the DOM button:
const ref = React.createRef();
<ButtonElement ref={ref}>{'Forward Ref'}</ButtonElement>

11. HOC (Higher Order Components)

Concretely, a higher-order component is a function that takes a component and returns a new component.

const EnhancedComponent = higherOrderComponent(WrappedComponent);

Examples: - Redux - const ConnectedMyThing = connect({age: state.age}, {setAge: () => dispatch({ type: "INCREMENT"})})(MyThing)

Here is a use-case of where you might use one. Say you have two components that both need to subscribe to something

const UnstyleComponent = () => {
  return (
    <>
      <p>My name is jack</p>
    </>
  )
}

// Notice how it accepts a component as its first argument, followed by other
// arbitrary arguments
const withStyle = (WrappedComponent, className) => (
  // Notice how it returns a component... and that it needs a props argument
  (props) => (
     <div className={className}>
        <WrappedComponent {...props} />
     </div>
   )
)

const StyledComponent = withStyle(UnstyleComponent, "blue-background")

Alternatively you might pass in more advanced functionality like functions


// THIS CODE MAY NOT ACTUALLY WORK -- just a demo
const withSubscription = (WrappedComponent, dataGrabber) => {
  (props) => {
    const [data, setData] = useState([])
    useEffect({
      setData(dataGrabber)
    }, [])

    return <WrappedComponent data={data}/>

  }
}

const BlogPostWithSubscription = withSubscription(
  // Different uses of this HOC might take different
  // component here, and different functions to grab the data
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

Appendix - Typical problem: Fetching data

function SomeComponent() {
  // Define some error catching stuff
 const [isError, setIsError] = useState(false);
 const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // define an async function here
    const fetchData = async () => {
      // Reset error at start of fetch
      setIsError(false);
      // Reset loading at start of fetch
      setIsLoading(true);

      const fetchParams = {
        method: "GET",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          "X-Auth-Token": token,
        },
      };

      // use a try block, since this shit can fail
      try {
        const result = await fetch(url, fetchParams).then(response => {
          // Maybe do something with http status
            const { status } = response;
            response.json()
        }).then(response=>
          setData(response);
        )

      } catch (error) {
        // set is error if catch
        setIsError(true);
      }

      // don't forget to reset loading
      setIsLoading(false);
    };

    // call that function
    fetchData();
    // Make dependent on user input as it comes from setState
  }, [query]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button"
        onClick={() => setQuery(`somesite:?query${query}`)}
      >
        Search
      </button>

        {/* Don't forget to use error  */}
        {isError && <div>Something went wrong ...</div>}
      <Fragment/>
      )
}

Appendix 2: Random small tips

Prefer returning objects from hooks

Return objects from custom hooks instead of lists

I returned

return [ setCanGoBack, handleBackButtonPress ]

But then the caller mis-labelled the functions

const [handleBackButtonPress, setCanGoBack] = useMyHook();

This mixup would not be possible had I returned an object.