frintjs / frint-props

Compose reactive props in FrintJS applications
https://frint.js.org
MIT License
12 stars 1 forks source link

frint-props: create `withHandlers` #24

Closed fahad19 closed 6 years ago

fahad19 commented 6 years ago

Example usage

Basic side-effects:

const props$ = compose(
  withState('counter', 'setCounter', 0),
  withHandlers({
    handleIncrement: props => () => {
      props.setCounter(props.counter + 1);
    }
  }
)();

The props$ observable will now emit an object with these structure:

Trigger async process, that generates new props

import { of } from 'rxjs/observable/of';

const props$ = compose(
  withDefaults({ todo: null }),
  withHandlers({
    fetchTodo: (props, ...args) => todoId => {
      return of({
        todo: {
          id: 123,
          title: 'My Todo Item',
        },
      });
    }
  })
)(arg1, arg2);

The props$ Observable will emit an object:

The ...args in fetchTodo function are the same arguments as passed to compose()(...args). Which from the perspective of frint-react's observe are (app, parentProps$).

viacheslaff commented 6 years ago

I was thinking about this for a while and came to conclusion that adding this kind of functionality goes way out of the responsibility scope of compose. Handling side-effects would fit much better in store (and it's already in the frint-store in form of epics) not inline within compose(). frint-props provides withStore method to connect to data the store.

User can also have action creators. So the only thing we're missing is the way to connect dispatching action creators into the props (mapDispatchToProps in Redux terminology) and that's it.

I wouldn't go with this proposal further than adding mapDispatchToProps analogue if it doesn't exist yet.

fahad19 commented 6 years ago

I should have been careful with my usage of the word "side effect".

If you notice more closely, the handler function is still dealing with props by calling a function that is already defined in the props object.

I can really see withHandlers being very useful when we want to trigger a new async process (like fetching something from the server) at will from the component, and then return the results to the base component upon receiving the response. Not everything needs to touch the Store.

viacheslaff commented 6 years ago

Once you start having more or less complex state and operations on it, it's better to extract it to a separate entity such as a store. Then it's clear source of truth, it's easier to inspect, to debug and to understand how it works.

I don't want ad-hoc invoked compose() HOC to accumulate all the state + handlers + async side-effects. It's going to a be a spaghetti of with... functions with inline arrays of callbacks, observables. It can grow huge and unmanageable.

It's fine™ to have simple state management described inline, but growing it there is a bad idea. That's why I don't support this idea. Maybe with exception of handlers that modify props in sync way. But again, for that you can just add methods to your component.

fahad19 commented 6 years ago

Once you start having more or less complex state and operations on it, it's better to extract it to a separate entity such as a store. Then it's clear source of truth, it's easier to inspect, to debug and to understand how it works.

I understand your concerns. It's a difference in mindset between Redux (single App, single Store, single source of truth) and FrintJS (single Root App, multiple Providers, multiple Child Apps).

There is nothing stopping you from having multiple Redux stores in a single FrintJS App instance. They can be set as store and anotherStore providers for example.


I don't want ad-hoc invoked compose() HOC to accumulate all the state + handlers + async side-effects. It's going to a be a spaghetti of with... functions with inline arrays of callbacks, observables. It can grow huge and unmanageable.

That's the composition layer in custom applications. And it is more up to the developers how they use and implement it. We can always recommend them what not to do, and suggest using Stores when things get more complicated.

But I don't think that invalidates the need of a withHandler function. It is needed for triggering some process based on an incoming event generated at the base component level (like onClick).

If the state is needed to be shared across multiple components (and Child Apps), going for a Store (set as a Provider) is the way to go for sure like you mentioned.

If the state can live and die along with the component, it can just stay here. The logic doesn't need to be spread anywhere else.

And the state can be either generated in a synchronous or asynchronous way.

viacheslaff commented 6 years ago

I understand your concerns. It's a difference in mindset between Redux

The concern is the same, disregarding number of stores/providers. Complex state management should happen in separate entity (provider/store)

And it is more up to the developers how they use and implement it.

It's not completely up to the developers. Would you let (or make it easier) for developers to write callback hell? With library we define API surface we have to maintain which also includes backward compatibility. The bigger it gets the harder it becomes. So we have to think carefully before adding random functionality.