Using useEffect effectively

David Khourshid · @davidkpiano

stately.ai 

React Advanced 2022

[]

old class component example

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null,
    };
  }

  componentDidMount() {
    fetchData('/some/data').then((res) => {
      this.setState({ data: res });
    });
  }
}
const [data, setData] = useState(null);

useEffect(async () => {
  const res = await fetchData('/some/data');
  
  setData(res);
});
const [data, setData] = useState(null);

useEffect(() => {
  fetchData('/some/data')
    .then(res => {
      setData(res)
    });
});
const [data, setData] = useState(null);

useEffect(() => {
  fetchData('/some/data')
    .then(res => {
      setData(res)
    });
}, []);
const [data, setData] = useState(null);

useEffect(() => {
  fetchData('/some/data/' + id)
    .then(res => {
      setData(res)
    });
}, [id]);

id: 1

id: 2 (cached)

const [data, setData] = useState(null);

useEffect(() => {
  let isCanceled = false;
  
  fetchData('/some/data/' + id)
    .then(res => {
      if (!isCanceled) { return; }
      setData(res)
    });
  
  return () => { isCanceled = true; }
}, [id]);

Effects execute twice on mount (in strict mode)!

cleanup

┬─┬ノ( º _ ºノ)

Unmount (simulated)

effect

(╯°□°)╯︵ ┻━┻

Remount

effect

(╯°□°)╯︵ ┻━┻

Mount

useEffect()
useDefect()
useFoot(() => {
  setGun(true); 🦶🔫
});

How does useEffect work?

It doesn't.

.. for many effects.

  • React 18 double exec 🚶‍♂️👈 🚪
  • [long, dependency, arrays]
  • if (!simple && conditionals)
  • Missing () => { cleanup functions }
  • Ad-hoc setState('calls')

Effects let you specify side effects that are caused by rendering itself, rather than by a particular event.

"Effects"

"events"

Activity effects

Action effects

Synchronization with
activity effects.

What is useEffect() for?

useEffect(() => {
  const handler = (event) => {
    // do something
  }
  
  window.addEventListener('resize', handler);
  
  return () => {
    window.removeEventListener('resize', handler);
  }
}, []);

Where do action
effects go?

function Component(props) {
  
  useEffect(() => {
    // ...
    
    return () => {/* ... */}
  }, [/* ... */]);
  
  
  // ...

  
  return (
    <div>{/* ... */}</div>
  );
}

???

No side-effects in render

useEffect (awkward)

Outside the component?

Event handlers.

Sorta.

<form onSubmit={event => {
  // 💥 side-effect!
  submitData(event);
}}>
  {/* ... */}
</form>

Where do action
effects go?

As close as possible
to the event

eventHandler()

someEffect

someEffect

someEffect

someEffect

Action effects happen
outside of rendering.

const FancyInput = ({ isFocused }) => {
  const ref = useRef();

  useEffect(() => {
    if (isFocused) {
      ref.current?.focus();
    }
  }, [isFocused]);

  return (
    <div>
      {/* If you blur, isFocused={true} is a lie */}
      <input ref={ref} />
    </div>
  );
};

const App = () => {
  // Focus state is duplicated (and possibly wrong)
  const [inputFocused, setInputFocused] = useState(false);

  return (
    <main>
      <FancyInput isFocused={inputFocused} />
      
      {/* What if there was another focused input? */}
      {/* Impossible state: multiple focused elements? */}
      {/* <FancyInput isFocused={true} /> */}

      <button
        onClick={() => {
          setInputFocused(true);
        }}
      >
        What the focus
      </button>
    </main>
  );
}
const FancyInput = forwardRef(function FancyInput(props, ref) {
  return (
    <div>
      <input ref={ref} />
    </div>
  );
});

function App() {
  const inputRef = useRef();

  // Focusing is a fire-and-forget effect that does not need useEffect
  function handleButtonClick() {
    inputRef.current?.focus();
  }

  return (
    <main>
      <FancyInput ref={inputRef} />

      <button onClick={handleButtonClick}>Hocus focus</button>
    </main>
  );
}

"Declarative"

"Imperative"

  • When something happens,
  • execute this effect.
  • When something happens,
  • it will cause the state to change
  • and depending on which parts of the state changed,
  • this effect should be executed,
  • but only if some condition is true.
  • And React may execute it again
  • for some future reason.
  • But only in Strict mode!
  • Which you shouldn't disable
  • for some future reason.

Dependency array

beta.reactjs.org

You don't need useEffect for

transforming data.

useEffect() ➡️ useMemo()
function Cart() {
  const [items, setItems] = useState([]);
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setTotal(
      items.reduce((currentTotal, item) => {
        return currentTotal + item.price;
      }, 0)
    );
  }, [items]);

  // ...
}

function Cart() {
  const [items, setItems] = useState([]);
  const total = useMemo(
    () =>
      items.reduce((currentTotal, item) => {
        return currentTotal + item.price;
      }, 0),
    [items]
  );

  // ...
}
function Cart() {
  const [items, setItems] = useState([]);
  const total = items.reduce((currentTotal, item) => {
    return currentTotal + item.price;
  }, 0);

  // ...
}




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

function Example() {
  const [value, setValue] = useState("");
  const [count, setCount] = useState(-1);
  
  useEffect(() => {
    setCount(count + 1)
  });
  
  const onChange = ({ target }) => setValue(target.value);
  
  return (
    <div>
      <input type="text" value={value} onChange={onChange} />
      <div>Number of changes: {count}</div>
    </div>
  );
}

You don't need useEffect for

communicating with parents.

useEffect() ➡️ eventHandler()
function Product({ onOpen, onClose }) {
  const [isOpen, setIsOpen] = useState(false);

  useEffect(() => {
    if (isOpen) {
      onOpen();
    } else {
      onClose();
    }
  }, [isOpen]);

  return (
    <div>
      <button
        onClick={() => {
          setIsOpen(!isOpen);
        }}
      >
        Toggle quick view
      </button>
    </div>
  );
}

click

state change

effect

function Product({ onOpen, onClose }) {
  const [isOpen, setIsOpen] = useState(false);

  function toggleView() {
    const nextIsOpen = !isOpen;
    setIsOpen(!isOpen);
    if (nextIsOpen) {
      onOpen();
    } else {
      onClose();
    }
  }

  return (
    <div>
      <button onClick={toggleView}>Toggle quick view</button>
    </div>
  );
}

click

effect

function useToggle({ onOpen, onClose }) {
  const [isOpen, setIsOpen] = useState(false);
  
  function toggler() {
    const nextIsOpen = !isOpen;
    setIsOpen(nextIsOpen);
    
    if (nextIsOpen) {
      onOpen();
    } else {
      onClose();
    }
  }
  
  return [isOpen, toggler];
}

function Product({ onOpen, onClose }) {
  const [isOpen, toggler] = useToggle({ onOpen, onClose });

  return (
    <div>
      <button onClick={toggler}>Toggle quick view</button>
    </div>
  );
}

You don't need useEffect for

subscribing to external stores.

useEffect() ➡️ useSyncExternalStore()
function Store() {
  const [isConnected, setIsConnected] = useState(true);

  useEffect(() => {
    const sub = storeApi.subscribe(({ status }) => {
      setIsConnected(status === 'connected');
    });

    return () => {
      sub.unsubscribe();
    };
  }, []);

  // ...
}

function Product({ id }) {
  const isConnected = useSyncExternalStore(
    // subscribe
    storeApi.subscribe,
    // get snapshot
    () => storeApi.getStatus() === 'connected',
    // get server snapshot
    true
  );

  // ...
}


You don't need useEffect for

fetching data.

useEffect() ➡️ renderAsYouFetch()
import { getItems } from '../storeApi';

function Store() {
  const [items, setItems] = useState([]);

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

    getItems().then((data) => {
      if (isCanceled) return;

      setItems(data);
    });

    return () => {
      isCanceled = true;
    };
  });

  // ...
}

import { useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/node";
import { getItems } from '../storeApi';

export const loader = async () => {
  const items = await getItems();
  
  return json(items);
}

export default function Store() {
  const items = useLoaderData();
  
  // ...
}
import { getItems } from '../storeApi';

function Store({ items }) {
  // ...
}

export async function getServerSideProps() {
  const items = await getItems();

  return { props: { items } }
}

export default Store;
import { getItems } from '../storeApi';
import { useQuery, useQueryClient } from 'react-query';

function Store() {
  const queryClient = useQueryClient()
  // ...
  
  return (
    <button onClick={() => {
      queryClient.prefetchQuery('items', getItems);
    }}>
      See items
    </button>
  );
}

function Items() {
  const { data } = useQuery('items', getItems);
  
  // ...
}
useEffect()
useQuery()
useSWR()
use()

⁉️

This enables React developers to access arbitrary asynchronous data sources with Suspense via a stable API.

const fetchPost = cache(async (id) => {
  // ...
})

function Post({ id }) {
  const post = use(fetchPost(id))
  return (
    <article>
      <h1>{post.title}</h1>
      <PostContent post={post} />
    </article>
  );
}

🏃‍♀️ Race conditions

⏪ No instant back button

🔍 No initial HTML content

🌊 Chasing waterfalls

Fetching in useEffect problems

You don't need useEffect for

initializing global singletons.

useEffect() ➡️ justCallIt()
function Store() {
  useEffect(() => {
    storeApi.authenticate();
  }, []);

  // ...
}

☝️ This will run twice!

function Store() {
  const didAuthenticateRef = useRef();

  useEffect(() => {
    if (didAuthenticateRef.current) {
      return;
    }

    storeApi.authenticate();

    didAuthenticateRef.current = true;
  }, []);
} 

🚩

Ref flag

let didAuthenticate = false;

function Store() {
  useEffect(() => {
    if (didAuthenticate) {
      return;
    }

    storeApi.authenticate();

    didAuthenticate = true;
  }, []);
} 

Potential wasted
render

storeApi.authenticate();

function Store() {
  // ...
} 
if (typeof window !== 'undefined') {
  storeApi.authenticate();
}

function Store() {
  // ...
} 
function renderApp() {
  if (typeof window !== 'undefined') {
    storeApi.authenticate();
  }

  appRoot.render(<Store />);
}

renderApp();

You don't need useEffect for

handling user events.

useEffect() ➡️ eventHandler()
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
  if (!isLoading || !formData) { return; }
  let isCanceled = false;
  
  submitData(event)
    .then(() => {
      if (isCanceled) { return; }
      setIsLoading(false);
    })
    .catch(err => {
      setIsLoading(false);
      setError(err);
    });
  
  return () => {
    isCanceled = true;
  }
}, [isLoading, formData]);

<form onSubmit={event => {
  setIsLoading(true);
  setFormData(event);
}}>
  {/* ... */}
</form>

<form onSubmit={event => {
  // 💥 side-effect!
  submitData(event);
}}>
  {/* ... */}
</form>
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

<form onSubmit={event => {
  if (isLoading) { return; }

  // 💥 side-effect!
  submitData(event)
    .then(() => { setIsLoading(false) })
    .catch(err => {
      setIsLoading(false);
      setError(err);
    });

  setIsLoading(true);
}}>
  {/* ... */}
</form>
const [state, send] = useCheckoutForm();

<form onSubmit={event => {
  send({ type: 'submit', data: event });
}}>
  {/* ... */}
</form>
const videoRef = useRef();
const [isPlaying, setIsPlaying] = useState(false);

useEffect(() => {
  if (isPlaying) {
    videoRef.current?.play();
  } else {
    videoRef.current?.pause();
  }
}, [isPlaying]);

useEffect(() => {
  if (isPlaying) {
    const handler = () => {
      setIsPlaying(false);
    };

    videoRef.current.addEventListener("ended", handler);
    return () => {
      videoRef.current?.removeEventListener("ended", handler);
    };
  }
}, [isPlaying]);

useEffect(() => {
  if (isPlaying) {
    const handler = (event) => {
      if (event.key === "Escape") {
        setIsPlaying(false);
      }
    };

    window.addEventListener("keydown", handler);

    return () => {
      window.removeEventListener("keydown", handler);
    };
  }
}, [isPlaying]);

mini

full

toggle / playVideo

toggle / pauseVideo

switch (state) {
  case 'mini':
    if (event.type === 'toggle') {
      playVideo();
      return 'full';
    }
    break;
  case 'full':
    if (event.type === 'toggle') {
      pauseVideo();
      return 'mini';
    }
    break;
  default:
    break;
}

Where do action effects go?

Event handlers.

In state transitions.

...which happen to be executed at the same time as event handlers.

useEffect is for synchronization

useEffect is for synchronization

Activity effects go in useEffect

useEffect is for synchronization

Activity effects go in useEffect

Action effects go in event handlers

useEffect is for synchronization

Activity effects go in useEffect

Action effects go in event handlers

Render-as-you-fetch 

useEffect is for synchronization

Activity effects go in useEffect

Action effects go in event handlers

Render-as-you-fetch

State transitions trigger effects

useEffect(() => {
  // my talk

  return () => {
  }
}, [])

Using useEffect effectively

By David Khourshid

Using useEffect effectively

  • 1,242