Giter VIP home page Giter VIP logo

use-methods's Introduction

use-methods Build Status

Installation

Pick your poison:

  • npm install use-methods
    
  • yarn add use-methods
    

Usage

This library exports a single React Hook, useMethods, which has all the power of useReducer but none of the ceremony that comes with actions and dispatchers. The basic API follows a similar pattern to useReducer:

const [state, callbacks] = useMethods(methods, initialState);

Instead of providing a single "reducer" function which is one giant switch statement over an action type, you provide a set of "methods" which modify the state or return new states. Likewise, what you get back in addition to the latest state is not a single dispatch function but a set of callbacks corresponding to your methods.

A full example:

import useMethods from 'use-methods';

function Counter() {

  const [
    { count }, // <- latest state
    { reset, increment, decrement }, // <- callbacks for modifying state
  ] = useMethods(methods, initialState);

  return (
    <>
      Count: {count}
      <button onClick={reset}>Reset</button>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </>
  );
}

const initialState = { count: 0 };

const methods = state => ({
  reset() {
    return initialState;
  },
  increment() {
    state.count++;
  },
  decrement() {
    state.count--;
  },
});

Note: the methods factory function must produce the same set of method names on every invocation.

Comparison to useReducer

Here's a more complex example involving a list of counters, implemented using useReducer and useMethods respectively:

useReducer vs useMethods comparison

Which of these would you rather write?

Immutability

use-methods is built on immer, which allows you to write your methods in an imperative, mutating style, even though the actual state managed behind the scenes is immutable. You can also return entirely new states from your methods where it's more convenient to do so (as in the reset example above).

If you would like to use the patches functionality from immer, you can pass an object to useMethods that contains the methods property and a patchListener property. The callback will be fed the patches applied to the state. For example:

const patchList: Patch[] = [];
const inverseList: Patch[] = [];

const methodsObject = {
  methods: (state: State) => ({
    increment() {
      state.count++;
    },
    decrement() {
      state.count--;
    }
  }),
  patchListener: (patches: Patch[], inversePatches: Patch[]) => {
    patchList.push(...patches);
    inverseList.push(...inversePatches);
  },
};

// ... and in the component
const [state, { increment, decrement }] = useMethods(methodsObject, initialState);

Memoization

Like the dispatch method returned from useReducer, the callbacks returned from useMethods aren't recreated on each render, so they will not be the cause of needless re-rendering if passed as bare props to React.memoized subcomponents. Save your useCallbacks for functions that don't map exactly to an existing callback! In fact, the entire callbacks object (as in [state, callbacks]) is memoized, so you can use this to your deps array as well:

const [state, callbacks] = useMethods(methods, initialState);

// can pass to event handlers props, useEffect, etc:
const MyStableCallback = useCallback((x: number) => {  
  callbacks.someMethod('foo', x);
}, [callbacks]);

// which is equivalent to:
const MyOtherStableCallback = useCallback((x: number) => {
  callbacks.someMethod('foo', x);
}, [callbacks.someMethod]);

Types

This library is built in TypeScript, and for TypeScript users it offers an additional benefit: one no longer needs to declare action types. The example above, if we were to write it in TypeScript with useReducer, would require the declaration of an Action type:

type Action =
  | { type: 'reset' }
  | { type: 'increment' }
  | { type: 'decrement' };

With useMethods the "actions" are implicitly derived from your methods, so you don't need to maintain this extra type artifact.

If you need to obtain the type of the resulting state + callbacks object that will come back from useMethods, use the StateAndCallbacksFor operator, e.g.:

const MyContext = React.createContext<StateAndCallbacksFor<typeof methods> | null>(null);

use-methods's People

Contributors

dependabot[bot] avatar dfbaskin avatar emiltholin avatar pelotom avatar pocesar avatar slikts 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

use-methods's Issues

Returning state object from reducer methods loses references to state type

Using this code sample:

export const App = () => {
  const [] = useMethods(
    state => ({
      set(value: number) {
        return {
          ...state,
          count: value
        };
      }
    }),
    initialState
  );
};

interface State {
  count: number;
}

const initialState: State = {
  count: 0
};

In (for example) vscode, if you rename the count property within the State interface, it will not propagate the count property in the object being returned from the increment method. Similar operations such as "go to definition" and "find references" will also not work.

Although the typings do require that the increment method returns something that matches the State interface structurally, it has lost the reference to the actual interface.

Aside from being a bit inconvenient, this is a potential hazard because TS allows extra properties in the returned object. Therefore, if one was to rename the count property on the interface the reducer would continue to compile without errors, and would fail in an unspecified manner at runtime instead.

Workarounds for this include:

  • only use immer to manipulate the state (i.e. always return void)
  • type the reducer method return types explicitly: set(value: number): State

make it clear that the whole "callbacks" object is memoized

I was unnecessarily passing individual methods to useEffect / useCallback instead of passing the whole callbacks from [state, callbacks]. I noticed it's memoized from looking the code, would be nice to make it clear that the whole object is stable

so doing:

const [state, callbacks] = useMethods(methods, initialState)

const = myCallback = useCallback(() => {
  callbacks.something()
}, [callbacks])

works

Is there a way to use Set types as part of the state?

It doesn't seem to work out of the box, is there a way to enable/use them?

Example:

// CREATING STATE AND ACTIONS
import useMethods from 'use-methods';

const initialState = {
  idList: new Set()
};

const methods = state => ({
  addId: id =>{state.idList.add(id)},
  removeId: id => {state.idList.delete(id)}
});

const [state, actions] = useMethods(methods, initialState);

// USAGE
actions.addId(123); //does update the state but does not trigger a re-render

Components not re-rendering when using spread syntax

Just came across this in my project.
Is this an expected behaviour?

const methods = (state: State) => ({
    setValues: (payload) => {
        // Works fine
        state.value1 = payload.value1;
        state.value2 = payload.value2;
        state.value3 = payload.value3;
    },
    setValuesSpread: (payload) => {
        // Doesn't re-render
        state = { ...state, ...payload };
    },
});

const initialState = {
    value1: '',
    value2: '',
    value3: '',
};

const TestUseMethods = () => {
    const [state, { setValues, setValuesSpread }] = useMethods(methods, initialState);
    const handleClick = () => setValues({
        value1: 'A house!',
        value2: 'A horse!',
        value3: 'A banana!',
    });
    const handleClickSpread = () => setValuesSpread({
        value1: 'A spreaded house!',
        value2: 'A spreaded horse!',
        value3: 'A spreaded banana!',
    });
    return (
        <div>
            <button onClick={handleClick}>Set values</button>
            <button onClick={handleClickSpread}>Set values spread</button>
            <ul>
                <li>Value1: {state.value1}</li>
                <li>Value2: {state.value2}</li>
                <li>Value3: {state.value3}</li>
            </ul>
        </div>
    );
};

Should method be called twice?

Just trying out useMethod for the first time and I noticed something. Here is my code:

import useMethods from 'use-methods'


// Initial State
const initialState = { 
    token: ''
};


// Methods
const methods = state => ({

    setToken(newToken) {
        console.log("useMethod setting token", newToken)
        state.token = newToken
    }

})


// Hook
export default function useAuthContextValue() {

    const [
        {token}, 
        {setToken}
    ] = useMethods(methods, initialState);

    return {
        token,
        setToken
    }

}

My console log is being logged twice each time I call setToken. I checked that I wasn't calling it twice from my consuming component by putting a console.log directly before the line that calls setToken and that log only happens once.

Is this expected behaviour?

Alternative syntax using `this`?

An alternate, more "method"-y API would be to use this as the state variable, which would mean you could provide a simple record instead of a function of state:

const initialState = { count: 0 };

const methods = {
  reset() {
    return initialState;
  },
  increment() {
    this.count++;
  },
  decrement() {
    this.count--;
  },
};

Another advantage is that VSCode gives this special emphasis and color, so it's easy to tell at a glance where state access and "mutation" is happening.

Downsides of this approach:

  • Can't use arrow functions to define methods
  • Can't decide to use a shorter name for the state variable, e.g. s
  • Can't compute variables from state that will be in scope for all methods, e.g.:
    const methods = state => {
      const { x, y, z } = state;
      return {
        // ... method definitions
      }
    };

I'm curious if others have any opinions about this.

Provide custom "useReducer" hook

Hi @pelotom ,

I want to suggest adding support for providing a custom "useReducer" hook inside the useMethods function, which adds supports to other middlewares, not just the default that came with the "react" module. I saw this project "reinspect" https://github.com/troch/reinspect , which provides integration with the redux-devtools extension for time travel debugging, something I used a lot with Redux before switching to context API and the hooks ecosystem.

I suggest adding a PR that will have a "useReducer" function as part of the useMethods hook, that will be optional by default and use the default "useReducer" from 'react' package.

Happy to contribute for this if needed.

Alternative class based API

Inspired by the typing goodness of useMethods, I've been using a modified version that works nicely with Typescript. I've been calling it useInstance :)

const state = useInstance(() => new CounterState());

And you define your state/actions in a single class:

class CounterState {

  nextId = 0;
  counters = [];
  updateCount = 0;

  addCounter() {
    this.counters.push({ id: this.nextId++, count: 0 });
  }
  incrementCounter(id) {
    this.getCounter(id).count++;
  }
  resetCounter(id) {
    this.getCounter(id).count = 0;
  }

  // "Dispatch" methods can call other private methods (and return values)

  private getCounter(id) {
    return this.counters.find(counter => counter.id === id);
  }

  // You can return the state to replace the state completely
  // Otherwise, do not return state (see immer for details)

  startOver() {
    return new CounterState();
  }

  // I've been playing with this idea.. 
  // A "magic" method that runs just before every dispatch end. 

  beforeDispatchEnd() {
    this.updateCount++;
    console.log("Update done. Count: " + this.updateCount);
  }
}

What I like about this API:

  • wraps state together with the actions that modify it (love or hate it)
  • from "dispatched" methods, you can call internal/private methods (with return values)
  • works nicely with types/refactoring

Here's a demo (as a JS class, but intended for Typescript):
https://codesandbox.io/embed/usereducer-vs-usemethods-comparison-llmvr

As it's a little out of scope I was going to create a new project.. or ... would you be interested in integrating something like this?

Related #18 #2

Should we swap the argument order?

Currently it's useMethods(initialState, methods), which feels more intuitive to me, but it goes against the precedent set by useReducer. Also useReducer has an optional third argument which we're not supporting at the moment but probably should.

Should state be kept separate from callbacks in the returned result?

Currently we return an object from useMethods which is a mixture of state and callbacks:

const initial = { count: 0 };

const methods = state => ({
  increment() {
    state.count++;
  },
  decrement() {
    state.count--;
  },
});

// ...

const { count, increment, decrement } = useMethods(initial, methods);

but maybe they should be kept separate, a la useState and useReducer:

const [{ count }, { increment, decrement }] = useMethods(initial, methods);

One advantage of this would be the option to use primitives instead of objects for state. For example in this case we could use a simple number for the state:

const initial = 0;

const methods = count => ({
  increment: () => count + 1,
  decrement: () => count - 1,
});

// ...

const [count, { increment, decrement }] = useMethods(initial, methods);

Question: Guidance for async effects?

Our team is currently using redux-loop for handling asynchronous side-effects kicked off by our reducer. We're really excited about use-methods, and we're trying to figure out if there's any guidance on how to initiate async calls and have those trigger state updates when they complete.

cc @alexburner

Want to obtain the new state synchronously - can't?

Given an initialized useMethods

  const [ productReviewState, stateChangers ] = useMethods(...);

And an updater like the following:

      updateAnswer: ({ questionId, value, isValid = true }) => {
        state.answers[questionId] = value;
        state.answerValidity[questionId] = isValid;
      }

When I run an effect hook, how can I synchronously have the value of the answerValidity subkey of productReviewState?

      console.log(
        `answerValidity[${questionId}]`,
        productReviewState.answerValidity[questionId]
      );
      stateChangers.updateAnswer({ questionId, value, isValid });
      console.log(
        `answerValidity[${questionId}]`,
        productReviewState.answerValidity[questionId]
      );

The second console log never reflects the updated value. I suppose this is just immutability at work, but I'd at least like a way to "requery" for the current value. TIA.

Sources are not included in published package, even though the source maps point to them

Hi! First of all, thanks for this awesome package!

The problem

The published package only includes the dist folder, however the generated source map at dist/index.js.map points to sources at ../src/index.ts.

This means that the source maps are effectively unusable, as they point to a file that is not available.
It also results in a warning when using this package with Parcel. Every time the application is built, Parcels shows the following warning:

⚠️ Could not load source file "../src/index.ts" in source map of "../node_modules/use-methods/dist/index.js".

Possible solutions

  1. Inline sources directly within the source map using the --inlineSources flag when compiling with tsc
  2. Publish sources as well, by changing the files property in package.json to:
"files": [
    "dist",
    "src"
  ]
  1. Disable source map generation completely, by setting the sourceMap property to false in tsconfig.json.

I don't think there's any practical difference between option 1 and 2, I assume it's just a matter of preference. Option 3 favours a leaner package at the expense of not having source available when developing.
I would strongly hope that you choose option 1 or 2 over 3. 🙂

Is this project alive?

There issues stale for several years and PRs waiting to be merged. Is there a code owner here willing to respond?

Lazy initialization

I wonder if you'd consider adding lazy initialization?

It could mimic the useReducer API, eg:

const [state, dispatch] = useReducer(reducer, initialArg, init);
https://reactjs.org/docs/hooks-reference.html#usereducer

... potentially without breaking changes (as far as I can tell).

I'd make a new pull request myself but your TS typing is beyond my current level!

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.