Skip to content

Instantly share code, notes, and snippets.

@naifen
Last active March 21, 2022 17:08
Show Gist options
  • Save naifen/60d9b607b6f5f70dd1449c52b9311f4b to your computer and use it in GitHub Desktop.
Save naifen/60d9b607b6f5f70dd1449c52b9311f4b to your computer and use it in GitHub Desktop.

React.js learning notes

React functional componenets and hooks

Use React memo API to prevent re-renders

In this example, every time you type something in the input field, the App component updates its state and triggers re-renders of the Count component. Use React memo in React Function Components to prevent a rerender when the incoming props of this component haven't changed:

import React, { useState, memo } from 'react';

const App = () => {
  const [count, setCount] = useState(0);
  const handleChange = event => setGreeting(event.target.value);

  return (
    <div>
      <input type="text" onChange={handleChange} />
      <Count count={count} />
    </div>
  );
};

// Note the use of memo API
const Count = memo(({ count }) => {
  console.log('Does it (re)render?');
  return <h1>{count}</h1>;
});

export default App;

Now, the Count component doesn't update anymore when the user types something into the input field. Only the App component re-renders. This performance optimization shouldn't be used as default though.

Use forwardRef to pass ref to a child component

It's NOT recommended to pass a ref from a Parent Component to a Child Component and that's why the assumption has always been: React Function Components cannot have refs. However, if you need to pass a ref to a Function Component -- because you have to measure the size of a function component's DOM node, for example, or like in this case to focus an input field from the outside -- you can forward the ref:

import React, {
  useEffect,
  createRef,
  forwardRef,
} from 'react';

const App = () => {
  const handleChange = event => setGreeting(event.target.value);
  const ref = createRef();

  useEffect(() => ref.current.focus(), []);

  return (
    <div>
      <Input value={greeting} handleChange={handleChange} ref={ref} />
    </div>
  );
};

// Note the use of forwardRef in Input component
const Input = forwardRef(({ value, handleChange }, ref) => (
  <input
    type="text"
    value={value}
    onChange={handleChange}
    ref={ref}
  />
));

export default App;

Force update with useReducer

Similar to forceUpdate API

Both useState and useReducer Hooks bail out of updates if the next value is the same as the previous one. Mutating state in place and calling setState will not cause a re-render.

const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
  forceUpdate();
}

Use with caution

useState is ASYNC

Also note that we are using a callback function within the setCount state function to access the current state. Since setter functions from useState are executed asynchronously by nature, you want to make sure to perform your state change on the current state and not on any stale state.

const handleIncrement = () =>
  setTimeout(
    () => setCount(currentCount => currentCount + 1),
    1000
  );

const handleDecrement = () =>
  setTimeout(
    () => setCount(currentCount => currentCount - 1),
    1000
  );

Instance variable in functional component with useRef hook

The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class component.

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      console.log("passed as reference to intervalRef object's current prop");
    });
    intervalRef.current = id;

    return () => {
      clearInterval(intervalRef.current);
    };
  });

  // the above interval can be cleared from an event handler in the same component
  function handleCancelClick() {
    clearInterval(intervalRef.current);
  }

  // ...
}

Use useCallback hook to prevent re-renders

Since javascript compares equality by reference, the function you create the first time a component renders will be different than the one created in subsequent renders. If you try passing a function as props or state, this means that it will be treated as a prop change every single time. By wrapping it in useCallback, React will know that it's the same function.

A strong use-case here to avoid child component re-renders In this example, every time this component renders, it will also trigger a whole re-render of the Button component because the removeFromCart function is unique every time.

const dispatch = useDispatch();

const removeFromCart = () => dispatch(removeItem(product.id));

return (
  <Button onClick={removeFromCart}>Delete</Button>
);

By creating the removeFromCart function with useCallback and pass product.id as a dependency, Button will only re-render when our product ID changes. This is similar to the shouldComponentUpdate API.

const removeFromCart = React.useCallback(() => {
  dispatch(removeItem(product.id))
}, [product.id]);

More on useCallback and useMemo for performance optimization

Here's a good article: https://kentcdodds.com/blog/usememo-and-usecallback

Javascript has a unique referential equality, for example:

true === true // true
false === false // true
1 === 1 // true
'a' === 'a' // true
{} === {} // false
[] === [] // false
() => {} === () => {} // false
const z = {}
z === z // true

Because of this, the Foo component is re-rendered every time whether if the dependency is changed, because javascript thinks option constant is a new one in every render.

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

  // useEffect is going to do a referential equality check on options between
  // every render, and thanks to the way JavaScript works, options will be new
  // every time so when React tests whether options changed between renders it'll
  // always evaluate to true, meaning the useEffect callback will be called
  // after every render rather than only when bar and baz change.
  React.useEffect(() => {
    buzz(options)
  }, [options])

  return <div>foobar</div>
}

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

// passing bar baz as dependencies still causing the same problem when they are
objects/array
function Foo({bar, baz}) {
  React.useEffect(() => {
    const options = {bar, baz}
    buzz(options)
  }, [bar, baz])

  return <div>foobar</div>
}

//but we can fix this by using useCallback useMemo hooks
function Blub() {
  const bar = React.useCallback(() => {}, [])
  const baz = React.useMemo(() => [1, 2, 3], [])
  return <Foo bar={bar} baz={baz} />
}

in the following example, Every time you click on either of those buttons, the DualCounter's state changes and therefore re-renders which in turn will re-render both of the CountButtons.

function CountButton({onClick, count}) {
  return <button onClick={onClick}>{count}</button>
}

function DualCounter() {
  const [count1, setCount1] = React.useState(0)
  const increment1 = () => setCount1(c => c + 1)

  const [count2, setCount2] = React.useState(0)
  const increment2 = () => setCount2(c => c + 1)

  return (
    <>
      <CountButton count={count1} onClick={increment1} />
      <CountButton count={count2} onClick={increment2} />
    </>
  )
}

To fix the CountButton re-renders we can use the memo API. Now React will only re-render CountButton when its props change

const CountButton = React.memo(function CountButton({onClick, count}) {
  return <button onClick={onClick}>{count}</button>
})

As methioned above, wrap the click handlers in useCallback API to prevent unecessary re-renders of CountButton components. This is because when DualCounter re-renders, these handler functions will be treated as new, thus trigger re-renders of CountButton.

function DualCounter() {
  const [count1, setCount1] = React.useState(0)
  const increment1 = React.useCallback(() => setCount1(c => c + 1), [])

  const [count2, setCount2] = React.useState(0)
  const increment2 = React.useCallback(() => setCount2(c => c + 1), [])

  return (
    <>
      <CountButton count={count1} onClick={increment1} />
      <CountButton count={count2} onClick={increment2} />
    </>
  )
}

useMemo for expensive computation

Imagine that a function that is computationally expensive, eg compute prime numbers: https://developer.mozilla.org/en-US/docs/Tools/Performance/Scenarios/Intensive_JavaScript

For example we need to render a prime number in a component:

function RenderPrimes({iterations, multiplier}) {
  const primes = calculatePrimes(iterations, multiplier)
  return <div>Primes! {primes}</div>
}

This could be pretty slow for old hardwares, with useMemo, we only need to calculate it once, since the primes constant is memorized.

function RenderPrimes({iterations, multiplier}) {
  const primes = React.useMemo(() => calculatePrimes(iterations, multiplier), [
    iterations,
    multiplier,
  ])
  return <div>Primes! {primes}</div>
}

Data fetch, custom hooks and useReducer

You almost always wanted to provide dependencies(even an empty array so it only run once when component mount initially) to a useEffect hook if you perform a data fetches from external service and set the state:

useEffect(async () => {
  const result = await axios(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  setData(result.data);
}, []); // or something like [keywords]

Because with out the dependencies, the state is set after every data fetch, the component updates and the effect runs again. It fetches the data again, result in a infinit loop.

Another glitch of the above example is, an async callback is passed in useEffect hook directly, we want to avoid this but rather call async functions inside of it

useEffect(() => {
  const fetchData = async () => {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );
    setData(result.data);
  };
  fetchData();
}, []);

We can also PROGRAMMATICALLY triggers the effect hook by providing values as dependencies

const [url, setUrl] = useState(
  'https://hn.algolia.com/api/v1/search?query=redux',
);
useEffect(() => {
  const fetchData = async () => {
    const result = await axios(url);
    setData(result.data);
  };
  fetchData();
}, [url]);

// and we can trigger it like this:
<button
  type="button"
  onClick={() =>
    setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
  }
>

An outstanding tutorial on this topic can be found here: https://www.robinwieruch.de/react-hooks-fetch-data

A more optimized version of the data fetch component using custom reducer can look like the following. Note the use of didCancel flag to not update the component's state withdata from async fetch after component unmounts.

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });
      try {
        const result = await axios(url);
        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };
    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);
  return [state, setUrl];
};

There is a more complex AbortController API to cancel fetch request, it could be an option to abort the fetch request on compoent unmounts, read more here: https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort

React and Typescript

React + Typescript cheatsheet

` https://github.com/typescript-cheatsheets/react-typescript-cheatsheet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment