Goodbye, useEffect

David Khourshid · @davidkpiano

stately.ai 

👋

React Brussels 2022

:-)

)-':

:-/

[]);

useEffect(() => {

  // DANGER ZONE

}, []);

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

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

×2

React 18 runs effects
twice on mount

(in strict mode)

is not for all effects.

useEffect()

🤔

componentDidMount

componentDidUpdate

componentWillUnmount

useEffect(() => {
  // componentDidMount?
}, []);
useEffect(() => {
  // componentDidUpdate?
}, [something, anotherThing]);
useEffect(() => {
  return () => {
    // componentWillUnmount?
  }
}, []);

useEffect is not a lifecycle hook.

Dependency array

useEffect(() => {
  
  doSomething();

}, [whenever, these, things, change])

Effect

"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

useEffect(() => {
  doSomething();
  
  return () => cleanup();
}, [whenThisChanges]);

Ideal

useEffect(() => {
  if (foo && bar && (baz || quo)) {
    doSomething();
  } else {
    doSomethingElse();
  }
  
  // oops, forgot the cleanup
}, [foo, bar, baz, quo]);

Ideal Reality

useEffect(() => {
  if (isOpen && component && containerElRef.current) {
    if (React.isValidElement(component)) {
      ionContext.addOverlay(overlayId, component, containerElRef.current!);
    } else {
      const element = createElement(component as React.ComponentClass, componentProps);
      ionContext.addOverlay(overlayId, element, containerElRef.current!);
    }
  }
}, [component, containerElRef.current, isOpen, componentProps]);
  useEffect(() => {
    if (removingValue && !hasValue && cssDisplayFlex) {
      setCssDisplayFlex(false);
    }
    setRemovingValue(false);
  }, [removingValue, hasValue, cssDisplayFlex]);
const isVisible = useOnScreen(ref)
const { data, error, mutate, size, setSize, isValidating } = useSWRInfinite(
  (...args) => getKey(...args, repo, PAGE_SIZE),
  fetcher
)

const issues = data ? [].concat(...data) : []
const isLoadingInitialData = !data && !error
const isLoadingMore =
      isLoadingInitialData ||
      (size > 0 && data && typeof data[size - 1] === 'undefined')
const isEmpty = data?.[0]?.length === 0
const isReachingEnd = size === PAGE_SIZE
const isRefreshing = isValidating && data && data.length === size

useEffect(() => {
  if (isVisible && !isReachingEnd && !isRefreshing) {
    setSize(size + 1)
  }
}, [isVisible, isRefreshing])

Dependences are the wrong

mental model for effects.

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

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

React 18 runs effects
twice on mount

(in strict mode)

How to fix this?

×2

cleanup

┬─┬ノ( º _ ºノ)

Unmount (simulated)

effect

(╯°□°)╯︵ ┻━┻

Remount

effect

(╯°□°)╯︵ ┻━┻

Mount

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

What is useEffect() for?

Synchronization.

useEffect(() => {
  const sub = createThing(input).subscribe(value => {
    // do something with value
  });
  
  return sub.unsubscribe;
}, [input]);

What is useEffect() for?

Synchronization.

const [itemData, setItemData] = useState(null);

useEffect(() => {
  // Synchronize with external system
  const sub = storeApi.subscribeToItem(itemId, setItemData);
  
  // Subscription disposal
  return sub.unsubscribe;
  
}, [itemId]); // Subscription dependency

Subscriptions can resubscribe
multiple times!

Action effects

Activity effects

"Fire-and-forget"

Synchronized

External
system

Activity effects

Synchronized

Unmount

Remount

Where do action
effects go?

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

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

???

No side-effects in render

useEffect (awkward)

Outside the component?

eventHandler()

someEffect

someEffect

someEffect

someEffect

Action effects happen
outside of rendering.

Where do action effects go?

Event handlers.

Sorta.

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

Event happens

Effect

Event happens

State changes

Effect

State changes

State changes

Effect

Event handler

useEffect

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 = items.reduce((currentTotal, item) => {
    return currentTotal + item.price;
  }, 0);

  // ...
}




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

  // ...
}
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>
  );
}
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>
  );
}
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;
  }, []);
} 
let didAuthenticate = false;

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

    storeApi.authenticate();

    didAuthenticate = true;
  }, []);
} 
storeApi.authenticate();

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

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

  appRoot.render(<Store />);
} 

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);

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

  // 💥 side-effect!
  submitData(event);

  setIsLoading(true);
}}>
  {/* ... */}
</form>
const [isLoading, setIsLoading] = useState(false);

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

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

  setIsLoading(true);
}}>
  {/* ... */}
</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>

Demo time

stately.ai/studio

When do effects happen?

State transitions.

Always.

Middleware, callbacks, sagas, reactions, sinks, monads (?), whenever

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

State transitions trigger effects

useEffect is for synchronization

State transitions trigger effects

Action effects go in event handlers

useEffect is for synchronization

State transitions trigger effects

Action effects go in event handlers

Render-as-you-fetch 

useEffect is for synchronization

State transitions trigger effects

Action effects go in event handlers

Render-as-you-fetch

Model effects with state machines

Thank you React Brussels!

Goodbye, useEffect

By David Khourshid

Goodbye, useEffect

  • 1,151