GithubHelp home page GithubHelp logo

bigab / use-epic Goto Github PK

View Code? Open in Web Editor NEW
46.0 1.0 4.0 2.03 MB

Use RxJS Epics as state management for your React Components

License: MIT License

JavaScript 100.00%
rxjs react hook observable epic

use-epic's Introduction

🏰 use-epic

Use RxJS Epics as state management for your React Components

Build Status MIT npm version Greenkeeper badge

What is an Epic❓

An Epic is a function which takes an Observable of actions (action$), an Observable of the current state (state$), and an object of dependencies (deps) and returns an Observable.

The idea of the Epic comes out of the fantastic redux middleware redux-observable, but a noteable difference is that, because redux-observable is redux middleware, the observable returned from the Epic emits new actions to be run through reducers to create new state, useEpic() skips the redux middleman and expects the Epic to return an observable of state updates.

function Epic(action$, state$, deps) {
  return newState$;
}

This simple idea opens up all the fantastic abilites of RxJS to your React components with a simple but powerful API.

πŸ”Ž Usage

function productEpic(action$, state$, deps) {
  const { productStore, cartObserver, props$ } = deps;
  combineLatest(action$.pipe(ofType('addToCart')), state$)
    .pipe(
      map(([productId, products]) => products.find(p => p.id === productId))
    )
    .subscribe(cartObserver);

  return props$.pipe(
    map(props => props.category),
    switchMap(category => productStore.productsByCategory(category)),
    startWith([])
  );
}

const ProductsComponent = props => {
  const [products, dispatch] = useEpic(productEpic, { props });

  // map dispatch to a component callback
  const addToCart = productId =>
    dispatch({ type: 'addToCart', payload: productId });

  return <ProductsList products={products} addToCart={addToCart} />;
};

βš’οΈ Installation

use-epic requires both react and rxjs as peer dependencies.

npm install use-epic rxjs react
yarn add use-epic rxjs react

πŸ—ƒοΈ Examples

See examples locally with npm run examples

Simple Fetch Example - CodeSandbox (source examples)

Alarm Clock Example - CodeSandbox (source examples)

[Beer Search] *coming soon*
[Pull to Refresh] *coming soon*
[Working with simple-store] *coming soon*

πŸ“– API

useEpic()

A React hook for using RxJS Observables for state management.

const [state, dispatch] = useEpic( epic, options? );

The useEpic() hook, accepts an epic function, and an options object, and returns a tuple of state and a dispatch callback, similar to useReducer().

arguments

  • epic an epic function, described below .

    function myEpic( action$, state$, deps ) { return newState$ }

    It should be noted, that only the first Epic function passed to useEpic() will be retained, so if you write your function inline like:

    const [state] = useEpic((action$, state$, deps) => {
      return action$.pipe(switchMap(action => fetchData(action.id)));
    });

    ...any variable closures used in the epic will not change, and component renders will generate a new Epic function that will merely be discared. For that reason we encourage defining Epics outside of the component.

  • options *optional an object with some special properties:

    • deps - an object with keys, any key/values on this deps object will be available on the deps argument in the Epic function
    • props - a way to "pass" component props into the Epic, anything passed here will be emitted to the special, always available, deps.props$, in the Epic. This should be used with caution, as it limits portability, but is available for when dispatching an action is not appropriate.
const CatDetails = props => {
  const [cat] = useEpic(kittenEpic, { deps: { kittenService }, props: cat.id });
  <Details subject={cat} />;
};

epic()

An epic is a function, that accepts an Observable of actions (action$), an Observable of the current state (state$), and an object of dependencies (deps) and returns an Observable of stateUpdates$.

function myEpic( action$, state$, deps ) { return newState$ }

The epic will be called by useEpic(), passing the action$, state$ and deps arguments, and it may either return a new RxJS Observable or undefined. If an observable is returned, and values emitted from that observable are set as state, the first element of the tuple returned from useEpic().

const [state, dispatch] = useEpic(epic);

arguments passed when the epic is called

  • action$ An observable of dispatched actions. The actions emitted are anything passed to the dispatch() callback returned from useEpic(). They can be anything, but by convention are often either objects with a type, payload and sometimes meta properties (e.g. { type: 'activate', payload: user }), or an array tuple with the type as the first element and the payload as the second (e.g. ['activate', user]).

  • state$ An observable of the current state. It can be sometimes helpful to have a reference to the current state when composing streams, say if your action.payload is an id and you'd like to map that to a state entity before further processing it. Unless the observable returned from useEpic() has initial state, from using startWith() or a BehaviorSubject, this will emit undefined to start.
    ⚠️ Caution: When using state$ it is possible to find yourself in an inifinte asynchronous loop. Take care in how it is used along with the returned newState$ observable.

  • deps an object of key/value pairs provided by the options of useEpic when it is called, or from the <EpicDepsProvider> component.

    The deps argument can be very useful for provding a dependency injection point into your Epics and therefore into your components. For example, if you provide an ajax dependecy in deps, you could provide the RxJS ajax function by default, but stub out ajax for tests or demo pages by wrapping your component in an <EpicDepsProvider> component.

      const kittyEpic = (action$, state$, { ajax: rxjs.ajax }) => {
        return action$.pipe(
          switchMap(({ payload: id })=> ajax(`/api/kittens/${id}`))
        );
      }
    
      const KittyComponent = () => {
        const [kitty, dispatch] = useEpic(kittyEpic);
    
        //... render and such
      }
    
      // mocking for tests
      test('should load kitty details when clicked', async () => {
        // stub out ajax for the test
        const fakeResponse = { name: 'Snuggles', breed: 'tabby' };
        const ajaxStub = jest.fn(() => Promise.resolve(fakeResponse));
    
        const { getByLabelText, getByText } = render(
          <EpicDepsProvider ajax={ajaxStub}>
            <KittyComponent />
          </EpicDepsProvider>
        );
    
        fireEvent.click(getByLabelText(/Cat #1/i));
        const detailsName = await getByText(/^Name:/);
        expect(detailsName.textContent).toBe('Name: Snuggles')
      });

    The deps object can be good for providing "services", config, or any number of other useful features to help decouple your components from their dependecies.

    deps.props$
    There is a special property props$ which is always provided by useEpic() and is the methods in which components can pass props into the Epic. The options.props property of the useEpic() call is always emitted to the deps.props$ observable.

ofType()

A RxJS Operator for convient filtering of action$ by type

action$.pipe( ofType( type, ...moreTypes? ) );

Just a convinience operator for filtering actions by type, from either the action itself 'promote', the conventional object form { type: 'promote', payload: { id: 23 } } or array form ['promote', { id: 23 }]. The ofType() operator only filters, so your type property will still be in the emitted value for the next operator or subscription.

arguments

  • type the ofType() operator can take one or more type arguments to match on, if any of the types match for the action emitted, the action will be emitted further down the stream. The type arguments are not restriced to Strings, they can be anything including symbols, functions or objects. They are matched with SameValueZero (pretty much ===) comparison.
const promotionChange$ = action$.pipe(ofType('promote', 'depromote'));

<EpicDepsProvider>

A React Provider component that supplies deps to any epic function used by the useEpic() hook, called anywhere lower in the component tree, just like Reacts context.Provider

<EpicDepsProvider kittenService={kittenService} catConfig={config}>
  <App />
</EpicDepsProvider>

// const kittyEpic = ( action$, state$, { kittenService, catConfig }) => {
//  ...
// }

Any props passed to the EpicDepsProvider component will be merged onto the deps object passed to the epic function when calling useEpic(). Any change in deps will unsubscribe from the newState$ observable, and recall the epic function, setting up new subscriptions, so try to change deps sparingly.

Testing

One benefit of using Epics for state management is that they are easy to test. Because they are just functions, you can ensure the behaviour of your Epic, just by calling it with some test observables and deps, emitting actions, and asserting on the newState$ emitted.

TODO: Create testing example
TODO: Create epic testing helper method

🌱 Contribute

Think you'd like to contribute to this project? Check out our contributing guideline and feel free to create issues and pull requests!

License

MIT Β© Adam L Barrett

use-epic's People

Contributors

bigab avatar dependabot[bot] avatar greenkeeper[bot] 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

Watchers

 avatar

use-epic's Issues

can we make the dispatch globally?

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

scenario:
I wanna dispatch different actions from different Epic.

const [, dispatchLogin] = useEpic(loginEpic)
const [, dispatchActivate] = useEpic(activateEpic)

dispatchActivate({type1,payload1})
dispatchLogin({type1,payload1})

above are different dispatch, and cannot be access and catch globally

Describe the solution you'd like
A clear and concise description of what you want to happen.

combine all the epics and use single dispatch like `redux-observable` combineEpics

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Add any other context or screenshots about the feature request here.

Typescript

Describe the solution you'd like
Provide type definitions to typescript users

Describe alternatives you've considered
I would accept a PR that just adds the typedefs, but if I do it myself I will probably just re-write the library in typescript (mostly as a learning exercise).

Additional context
Of course the library will still export as JS, users will not be required to use TypeScript to use use-epic

An in-range update of rollup is breaking the build 🚨

The devDependency rollup was updated from 1.27.6 to 1.27.7.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

rollup is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build could not complete due to an error (Details).
  • ❌ Travis CI - Branch: The build errored.

Release Notes for v1.27.7

2019-12-01

Bug Fixes

  • Fix a scenario where a reassignments to computed properties were not tracked (#3267)

Pull Requests

Commits

The new version differs by 4 commits.

See the full diff

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Initial State is always undefined

Describe the bug
Despite this test passing, for the first render the state is always undefined.

To Reproduce
Steps to reproduce the behavior:

  1. import useEpic
  2. use it in a component with an Epic that returns an either a BehaviorSubject with initial state or an observable that pipes to startWith
  3. Rely on (or just log) that state on the component render

Expected behavior
The state should be available on the first render

Additional context
One solution would be to subscribe and unsubscribe immediately in first render to get the initial state. If that solution is used we should give a way to opt out, because that could cause weirdness with other peoples observables.

Return methods (action creators) instead of `dispatch`

Is your feature request related to a problem? Please describe.

Having to define a lot of callbacks from the dispatch method can be tedious:

const [
  { restaurant },
  dispatch,
] = useEpic(restaurantFilterEpic);

const handleRegionChange = useCallback(e => dispatch(['SELECT_REGION', e.target.value])), dispatch);
const handleCityChange =  useCallback(e => dispatch(['SELECT_CITY', e.target.value]));
const handleRestaurantChange =  useCallback(e => dispatch(['SELECT_RESTAURANT', target.value]));

return <RestaurantSelector
  restaurant={restaurant}
  onRegionChange={handleRegionChange}
  onCityChange={handleCityChange}
  onRestaurantChange={handleRestaurantChange}
/>

Describe the solution you'd like
I'd like an option, to receive action creator style methods instead of the dispatch, perhaps something like:

const [
  { restaurant },
  { handleRegionChange, handleCityChange, handleRestaurantChange },
] = useEpic(restaurantFilterEpic, {
    actions: {
      handleRegionChange: ['SELECT_REGION', e => e.target.value],
      handleCityChange: ['SELECT_CITY', e => e.target.value],
      handleRestaurantChange: ['SELECT_RESTAURANT', e => e.target.value],
    }
  });

return <RestaurantSelector
  restaurant={restaurant}
  onRegionChange={handleRegionChange}
  onCityChange={handleCityChange}
  onRestaurantChange={handleRestaurantChange}
/>

The actions option should memoize the callbacks, but in a more efficient way than useCallback

Describe alternatives you've considered

After writing the examples I have to question: Is it worth it, the amount of code written is pretty close, maybe this isn't the best idea?

Suspense

Is your feature request related to a problem? Please describe.
In order to take advantage of the React suspense component, along with concurrent mode, so that w don't have to include loading: true, and the like, in our state....

Describe the solution you'd like
As always intended but I was waiting until some big announcement about data fetching and suspense at React Conf that never really came, so....

Use Suspense in React by an option called suspense which will default to false at first but may change in the future (breaking change) to default to true

If suspense is set to true, whenever the newState$ observable, returned from the Epic returns undefined, it will throw a promise, that will resolve on the next non-undefined value emitted from new state.

Describe alternatives you've considered
Of course you are free to not opt in (or opt out in the future) to this behaviour and handle your own loading states and spinners, I think the React way is to use the suspense component.

An in-range update of eslint-plugin-react is breaking the build 🚨

The devDependency eslint-plugin-react was updated from 7.18.2 to 7.18.3.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

eslint-plugin-react is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ❌ continuous-integration/travis-ci/push: The Travis CI build could not complete due to an error (Details).
  • ❌ Travis CI - Branch: The build errored.

Commits

The new version differs by 2 commits.

  • 0a717a5 Update CHANGELOG and bump version
  • 8b576be [Fix] jsx-indent: don't check literals not within JSX

See the full diff

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

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.