• Stars
    star
    791
  • Rank 57,558 (Top 2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 4 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

useReducer + useEffect = useEffectReducer

useEffectReducer

A React hook for managing side-effects in your reducers.

Inspired by the useReducerWithEmitEffect hook idea by Sophie Alpert.

If you know how to useReducer, you already know how to useEffectReducer.

💻 CodeSandbox example: Dog Fetcher with useEffectReducer

Installation

Install it:

npm install use-effect-reducer

Import it:

import { useEffectReducer } from 'use-effect-reducer';

Create an effect reducer:

const someEffectReducer = (state, event, exec) => {
  // execute effects like this:
  exec(() => {/* ... */});

  // or parameterized (better):
  exec({ type: 'fetchUser', user: event.user });

  // and treat this like a normal reducer!
  // ...

  return state;
};

Use it:

// ...
const [state, dispatch] = useEffectReducer(someEffectReducer, initialState, {
  // implementation of effects
});

// Just like useReducer:
dispatch({ type: 'FETCH', user: 'Sophie' });

Isn't this unsafe?

No - internally, useEffectReducer (as the name implies) is abstracting this pattern:

// pseudocode
const myReducer = ([state], event) => {
  const effects = [];
  const exec = (effect) => effects.push(effect);
  
  const nextState = // calculate next state
  
  return [nextState, effects];
}

// in your component
const [[state, effects], dispatch] = useReducer(myReducer);

useEffect(() => {
  effects.forEach(effect => {
    // execute the effect
  });
}, [effects]);

Instead of being implicit about which effects are executed and when they are executed, you make this explicit in the "effect reducer" with the helper exec function. Then, the useEffectReducer hook will take the pending effects and properly execute them within a useEffect() hook.

Quick Start

An "effect reducer" takes 3 arguments:

  1. state - the current state
  2. event - the event that was dispatched to the reducer
  3. exec - a function that captures effects to be executed and returns an effect entity that allows you to control the effect
import { useEffectReducer } from 'use-effect-reducer';

// I know, I know, yet another counter example
const countReducer = (state, event, exec) => {
  switch (event.type) {
    case 'INC':
      exec(() => {
        // "Execute" a side-effect here
        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>
  );
};

Named Effects

A better way to make reusable effect reducers is to have effects that are named and parameterized. This is done by running exec(...) an effect object (instead of a function) and specifying that named effect's implementation as the 3rd argument to useEffectReducer(reducer, initial, effectMap).

const fetchEffectReducer = (state, event, exec) => {
  switch (event.type) {
    case 'FETCH':
      // Capture a named effect to be executed
      exec({ type: 'fetchFromAPI', user: event.user });

      return {
        ...state,
        status: 'fetching',
      };
    case 'RESOLVE':
      return {
        status: 'fulfilled',
        user: event.data,
      };
    default:
      return state;
  }
};

const initialState = { status: 'idle', user: undefined };

const fetchFromAPIEffect = (_, effect, dispatch) => {
  fetch(`/api/users/${effect.user}`)
    .then(res => res.json())
    .then(data => {
      dispatch({
        type: 'RESOLVE',
        data,
      });
    });
};

const Fetcher = () => {
  const [state, dispatch] = useEffectReducer(fetchEffectReducer, initialState, {
    // Specify how effects are implemented
    fetchFromAPI: fetchFromAPIEffect,
  });

  return (
    <button
      onClick={() => {
        dispatch({ type: 'FETCH', user: 42 });
      }}
    >
      Fetch user
    </div>
  );
};

Effect Implementations

An effect implementation is a function that takes 3 arguments:

  1. The state at the time the effect was executed with exec(effect)
  2. The event object that triggered the effect
  3. The effect reducer's dispatch function to dispatch events back to it. This enables dispatching within effects in the effectMap if it is written outside of the scope of your component. If your effects require access to variables and functions in the scope of your component, write your effectMap there.

The effect implementation should return a disposal function that cleans up the effect:

// Effect defined inline
exec(() => {
  const id = setTimeout(() => {
    // do some delayed side-effect
  }, 1000);

  // disposal function
  return () => {
    clearTimeout(id);
  };
});
// Parameterized effect implementation
// (in the effect reducer)
exec({ type: 'doDelayedEffect' });

// ...

// (in the component)
const [state, dispatch] = useEffectReducer(someReducer, initialState, {
  doDelayedEffect: () => {
    const id = setTimeout(() => {
      // do some delayed side-effect
    }, 1000);

    // disposal function
    return () => {
      clearTimeout(id);
    };
  },
});

Initial Effects

The 2nd argument to useEffectReducer(state, initialState) can either be a static initialState or a function that takes in an effect exec function and returns the initialState:

const fetchReducer = (state, event) => {
  if (event.type === 'RESOLVE') {
    return {
      ...state,
      data: event.data,
    };
  }

  return state;
};

const getInitialState = exec => {
  exec({ type: 'fetchData', someQuery: '*' });

  return { data: null };
};

// (in the component)
const [state, dispatch] = useEffectReducer(fetchReducer, getInitialState, {
  fetchData(_, { someQuery }) {
    fetch(`/some/api?${someQuery}`)
      .then(res => res.json())
      .then(data => {
        dispatch({
          type: 'RESOLVE',
          data,
        });
      });
  },
});

Effect Entities

The exec(effect) function returns an effect entity, which is a special object that represents the running effect. These objects can be stored directly in the reducer's state:

const someReducer = (state, event, exec) => {
  // ...

  return {
    ...state,
    // state.someEffect is now an effect entity
    someEffect: exec(() => {
      /* ... */
    }),
  };
};

The advantage of having a reference to the effect (via the returned effect entity) is that you can explicitly stop those effects:

const someReducer = (state, event, exec) => {
  // ...

  // Stop an effect entity
  exec.stop(state.someEffect);

  return {
    ...state,
    // state.someEffect is no longer needed
    someEffect: undefined,
  };
};

Effect Cleanup

Instead of implicitly relying on arbitrary values in a dependency array changing to stop an effect (as you would with useEffect), effects can be explicitly stopped using exec.stop(entity), where entity is the effect entity returned from initially calling exec(effect):

const timerReducer = (state, event, exec) => {
  if (event.type === 'START') {
    return {
      ...state,
      timer: exec(() => {
        const id = setTimeout(() => {
          // Do some delayed effect
        }, 1000);

        // Disposal function - will be called when
        // effect entity is stopped
        return () => {
          clearTimeout(id);
        };
      }),
    };
  } else if (event.type === 'STOP') {
    // Stop the effect entity
    exec.stop(state.timer);

    return state;
  }

  return state;
};

All running effect entities will automatically be stopped when the component unmounts.

Replacing Effects

If you want to replace an effect with another (likely similar) effect, instead of calling exec.stop(entity) and calling exec(effect) to manually replace an effect, you can call exec.replace(entity, effect) as a shorthand:

const doSomeDelay = () => {
  const id = setTimeout(() => {
    // do some delayed effect
  }, delay);

  return () => {
    clearTimeout(id);
  };
};

const timerReducer = (state, event, exec) => {
  if (event.type === 'START') {
    return {
      ...state,
      timer: exec(() => doSomeDelay()),
    };
  } else if (event.type === 'LAP') {
    // Replace the currently running effect represented by `state.timer`
    // with a new effect
    return {
      ...state,
      timer: exec.replace(state.timer, () => doSomeDelay()),
    };
  } else if (event.type === 'STOP') {
    // Stop the effect entity
    exec.stop(state.timer);

    return state;
  }

  return state;
};

String Events

The events handled by the effect reducers are intended to be event objects with a type property; e.g., { type: 'FETCH', other: 'data' }. For events without payload, you can dispatch the event type alone, which will be converted to an event object inside the effect reducer:

// dispatched as `{ type: 'INC' }`
// and is the same as `dispatch({ type: 'INC' })`
dispatch('INC');

API

useEffectReducer hook

The useEffectReducer hook takes the same first 2 arguments as the built-in useReducer hook, and returns the current state returned from the effect reducer, as well as a dispatch function for sending events to the reducer.

const SomeComponent = () => {
  const [state, dispatch] = useEffectReducer(someEffectReducer, initialState);

  // ...
};

The 2nd argument to useEffectReducer(...) can either be a static initialState or a function that takes in exec and returns an initialState (with executed initial effects). See Initial Effects for more information.

const SomeComponent = () => {
  const [state, dispatch] = useEffectReducer(
    someEffectReducer,
    exec => {
      exec({ type: 'someEffect' });
      return someInitialState;
    },
    {
      someEffect(state, effect) {
        // ...
      },
    }
  );

  // ...
};

Additionally, the useEffectReducer hook takes a 3rd argument, which is the implementation details for named effects:

const SomeComponent = () => {
  const [state, dispatch] = useEffectReducer(someEffectReducer, initialState, {
    log: (state, effect, dispatch) => {
      console.log(state);
    },
  });

  // ...
};

exec(effect)

Used in an effect reducer, exec(effect) queues the effect for execution and returns an effect entity.

The effect can either be an effect object:

// ...
const entity = exec({
  type: 'alert',
  message: 'hello',
});

Or it can be an inline effect implementation:

// ...
const entity = exec(() => {
  alert('hello');
});

exec.stop(entity)

Used in an effect reducer, exec.stop(entity) stops the effect represented by the entity. Returns void.

// Queues the effect entity for disposal
exec.stop(someEntity);

exec.replace(entity, effect)

Used in an effect reducer, exec.replace(entity, effect) does two things:

  1. Queues the entity for disposal (same as calling exec.stop(entity))
  2. Returns a new effect entity that represents the effect that replaces the previous entity.

TypeScript

The effect reducer can be specified as an EffectReducer<TState, TEvent, TEffect>, where the generic types are:

  • The state type returned from the reducer
  • The event object type that can be dispatched to the reducer
  • The effect object type that can be executed
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':
    // State, event, and effect types will be inferred!

    // Also you should probably switch on
    // `state.status` first ;-)

    // ...

    default:
      return state;
  }
};

More Repositories

1

react-redux-form

Create forms easily in React with Redux.
JavaScript
2,066
star
2

flipping

Flipping awesome animations.
TypeScript
1,380
star
3

RxCSS

JavaScript
416
star
4

sassdash

The Sass implementation of lodash.
SCSS
386
star
5

sass-svg

Inline SVG for Sass.
CSS
314
star
6

frontend-masters-xstate-workshop

Frontend Masters State Machines & XState Workshop
JavaScript
242
star
7

frontend-masters-css

SCSS
169
star
8

sassport

Sass with JavaScript superpowers.
JavaScript
147
star
9

frontend-masters-react-workshop

Code for the Frontend Masters React State Modeling Workshop
JavaScript
128
star
10

nm8

Ridiculously small animation library.
TypeScript
122
star
11

frontend-masters-xstate-v2

Frontend Masters XState Workshop v2
JavaScript
121
star
12

react-simple-datepicker

Simple datepicker for React.
JavaScript
119
star
13

xstate-test-demo

Demo React project for model-based testing with @xstate/test
JavaScript
111
star
14

durable-entities-xstate

XState + Durable Entities = 🚀
TypeScript
53
star
15

xstate-react-workshop

React Finland - Modeling React Applications with Statecharts Workshop
JavaScript
24
star
16

weatherboard

TypeScript
23
star
17

xviz

Small graph visualization library
TypeScript
19
star
18

propelcss

CSS
19
star
19

postcss-emoji-style

JavaScript
19
star
20

xstate-todomvc

Created with CodeSandbox
JavaScript
18
star
21

xstate-examples

Example XState Projects
17
star
22

xstate-command

Command palette with XState
TypeScript
12
star
23

react-finland-xstate-2022

React Finland 2022 XState Workshop
JavaScript
11
star
24

mbt-workshop

Model-Based Testing Workshop
JavaScript
11
star
25

react-finland-xstate-workshop

React Finland XState Workshop
JavaScript
11
star
26

sass-v

CSS Custom Properties + Sass = 🚀
SCSS
10
star
27

RxAnimate

Reactive Animations
TypeScript
10
star
28

openapi-test

OpenAPI Testing Library
TypeScript
10
star
29

node-azure-mvc

Example application for creating an MVC Express + Node + TypeScript app and deploying it to Azure
TypeScript
10
star
30

redux-test-store

Test existing stores in Redux, the easy way
JavaScript
8
star
31

skylake

💎 A light JavaScript library.
JavaScript
8
star
32

xstate-interpreter

Reactive statechart interpreter.
TypeScript
7
star
33

paz

Peaceful Azure deployments with node
6
star
34

movement

Sass/SCSS/CSS animation framework for creating, composing, sequencing, and using animations.
CSS
6
star
35

davidkpiano

5
star
36

getrect

TypeScript
5
star
37

rxio

TypeScript
4
star
38

xstate-tetris

TypeScript
4
star
39

issuspensereadyyet

...
JavaScript
4
star
40

tracker

Track DOM elements efficiently.
4
star
41

netlify-edge-machine

TypeScript
4
star
42

neo4j-workshop

Neo4j Statecharts Workshop
JavaScript
3
star
43

rxform

TypeScript
3
star
44

gulp-sassport

Sassport gulp plugin
JavaScript
3
star
45

sassport-reference

Import by Reference in Sass using Sassport
JavaScript
3
star
46

vanilla-todomvc-ts

TypeScript
3
star
47

cadenza-old

SCSS/CSS Framework
CSS
2
star
48

movi

Stately animations
2
star
49

cse-articles

Microsoft CSE Articles
2
star
50

rxstyle

Animation framework for RxJS
JavaScript
2
star
51

sassport-svgo

Inline, optimized SVG inside your Sass files.
JavaScript
2
star
52

sassport-dynamic

Dynamic imports for Node-Sass using Sassport
JavaScript
2
star
53

rrf-docs

CSS
2
star
54

react-redux-form-builder

React Redux Form Builder
JavaScript
2
star
55

webanimate

TypeScript
2
star
56

stately-site

HTML
2
star
57

cz-temp

CSS
2
star
58

angular-fluid

Form Logic User Interface Directive - Form Logic implementation for AngularJS
CSS
2
star
59

orlandodevs-site

HTML
1
star
60

react-redux-form-docs

1
star
61

styledash

JavaScript
1
star
62

react-state-machine

Finite State Machines for React.
1
star
63

logicapp-specflow-demo

C#
1
star
64

sassport-math

Math for Sass.
JavaScript
1
star
65

emojio

1
star
66

goatspresso

Goats and Espresso
JavaScript
1
star
67

test-gen-demo

TypeScript
1
star
68

react-redux-form-book

1
star
69

postcss-movement

1
star
70

rxy

For Atomic Hack Party
PHP
1
star
71

movt

Movement
JavaScript
1
star
72

react-dynamic

Dynamic.js Animation Library for React
1
star
73

styleguides

1
star
74

react-move

Nothing to see here
JavaScript
1
star
75

cadenza

1
star
76

sass-z

Z-index management for Sass.
1
star
77

generator-sass-guide

Yeoman generator for Sass Guidelines
JavaScript
1
star