Git Product home page Git Product logo

use-reducer-with-side-effects's Introduction

Use Reducer With Side Effects

Actions Status npm version

React's useReducer hook should be given a reducer that is a pure function with no side effects. (useReducer might call the reducer more than once with the same initial state.) Sometimes, however, it makes sense to include network calls or other side effects within the reducer in order to keep your program logic all in one place.

Inspired by the reducerComponent of ReasonReact, this library provides a way to declaratively declare side effects with updates, or to execute a side effect through the reducer while keeping the reducer pure. The general idea being that the side effects simply declare intent to execute further code, but belong with the update. reducers always return one of Update, NoUpdate, UpdateWithSideEffects, or SideEffects function.

One example in which this may be useful is when dispatching a second action depends on the success of the first action, instead of waiting to find out, one can declare the side effect along side the update.

Install

  • npm install use-reducer-with-side-effects
  • yarn add use-reducer-with-side-effects

Exports

  • Update - Return synchronous new state wrapped in this function for a plain update. Update(newState)
  • NoUpdate - Indicate that there are no changes and no side effects. NoUpdate()
  • SideEffect - Receives a callback function with state, and dispatch as arguments. SideEffect((state, dispatch) => { /* do something */ }
  • UpdateWithSideEffect - Very similar to Update and SideEffect combined. It takes the updated state as the first argument (as Update) and a side-effect callback as the second argument (as SideEffect). The callback function receives the updated state (newState) and a dispatch. UpdateWithSideEffect(newState, (state, dispatch) => { /* do something */ })

Default Export - useReducerWithSideEffects hook;

Nearly direct replacement to React's useReducer hook, however, the provided reducer must return the result of one of the above functions (Update/NoUpdate/UpdateWithSideEffects/SideEffects) instead of an updated object. See the useReducer documentation for different options on how to define the initial state.

const [state, dispatch] = useReducerWithSideEffects(reducer, initialState, init)

const [state, dispatch] = useReducerWithSideEffects(reducer, {})

Modify Existing Reducer

If you've got an existing reducer that works with React's useReducer and you want to modify to use this library, do the following:

  1. Modify every state change return to use Update.

    old: return {...state, foo: 'bar'}

    new: return Update({...state, foo: 'bar'}

  2. Modify every unchanged state return to use NoUpdate.

    old: return state

    new: return NoUpdate()

Now the reducer may be used with useReducerWithSideEffects and can have side effects added by using the SideEffect or UpdateWithSideEffect methods.

Example

A comparison using an adhoc use effect versus this library

adhoc
import React, { useReducer } from 'react';

function Avatar({ userName }) {
  const [state, dispatch] = useReducer(
    (state, action) => {
      switch (action.type) {
        case FETCH_AVATAR: {
          return { ...state, fetchingAvatar: true };
        }
        case FETCH_AVATAR_SUCCESS: {
          return { ...state, fetchingAvatar: false, avatar: action.avatar };
        }
        case FETCH_AVATAR_FAILURE: {
          return { ...state, fetchingAvatar: false };
        }
      }
    },
    { avatar: null }
  );

  useEffect(() => {
    dispatch({ type: FETCH_AVATAR });
    fetch(`/avatar/${userName}`).then(
      avatar => dispatch({ type: FETCH_AVATAR_SUCCESS, avatar }),
      dispatch({ type: FETCH_AVATAR_FAILURE })
    );
  }, [userName]);

  return <img src={!state.fetchingAvatar && state.avatar ? state.avatar : DEFAULT_AVATAR} />
}

Library with colocated async action

import useReducerWithSideEffects, { UpdateWithSideEffect, Update } from 'use-reducer-with-side-effects';

function Avatar({ userName }) {
  const [{ fetchingAvatar, avatar }, dispatch] = useReducerWithSideEffects(
    (state, action) => {
      switch (action.type) {
        case FETCH_AVATAR: {
          return UpdateWithSideEffect({ ...state, fetchingAvatar: true }, (state, dispatch) => { // the second argument can also be an array of side effects
                fetch(`/avatar/${userName}`).then(
                  avatar =>
                    dispatch({
                      type: FETCH_AVATAR_SUCCESS,
                      avatar
                    }),
                  dispatch({ type: FETCH_AVATAR_FAILURE })
                );
          });
        }
        case FETCH_AVATAR_SUCCESS: {
          return Update({ ...state, fetchingAvatar: false, avatar: action.avatar });
        }
        case FETCH_AVATAR_FAILURE: {
          return Update({ ...state, fetchingAvatar: false })
        }
      }
    },
    { avatar: null }
  );

  useEffect(() => dispatch({ type: FETCH_AVATAR }) , [userName]);

  return <img src={!fetchingAvatar && avatar ? avatar : DEFAULT_AVATAR} />;
}

use-reducer-with-side-effects's People

Contributors

chrisdhanaraj avatar conorhastings avatar dependabot[bot] avatar hasnat avatar ianepperson avatar jamesplease avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

use-reducer-with-side-effects's Issues

Typings

Is there anyway we can get some typescript definitions for this library?

Bug: error thrown when dispatching an `Update()`

Type: Bug
Library version: 1.0.0

What's happening?

I'm creating a reducer and dispatching an Update(). This is throwing the following error:

index.js:70 Uncaught TypeError: newSideEffect is not iterable

This is the LoC throwing the error

What's the expected behavior?

This would not error.

What's the source of the problem?

I believe this problem was introduced in eac6b68 and then almost but not quite fixed in 54e75bf . Currently, the LoC that are throwing the error don't do anything.

Possible resolution

There are a bunch of ways to fix this, one is:

const newSideEffects = newSideEffect
      ? [
          ...state.sideEffects,
          ...(Array.isArray(newSideEffect) ? newSideEffect : [newSideEffect]),
        ]
      : state.sideEffects;

Storing closures inside the state and async side effects

Hello @conorhastings

Thanks for this library. Unfortunately I discovered it too late as I finished working on my spin on the same topic: https://github.com/frankiesardo/use-reducer-with-effects but I'm glad to see other people share a similar philosophy. The library I authored differ slightly in the sense that the side effects are represented as data instead of closures but I think they're conceptually very similar otherwise. I'd be happy to hear your thoughts!

I have two questions regarding your implementation:

  1. Your sideEffect is stored in the state object as a function / closure. Does this have any implications re: equality checks and serialisability of the state object?
  2. I execute the sideEffects as soon as they are returned by the reducer: https://github.com/frankiesardo/use-reducer-with-effects/blob/master/src/index.js#L6-L15 do you think that's wrong? I see you have them in an async block. I also don't bother keeping a queue of side effects since as soon as a side effect is returned by the reducer, the useEffect is triggered and the side effect is removed from the state object. Do you think that can cause problems?

In any case, thanks for taking the time and BTW your README is extremely clear and well put together, well done for that ๐Ÿ‘

Missing `fetchingAvatar` Declaration in readme example

Hi!

Cool library. I think the readme is missing a declaration which confused me.

const [{ avatar }, dispatch] = useReducerWithSideEffects(...

Should probably be

const [{ avatar, fetchingAvatar }, dispatch] = useReducerWithSideEffects(...

Right? Am I understanding correctly?

Question: what are "cancel functions" for?

Yo! I'm pretty familiar with the dispatch/state side of this lib now, but I wanted to learn more about the side effects portion, so I was reading through the code and noticed the idea of "cancel functions", which are functions that can be returned from side effects.

Do you have a tl;dr or an example of how those might be used?

API Portability

I like that this lib, and its API, draws such strong inspiration from ReasonReact, with the inclusion of functions like Update and NoUpdate. A side-effect of those specific functions is that it reduces the portability of the reducers that authors write.

Because this lib has a hard dependency on React, any reducer that you write using Update/etc. must also require React.

One of the things I liked about React's reducer API is that the reducers themselves are agnostic to React. It is pretty cool to write a reducer one time and to be able to use it in different contexts.

Technically it's possible to do that with this library as well by explicitly returning an object with the shape:

{
  newState: { ... },
  newEffects: [ ... ]
}

but it's undocumented, so I'm thinking of it as an internal API.

Questions for you:

  • do you think there's any value in API portability?
  • if so, would you consider the return value of Update, etc. to be public API?

The motivation for this issue is that I'm writing reducers that I plan to use both in and outside of React apps, and i want to use the useReducersWithSideEffects architecture in both. In the React world I planned to use this library to integrate it into my app, and then I planned to write a vanilla JS version for outside of React.

Update `init` behavior

When using useReducer, init returns the state:

useReducer(reducer, initialState, () => {
  return {
    someState: true
  };
})

With the current API of this library, you must return the "internal" structure of the store:

useReducerWithSideEffects(reducer, initialState, () => {
  return {
    state: { someState: true },
    sideEffects: [] // you also must include this or else an uncaught exception will occur
  };
})

Users are unable to use Update/UpdateWithSideEffects/etc., because those don't return the correct key: they return newState rather than state.

I suggest refactoring Update/etc. to internally use the key state instead. That way, users of init can use a consistent API with this lib:

useReducerWithSideEffects(reducer, initialState, () => {
  return Update({ someState: true });
})

This change could even be released as a bug fix, I think, since it only changes non-public structures.

I'm curious to hear your thoughts on this @conorhastings !

Feature request: `composeReducers`

Type

Feature request

Wot's the request

I think it'd be pretty cool if this library introduced a new method, composeReducers, that managed composing multiple reducers into one reducer.

More info

With a typical reducer API, like the one used in useReducer, composing reducers is straightforward. One way to do it is:

export default function composeReducers(reducers) {
  return (state, action) =>
    reducers.reduceRight(
      (prevState, reducer) => reducer(prevState, action),
      state
    );
}

It is quite a bit more complex to compose reducers with use-reducer-with-side-effects. A few of the things you have to keep in mind are:

  • the return value of a reducer is on longer the input for the next one, since they receive state as an input but output both state and side effects
  • since the sideEffects don't pass through to the next reducer, you must keep and concatenate an array of all of the returned side effects outside of the loop
  • a Symbol is used for NoUpdates in this lib, rather than the previous state object, so you must track the # of Symbols returned to determine if an update actually occurred

None of these points are insurmountable, but it is a bit to think about as a consumer of this lib who is trying to split out a big reducer into smaller pieces. A composeReducers export from this lib would make it easier for devs to organize their reducers in this way.

Other notes

If this isn't added, then it might be a good idea to explicitly export NO_UPDATE_SYMBOL so that folks can reliably check for no updates when writing their own composer fn. (It can be accessed by calling NoUpdate() but that seems a lil hacky for this purpose imo)

Prior art


Here's an example implementation in userland that seems to be working. It could probably be tidied up a bit with direct access to NO_UPDATE_SYMBOL.

If you're ๐Ÿ‘ to this idea, I can make a PR. Lmk what you think!

Tests

to be confident in updates we need test

@jamesplease want to ideate on what we might to cover

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.