use-effect-reducer

repository·master·Indexed 21 days ago

https://github.com/davidkpiano/useeffectreducer

A React hook (version 0.7.0) that extends the useReducer pattern by allowing side-effects to be explicitly managed and dispatched from within the reducer function. It provides a mechanism to trigger, stop, and replace effects using an exec function, supporting both inline functions and named effects via an effectsMap for decoupled implementations.

Tokens
3.7K
Snippets
13
Records
14
Agent score
24%

What's inside use-effect-reducer

  1. Manage Effect Lifecycles with Effect Entities

    master

    When you call exec(effect), it returns an effect entity. You can store this entity in your state to gain control over the running effect later.

    Stop an effect

    Use exec.stop(entity) to explicitly stop an effect and run its cleanup function. All running effects are automatically cleaned up when the component unmounts.

    Replace an effect

    Use exec.replace(entity, effect) to stop an existing effect and immediately start a new one. This returns a new effect entity.

    const timerReducer = (state, event, exec) => {
      if (event.type === 'START') {
        // Store the entity in state
        return {
          ...state,
          timer: exec(() => {
            const id = setTimeout(() => { /* ... */ }, 1000);
            return () => clearTimeout(id);
          }),
        };
      } else if (event.type === 'STOP') {
        // Stop using the stored entity
        exec.stop(state.timer);
        return state;
      } else if (event.type === 'LAP') {
        // Replace the existing effect with a new one
        return {
          ...state,
          timer: exec.replace(state.timer, () => doSomeDelay()),
        };
      }
      return state;
    };
  2. Use Named, Parameterized Effects

    master

    Instead of passing inline functions to exec, you can use named effects to make your reducers reusable and decoupled from implementation details.

    1. In the reducer, pass an object to exec({ type: 'effectName', ...params }).
    2. Provide an effectsMap as the third argument to useEffectReducer. This map contains the implementations for each effect type.

    An effect implementation receives three arguments:

    • state: The state at the time exec was called.
    • effect: The effect object passed to exec.
    • dispatch: The dispatch function to send events back to the reducer.
    const fetchEffectReducer = (state, event, exec) => {
      switch (event.type) {
        case 'FETCH':
          // Pass an object describing the effect
          exec({ type: 'fetchFromAPI', user: event.user });
          return { ...state, status: 'fetching' };
        case 'RESOLVE':
          return { status: 'fulfilled', user: event.data };
        default:
          return state;
      }
    };
    
    const Fetcher = () => {
      const [state, dispatch] = useEffectReducer(
        fetchEffectReducer,
        { status: 'idle', user: undefined },
        {
          // Implementation map
          fetchFromAPI: (_, effect, dispatch) => {
            fetch(`/api/users/${effect.user}`)
              .then((res) => res.json())
              .then((data) => {
                dispatch({ type: 'RESOLVE', data });
              });
          },
        }
      );
    
      // ...
    };
  3. Initialize state with Initial Effects

    master

    The second argument to useEffectReducer can be an initialization function instead of a static state object. This function receives exec and returns the initial state. This allows you to trigger effects immediately upon component mount.

    const getInitialState = (exec) => {
      exec({ type: 'fetchData', query: '*' });
      return { data: null };
    };
    
    const [state, dispatch] = useEffectReducer(fetchReducer, getInitialState, {
      fetchData(_, { query }, dispatch) {
        fetch(`/api?${query}`)
          .then((res) => res.json())
          .then((data) => dispatch({ type: 'RESOLVE', data }));
      },
    });
  4. Quick Start with useEffectReducer

    master

    The useEffectReducer hook is similar to useReducer. It takes an effect reducer function as its first argument. The reducer function receives three arguments:

    1. state: The current state.
    2. event: The event dispatched.
    3. exec: A function used to capture effects to be executed. Calling exec returns an effect entity.

    When exec is called within the reducer, the hook ensures the effect is executed within a useEffect lifecycle hook.

    import { useEffectReducer } from 'use-effect-reducer';
    
    const countReducer = (state, event, exec) => {
      switch (event.type) {
        case 'INC':
          exec(() => {
            console.log('Going up!');
          });
    
          return {
            ...state,
            count: state.count + 1,
          };
    
        default:
          return state;
      }
    };
    
    const App = () => {
      const [state, dispatch] = useEffectReducer(countReducer, { count: 0 });
    
      return (
        <div>
          <output>Count: {state.count}</output>
          <button onClick={() => dispatch('INC')}>Increment</button>
        </div>
      );
    };
  5. Type the effect reducer with TypeScript

    master

    Use the EffectReducer<TState, TEvent, TEffect> generic type to provide full type safety for your state, events, and named effects.

    import { useEffectReducer, EffectReducer } from 'use-effect-reducer';
    
    interface User {
      name: string;
    }
    
    type FetchState =
      | { status: 'idle'; user: undefined }
      | { status: 'fetching'; user: User | undefined }
      | { status: 'fulfilled'; user: User };
    
    type FetchEvent =
      | { type: 'FETCH'; user: string }
      | { type: 'RESOLVE'; data: User };
    
    type FetchEffect = {
      type: 'fetchFromAPI';
      user: string;
    };
    
    const fetchEffectReducer: EffectReducer<
      FetchState,
      FetchEvent,
      FetchEffect
    > = (state, event, exec) => {
      switch (event.type) {
        case 'FETCH':
          exec({ type: 'fetchFromAPI', user: event.user });
          return { ...state, status: 'fetching' };
        case 'RESOLVE':
          return { status: 'fulfilled', user: event.data };
        default:
          return state;
      }
    };
  6. useEffectReducer API Reference

    master

    useEffectReducer hook

    Signatures:

    • useEffectReducer(effectReducer, initialState)
    • useEffectReducer(effectReducer, initialState, effectsMap)
    • useEffectReducer(effectReducer, initFunction, effectsMap)

    exec(effect)

    Queues an effect for execution. Can take an inline function or an effect object.

    • const entity = exec(() => { /* side effect */ });
    • const entity = exec({ type: 'namedEffect', payload: 123 });
    • Returns: An effect entity.

    exec.stop(entity)

    Stops the effect represented by the entity and runs its cleanup function.

    exec.replace(entity, effect)

    Stops the existing entity and queues a new effect.

    • Returns: A new effect entity.
  7. Implement an `EffectFunction`

    master

    An EffectFunction is the logic that runs when an effect is started. It receives the current state, the effect configuration object, and the dispatch function. It can optionally return a CleanupFunction to be called when the effect is stopped or replaced.

    Signature: (state, effect, dispatch) => CleanupFunction | void

    type EffectFunction<TState, TEvent extends EventObject, TEffect extends EffectObject<TState, TEvent>> = (
      state: TState,
      effect: TEffect,
      dispatch: Dispatch<TEvent>
    ) => CleanupFunction | void;
  8. Create an effect with `toEffect`

    master

    The toEffect utility allows you to convert a standalone EffectFunction into an Effect object. This is useful for defining effects that are primarily functions but need a specific type identifier for the reducer logic.

    import { toEffect } from 'use-effect-reducer';
    
    const myEffect = (state, effect, dispatch) => {
      // effect logic here
      return () => console.log('cleanup');
    };
    
    const effectObject = toEffect(myEffect);
    // effectObject now has { type: 'myEffect', exec: myEffect }
  9. Define an `EffectReducer` function

    master

    An EffectReducer is a function that determines how the state changes in response to an event and manages side effects.

    Its signature is: (state, event, exec) => nextState

    • state: The current state.
    • event: The event object (must conform to EventObject).
    • exec: An EffectReducerExec object used to trigger, stop, or replace effects within the reducer logic.

    When you call exec(effect) inside the reducer, it queues the effect to be executed after the reducer finishes and the component re-renders.

    type EffectReducer<TState, TEvent extends EventObject, TEffect extends EffectObject<TState, TEvent>> = (
      state: TState,
      event: TEvent,
      exec: EffectReducerExec<TState, TEvent, TEffect>
    ) => TState;
  10. Use the `useEffectReducer` hook

    master

    The useEffectReducer hook is the primary entry point for managing state and side effects in a unified way. It combines the pattern of useReducer with a managed effect system.

    It accepts an effectReducer function, an initialState (which can be a value or a getter function), and an optional effectsMap to map effect types to specific implementations.

    It returns a tuple containing the current state and a dispatch function. The dispatch function can accept either a full EventObject or a simple string representing the event type.

    const [state, dispatch] = useEffectReducer(
      effectReducer,
      initialState,
      effectsMap
    );