Goodbye, useEffect

David Khourshid

@davidkpiano · stately.ai

:-)

)-':

:-/

[]);

useEffect(() => {

  // DANGER ZONE

}, []);

useImperativeHandle()
useEffect()
useEff this

is not for effects*

useEffect()

🤔

componentDidMount

componentDidUpdate

componentWillUnmount

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

useEffect is not a lifecycle hook.

https://twitter.com/tlakomy/status/1501574622839463936

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

useEffect is not a state setter.

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

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

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

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

No dependency array!

Dependency array

useEffect(() => {
  
  /* do this effect */

}, [/* whenever something here changes */])

"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 no reason concurrent rendering.

Dependency array

useEffect(() => {
  doSomething();
  
  return () => cleanup();
}, [whenThisChanges]);
useEffect(() => {
  if (foo && bar && (baz || quo)) {
    doSomething();
  } else {
    doSomethingElse();
  }
  
  // oops, forgot the cleanup
}, [foo, bar, baz, quo]);
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.

Where do
effects go?

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

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

???

No side-effects in render

useEffect (awkward)

Outside the component?

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

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

×2

React 18 runs effects
twice on mount

(in strict mode)

effect

(╯°□°)╯︵ ┻━┻

Mount

cleanup

┬─┬ノ( º _ ºノ)

Unmount (simulated)

effect

(╯°□°)╯︵ ┻━┻

Remount

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]);
useEffect(() => {
  const handler = (event) => {
    setPointer({ x: event.clientX, y: event.clientY })
  };
  
  elRef.current.addEventListener('pointermove', handler);
  
  return () => {
    elRef.current.removeEventListener('pointermove', handler);
  }
}, []);

Action effects

Activity effects

"Fire-and-forget"

Synchronized

Activity effects

Synchronized

Unmount

Remount

Where do action effects go?

Event handlers.

Sorta.

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

someEffect

someEffect

someEffect

someEffect

Effects happen outside
of rendering.

<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, dispatch] = useFormReducer();

<form onSubmit={event => {
  dispatch(event);
}}>
  {/* ... */}
</form>

UI is a function of state

(state, event) => nextState
effects...?
(state) => UI

When do effects happen?

State transitions.

Always.

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

(state, event) => (nextState,       )
effects
(state, event) => nextState

🍵

idle

loading

LOAD / fetchData

(idle, LOAD) => (loading, [         ])
fetchData

idle

loading

LOAD / fetchData

success

RESOLVE / assign

RELOAD / fetchData

idle

loading

 

LOAD

success

RELOAD

entry / fetchData

exit / logTelemetry

RESOLVE / assign

  • ➡️ Transition actions
  • 📥 Entry actions
  • 📤 Exit actions
import { useState, useCallback } from 'react';

function useSpicyReducer(reducer, initialState, executeEffect) {
  const [state, setState] = useState(initialState);

  const spicyDispatch = useCallback(
    (event) => {
      // Calculate next state
      const nextState = reducer(state, event);
      
      // Execute effect based on transition
      executeEffect(state, event, nextState);
      
      // Commit next state
      setState(nextState);
    },
    [reducer, state, executeEffect]
  );

  return [state, spicyDispatch];
}

⬅ Exercise left to reader

Where do action effects go?

Event handlers.

In state transitions.

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

Effects with external stores

useSyncExternalStore()

store
store.whatever(...)

sync

interact

Component

import { useSyncExternalStore } from 'react';
import { someStore } from './somewhere';

function Component() {
  const state = useSyncExternalStore(
    someStore.subscribe,
    someStore.getSnapshot);
  
  return (
    <button onClick={() => {
      someStore.dispatch({ type: 'someEvent' })
    }>
      Click me
    </button>
  );
}
import { createMachine, interpret } from 'xstate';
import { useMachine } from '@xstate/react';

const machine = createMachine({
  // ...
});

const Wizard = () => {
  const [state, send] = useMachine(machine);
  
  return (
    <form onSubmit={() => send('SUBMIT')}>
      {state.matches('first') && <FirstStep />}
      {state.matches('second') && <SecondStep />}
      {state.matches('review') && <ReviewStep />}
      {state.matches('submitting') && <Submitting />}
    </form>
  );
}
⬅ useSyncExternalStore()
import { createMachine, interpret } from 'xstate';
import { useMachine } from '@xstate/react';

const machine = createMachine({
  initial: 'first',
  states: {
    first: {
      entry: doSomething,
      // ...
    },
    // ...
    submitting: {
      invoke: {
        src: (context) => submitForm(context.data),
        onDone: {
          actions: logAnalytics,
          target: 'submitted'
        }
      }
    },
    submitted: {
      // ...
    }
  }
});

Entry + exit actions

Invocations (activities)

Transition actions

npm i xstate

State management

State orchestration

  • State store
  • State updates
  • Subscriptions
  • Event dispatching
  • Multiple stores
  • Communication
  • Effect management
  • Finite states + transitions

Execute on mount, only once

const executedRef = useRef(false);

useEffect(() => {
  if (executedRef.current) { return; }
  
  doSomething();
  
  executedRef.current = true;
}, [/* ... */]);
false
true

🚩

Fetch-on-render 

Fetching data

Render-as-you-fetch with Suspense.

(sorry, useSWR & useQuery)

is a lie.

Fetching data with Suspense

function Component() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    let canceled = false;
    
    fetch('some/resource').then(data => {
      if (canceled) return;
      setData(data);
    });
    
    return () => { canceled = true }
  }, []);

  return // ...
}
function Component() {
  const data = someResource.read(); // might suspend

  return // ...
}

Fetching data 

function Component() {
  const [isSubmitting, setIsSubmitting]
    = useState(false);
  const [error, setError] = useState(null);

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

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

      setIsSubmitting(true);
    }}>
     {/* ... */}
    </form>
  );
}
function Component() {
  const [isSubmitting, setIsSubmitting]
    = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    if (isSubmitting) { return; }

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

    setIsSubmitting(true);
  }, [isSubmitting]);

  return (
    <form onSubmit={event => {
      setIsSubmitting(true);
    }}>
     {/* ... */}
    </form>
  );
}
function Component() {
  const [isSubmitting, setIsSubmitting]
    = useState(false);
  const [error, setError] = useState(null);

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

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

      setIsSubmitting(true);
    }}>
     {/* ... */}
    </form>
  );
}
import { useFormStore } from './somewhere';

function Component() {
  const [state, send] = useFormStore();

  return (
    <form onSubmit={event => {
      send(submitEvent(event));
    }}>
     {/* ... */}
    </form>
  );
}

Effects are state management.

Don't put side-effects aside.

(state, event) => (nextState,       )
effects

useEffect is for synchronization

useEffect is for synchronization

State transitions trigger effects

useEffect is for synchronization

State transitions trigger effects

Effects go in event handlers

useEffect is for synchronization

State transitions trigger effects

Effects go in event handlers

Render-as-you-fetch (suspense)

useEffect is for synchronization

State transitions trigger effects

Effects go in event handlers

Render-as-you-fetch (suspense)

Model effects with state machines

Thank you, Reactathon!

Goodbye, useEffect

By David Khourshid

Goodbye, useEffect

  • 1,294